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

package hcltest

import (
	"strings"
	"testing"

	"reflect"

	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty/cty"
)

// Assert mocks are compatible with the interfaces they implement.
var _ hcl.Body = mockBody{}
var _ hcl.Expression = mockExprLiteral{}
var _ hcl.Expression = mockExprVariable("")

func TestMockBodyPartialContent(t *testing.T) {
	tests := map[string]struct {
		In        *hcl.BodyContent
		Schema    *hcl.BodySchema
		Want      *hcl.BodyContent
		Remain    *hcl.BodyContent
		DiagCount int
	}{
		"empty": {
			&hcl.BodyContent{},
			&hcl.BodySchema{},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			0,
		},
		"attribute requested": {
			&hcl.BodyContent{
				Attributes: MockAttrs(map[string]hcl.Expression{
					"name": MockExprLiteral(cty.StringVal("Ermintrude")),
				}),
			},
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name: "name",
					},
				},
			},
			&hcl.BodyContent{
				Attributes: MockAttrs(map[string]hcl.Expression{
					"name": MockExprLiteral(cty.StringVal("Ermintrude")),
				}),
				Blocks: hcl.Blocks{},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			0,
		},
		"attribute remains": {
			&hcl.BodyContent{
				Attributes: MockAttrs(map[string]hcl.Expression{
					"name": MockExprLiteral(cty.StringVal("Ermintrude")),
				}),
			},
			&hcl.BodySchema{},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			&hcl.BodyContent{
				Attributes: MockAttrs(map[string]hcl.Expression{
					"name": MockExprLiteral(cty.StringVal("Ermintrude")),
				}),
				Blocks: hcl.Blocks{},
			},
			0,
		},
		"attribute missing": {
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
			},
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name:     "name",
						Required: true,
					},
				},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			1, // missing attribute "name"
		},
		"block requested, no labels": {
			&hcl.BodyContent{
				Blocks: hcl.Blocks{
					{
						Type: "baz",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type: "baz",
					},
				},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks: hcl.Blocks{
					{
						Type: "baz",
					},
				},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			0,
		},
		"block requested, wrong labels": {
			&hcl.BodyContent{
				Blocks: hcl.Blocks{
					{
						Type: "baz",
					},
				},
			},
			&hcl.BodySchema{
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type:       "baz",
						LabelNames: []string{"foo"},
					},
				},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks: hcl.Blocks{
					{
						Type: "baz",
					},
				},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			1, // "baz" requires 1 label
		},
		"block remains": {
			&hcl.BodyContent{
				Blocks: hcl.Blocks{
					{
						Type: "baz",
					},
				},
			},
			&hcl.BodySchema{},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks:     hcl.Blocks{},
			},
			&hcl.BodyContent{
				Attributes: hcl.Attributes{},
				Blocks: hcl.Blocks{
					{
						Type: "baz",
					},
				},
			},
			0,
		},
		"various": {
			&hcl.BodyContent{
				Attributes: MockAttrs(map[string]hcl.Expression{
					"name": MockExprLiteral(cty.StringVal("Ermintrude")),
					"age":  MockExprLiteral(cty.NumberIntVal(32)),
				}),
				Blocks: hcl.Blocks{
					{
						Type: "baz",
					},
					{
						Type:   "bar",
						Labels: []string{"foo1"},
					},
					{
						Type:   "bar",
						Labels: []string{"foo2"},
					},
				},
			},
			&hcl.BodySchema{
				Attributes: []hcl.AttributeSchema{
					{
						Name: "name",
					},
				},
				Blocks: []hcl.BlockHeaderSchema{
					{
						Type:       "bar",
						LabelNames: []string{"name"},
					},
				},
			},
			&hcl.BodyContent{
				Attributes: MockAttrs(map[string]hcl.Expression{
					"name": MockExprLiteral(cty.StringVal("Ermintrude")),
				}),
				Blocks: hcl.Blocks{
					{
						Type:   "bar",
						Labels: []string{"foo1"},
					},
					{
						Type:   "bar",
						Labels: []string{"foo2"},
					},
				},
			},
			&hcl.BodyContent{
				Attributes: MockAttrs(map[string]hcl.Expression{
					"age": MockExprLiteral(cty.NumberIntVal(32)),
				}),
				Blocks: hcl.Blocks{
					{
						Type: "baz",
					},
				},
			},
			0,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			inBody := MockBody(test.In)
			got, remainBody, diags := inBody.PartialContent(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)
				}
			}

			if !reflect.DeepEqual(got, test.Want) {
				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
			}

			gotRemain := remainBody.(mockBody).C
			if !reflect.DeepEqual(gotRemain, test.Remain) {
				t.Errorf("wrong remain\ngot:  %#v\nwant: %#v", gotRemain, test.Remain)
			}
		})
	}
}

