// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package hclsyntax

import (
	"fmt"
	"reflect"
	"testing"

	"github.com/davecgh/go-spew/spew"
	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty-debug/ctydebug"
	"github.com/zclconf/go-cty/cty"
)

func TestBodyContent(t *testing.T) {
	tests := []struct {
		body      *Body
		schema    *hcl.BodySchema
		partial   bool
		want      *hcl.BodyContent
		diagCount int
	}{
		{
			&Body{},
			&hcl.BodySchema{},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			0,
		},

		// Attributes
		{
			&Body{
				Attributes: Attributes{
					"foo": &Attribute{
						Name: "foo",
					},
				},
			},
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name: "foo",
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{
					"foo": &hcl.Attribute{
						Name: "foo",
					},
				},
			},
			0,
		},
		{
			&Body{
				Attributes: Attributes{
					"foo": &Attribute{
						Name: "foo",
					},
				},
			},
			&hcl.BodySchema{},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			1, // attribute "foo" is not expected
		},
		{
			&Body{
				Attributes: Attributes{
					"foo": &Attribute{
						Name: "foo",
					},
				},
			},
			&hcl.BodySchema{},
			true,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			0, // in partial mode, so extra "foo" is acceptable
		},
		{
			&Body{
				Attributes: Attributes{},
			},
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name: "foo",
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			0, // "foo" not required, so no error
		},
		{
			&Body{
				Attributes: Attributes{},
			},
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name:     "foo",
						Required: true,
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			1, // "foo" is required
		},
		{
			&Body{
				Attributes: Attributes{
					"foo": &Attribute{
						Name: "foo",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "foo",
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			1, // attribute "foo" not expected (it's defined as a block)
		},

		// Blocks
		{
			&Body{
				Blocks: Blocks{
					&Block{
						Type: "foo",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "foo",
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks: hcl.Blocks{
					{
						Type: "foo",
						Body: (*Body)(nil),
					},
				},
			},
			0,
		},
		{
			&Body{
				Blocks: Blocks{
					&Block{
						Type: "foo",
					},
					&Block{
						Type: "foo",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "foo",
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks: hcl.Blocks{
					{
						Type: "foo",
						Body: (*Body)(nil),
					},
					{
						Type: "foo",
						Body: (*Body)(nil),
					},
				},
			},
			0,
		},
		{
			&Body{
				Blocks: Blocks{
					&Block{
						Type: "foo",
					},
					&Block{
						Type: "bar",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "foo",
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks: hcl.Blocks{
					{
						Type: "foo",
						Body: (*Body)(nil),
					},
				},
			},
			1, // blocks of type "bar" not expected
		},
		{
			&Body{
				Blocks: Blocks{
					&Block{
						Type: "foo",
					},
					&Block{
						Type: "bar",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "foo",
					},
				},
			},
			true,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks: hcl.Blocks{
					{
						Type: "foo",
						Body: (*Body)(nil),
					},
				},
			},
			0, // extra "bar" allowed because we're in partial mode
		},
		{
			&Body{
				Blocks: Blocks{
					&Block{
						Type:   "foo",
						Labels: []string{"bar"},
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type:       "foo",
						LabelNames: []string{"name"},
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks: hcl.Blocks{
					{
						Type:   "foo",
						Labels: []string{"bar"},
						Body:   (*Body)(nil),
					},
				},
			},
			0,
		},
		{
			&Body{
				Blocks: Blocks{
					&Block{
						Type: "foo",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type:       "foo",
						LabelNames: []string{"name"},
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			1, // missing label "name"
		},
		{
			&Body{
				Blocks: Blocks{
					&Block{
						Type:   "foo",
						Labels: []string{"bar"},

						LabelRanges: []hcl.Range{{}},
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "foo",
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			1, // no labels expected
		},
		{
			&Body{
				Blocks: Blocks{
					&Block{
						Type:   "foo",
						Labels: []string{"bar", "baz"},

						LabelRanges: []hcl.Range{{}, {}},
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type:       "foo",
						LabelNames: []string{"name"},
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			1, // too many labels
		},
		{
			&Body{
				Attributes: Attributes{
					"foo": &Attribute{
						Name: "foo",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "foo",
					},
				},
			},
			false,
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			1, // should've been a block, not an attribute
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
			var got *hcl.BodyContent
			var diags hcl.Diagnostics
			if test.partial {
				got, _, diags = test.body.PartialContent(test.schema)
			} else {
				got, diags = test.body.Content(test.schema)
			}

			if len(diags) != test.diagCount {
				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
				for _, diag := range diags {
					t.Logf(" - %s", diag.Error())
				}
			}

			if !reflect.DeepEqual(got, test.want) {
				t.Errorf(
					"wrong result\ndiff: %s",
					cmp.Diff(test.want, got),
				)
			}
		})
	}
}

func TestBodyJustAttributes(t *testing.T) {
	tests := []struct {
		body      *Body
		want      hcl.Attributes
		diagCount int
	}{
		{
			&Body{},
			hcl.Attributes{},
			0,
		},
		{
			&Body{
				Attributes: Attributes{},
			},
			hcl.Attributes{},
			0,
		},
		{
			&Body{
				Attributes: Attributes{
					"foo": &Attribute{
						Name: "foo",
						Expr: &LiteralValueExpr{
							Val: cty.StringVal("bar"),
						},
					},
				},
			},
			hcl.Attributes{
				"foo": &hcl.Attribute{
					Name: "foo",
					Expr: &LiteralValueExpr{
						Val: cty.StringVal("bar"),
					},
				},
			},
			0,
		},
		{
			&Body{
				Attributes: Attributes{
					"foo": &Attribute{
						Name: "foo",
						Expr: &LiteralValueExpr{
							Val: cty.StringVal("bar"),
						},
					},
				},
				Blocks: Blocks{
					{
						Type: "foo",
					},
				},
			},
			hcl.Attributes{
				"foo": &hcl.Attribute{
					Name: "foo",
					Expr: &LiteralValueExpr{
						Val: cty.StringVal("bar"),
					},
				},
			},
			1, // blocks are not allowed here
		},
		{
			&Body{
				Attributes: Attributes{
					"foo": &Attribute{
						Name: "foo",
						Expr: &LiteralValueExpr{
							Val: cty.StringVal("bar"),
						},
					},
				},
				hiddenAttrs: map[string]struct{}{
					"foo": {},
				},
			},
			hcl.Attributes{},
			0,
		},
	}

	for i, test := range tests {
		t.Run(fmt.Sprintf("%02d", i), func(t *testing.T) {
			got, diags := test.body.JustAttributes()

			if len(diags) != test.diagCount {
				t.Errorf("wrong number of diagnostics %d; want %d", len(diags), test.diagCount)
				for _, diag := range diags {
					t.Logf(" - %s", diag.Error())
				}
			}

			diff := cmp.Diff(
				test.want, got,
				ctydebug.CmpOptions,
			)
			if diff != "" {
				t.Errorf(
					"wrong result\nbody: %s\ndiff: %s",
					spew.Sdump(test.body),
					diff,
				)
			}
		})
	}
}