func TestExprList(t *testing.T) {
	tests := map[string]struct {
		In    hcl.Expression
		Want  []hcl.Expression
		Diags string
	}{
		"as list": {
			In: MockExprLiteral(cty.ListVal([]cty.Value{
				cty.StringVal("foo"),
				cty.StringVal("bar"),
			})),
			Want: []hcl.Expression{
				MockExprLiteral(cty.StringVal("foo")),
				MockExprLiteral(cty.StringVal("bar")),
			},
		},
		"as tuple": {
			In: MockExprLiteral(cty.TupleVal([]cty.Value{
				cty.StringVal("foo"),
				cty.StringVal("bar"),
			})),
			Want: []hcl.Expression{
				MockExprLiteral(cty.StringVal("foo")),
				MockExprLiteral(cty.StringVal("bar")),
			},
		},
		"not list": {
			In: MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
				"a": cty.StringVal("foo"),
				"b": cty.StringVal("bar"),
			})),
			Want:  nil,
			Diags: "list expression is required",
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			got, diags := hcl.ExprList(tc.In)
			if tc.Diags != "" {
				if diags.HasErrors() && !strings.Contains(diags.Error(), tc.Diags) {
					t.Errorf("expected error %q, got %q", tc.Diags, diags)
				}
				if !diags.HasErrors() {
					t.Errorf("expected diagnostic message %q", tc.Diags)
				}
			} else if diags.HasErrors() {
				t.Error(diags)
			}

			if !reflect.DeepEqual(got, tc.Want) {
				t.Errorf("incorrect expression,\ngot:  %#v\nwant: %#v", got, tc.Want)
			}
		})
	}
}

func TestExprMap(t *testing.T) {
	tests := map[string]struct {
		In    hcl.Expression
		Want  []hcl.KeyValuePair
		Diags string
	}{
		"as object": {
			In: MockExprLiteral(cty.ObjectVal(map[string]cty.Value{
				"name":  cty.StringVal("test"),
				"count": cty.NumberIntVal(2),
			})),
			Want: []hcl.KeyValuePair{
				{
					Key:   MockExprLiteral(cty.StringVal("count")),
					Value: MockExprLiteral(cty.NumberIntVal(2)),
				},
				{
					Key:   MockExprLiteral(cty.StringVal("name")),
					Value: MockExprLiteral(cty.StringVal("test")),
				},
			},
		},
		"as map": {
			In: MockExprLiteral(cty.MapVal(map[string]cty.Value{
				"name":    cty.StringVal("test"),
				"version": cty.StringVal("2.0.0"),
			})),
			Want: []hcl.KeyValuePair{
				{
					Key:   MockExprLiteral(cty.StringVal("name")),
					Value: MockExprLiteral(cty.StringVal("test")),
				},
				{
					Key:   MockExprLiteral(cty.StringVal("version")),
					Value: MockExprLiteral(cty.StringVal("2.0.0")),
				},
			},
		},
		"not map": {
			In: MockExprLiteral(cty.ListVal([]cty.Value{
				cty.StringVal("foo"),
				cty.StringVal("bar"),
			})),
			Want:  nil,
			Diags: "map expression is required",
		},
	}

	for name, tc := range tests {
		t.Run(name, func(t *testing.T) {
			got, diags := hcl.ExprMap(tc.In)
			if tc.Diags != "" {
				if diags.HasErrors() && !strings.Contains(diags.Error(), tc.Diags) {
					t.Errorf("expected error %q, got %q", tc.Diags, diags)
				}
				if !diags.HasErrors() {
					t.Errorf("expected diagnostic message %q", tc.Diags)
				}
			} else if diags.HasErrors() {
				t.Error(diags)
			}

			if !reflect.DeepEqual(got, tc.Want) {
				t.Errorf("incorrect expression,\ngot:  %#v\nwant: %#v", got, tc.Want)
			}
		})
	}
}
