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

package hclsyntax

import (
	"reflect"
	"testing"

	"github.com/hashicorp/hcl/v2"
)

func TestBlocksAtPos(t *testing.T) {
	tests := map[string]struct {
		Src       string
		Pos       hcl.Pos
		WantTypes []string
	}{
		"empty": {
			``,
			hcl.Pos{Byte: 0},
			nil,
		},
		"spaces": {
			`    `,
			hcl.Pos{Byte: 1},
			nil,
		},
		"single in header": {
			`foo {}`,
			hcl.Pos{Byte: 1},
			[]string{"foo"},
		},
		"single in body": {
			`foo {    }`,
			hcl.Pos{Byte: 7},
			[]string{"foo"},
		},
		"single in body with unselected nested": {
			`
			foo {

				bar {

				}
			}
			`,
			hcl.Pos{Byte: 10},
			[]string{"foo"},
		},
		"single in body with unselected sibling": {
			`
			foo {  }
			bar {  }
			`,
			hcl.Pos{Byte: 10},
			[]string{"foo"},
		},
		"selected nested two levels": {
			`
			foo {
				bar {

				}
			}
			`,
			hcl.Pos{Byte: 20},
			[]string{"foo", "bar"},
		},
		"selected nested three levels": {
			`
			foo {
				bar {
					baz {

					}
				}
			}
			`,
			hcl.Pos{Byte: 31},
			[]string{"foo", "bar", "baz"},
		},
		"selected nested three levels with unselected sibling after": {
			`
			foo {
				bar {
					baz {

					}
				}
				not_wanted {}
			}
			`,
			hcl.Pos{Byte: 31},
			[]string{"foo", "bar", "baz"},
		},
		"selected nested three levels with unselected sibling before": {
			`
			foo {
				not_wanted {}
				bar {
					baz {

					}
				}
			}
			`,
			hcl.Pos{Byte: 49},
			[]string{"foo", "bar", "baz"},
		},
		"unterminated": {
			`foo {    `,
			hcl.Pos{Byte: 7},
			[]string{"foo"},
		},
		"unterminated nested": {
			`
			foo {
				bar {
			}
			`,
			hcl.Pos{Byte: 16},
			[]string{"foo", "bar"},
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			f, diags := ParseConfig([]byte(test.Src), "", hcl.Pos{Line: 1, Column: 1})
			for _, diag := range diags {
				// We intentionally ignore diagnostics here because we should be
				// able to work with the incomplete configuration that results
				// when the parser does its recovery behavior. However, we do
				// log them in case it's helpful to someone debugging a failing
				// test.
				t.Log(diag.Error())
			}

			blocks := f.BlocksAtPos(test.Pos)
			outermost := f.OutermostBlockAtPos(test.Pos)
			innermost := f.InnermostBlockAtPos(test.Pos)

			gotTypes := make([]string, len(blocks))
			for i, block := range blocks {
				gotTypes[i] = block.Type
			}

			if len(test.WantTypes) == 0 {
				if len(gotTypes) != 0 {
					t.Errorf("wrong block types\ngot:  %#v\nwant: (none)", gotTypes)
				}
				if outermost != nil {
					t.Errorf("wrong outermost type\ngot:  %#v\nwant: (none)", outermost.Type)
				}
				if innermost != nil {
					t.Errorf("wrong innermost type\ngot:  %#v\nwant: (none)", innermost.Type)
				}
				return
			}

			if !reflect.DeepEqual(gotTypes, test.WantTypes) {
				if len(gotTypes) != 0 {
					t.Errorf("wrong block types\ngot:  %#v\nwant: %#v", gotTypes, test.WantTypes)
				}
			}
			if got, want := outermost.Type, test.WantTypes[0]; got != want {
				t.Errorf("wrong outermost type\ngot:  %#v\nwant: %#v", got, want)
			}
			if got, want := innermost.Type, test.WantTypes[len(test.WantTypes)-1]; got != want {
				t.Errorf("wrong innermost type\ngot:  %#v\nwant: %#v", got, want)
			}
		})
	}
}

func TestAttributeAtPos(t *testing.T) {
	tests := map[string]struct {
		Src      string
		Pos      hcl.Pos
		WantName string
	}{
		"empty": {
			``,
			hcl.Pos{Byte: 0},
			"",
		},
		"top-level": {
			`foo = 1`,
			hcl.Pos{Byte: 0},
			"foo",
		},
		"top-level with ignored sibling after": {
			`
			foo = 1
			bar = 2
			`,
			hcl.Pos{Byte: 6},
			"foo",
		},
		"top-level ignored sibling before": {
			`
			foo = 1
			bar = 2
			`,
			hcl.Pos{Byte: 17},
			"bar",
		},
		"nested": {
			`
			foo {
				bar = 2
			}
			`,
			hcl.Pos{Byte: 17},
			"bar",
		},
		"nested in unterminated block": {
			`
			foo {
				bar = 2
			`,
			hcl.Pos{Byte: 17},
			"bar",
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			f, diags := ParseConfig([]byte(test.Src), "", hcl.Pos{Line: 1, Column: 1})
			for _, diag := range diags {
				// We intentionally ignore diagnostics here because we should be
				// able to work with the incomplete configuration that results
				// when the parser does its recovery behavior. However, we do
				// log them in case it's helpful to someone debugging a failing
				// test.
				t.Log(diag.Error())
			}

			got := f.AttributeAtPos(test.Pos)

			if test.WantName == "" {
				if got != nil {
					t.Errorf("wrong attribute name\ngot:  %#v\nwant: (none)", got.Name)
				}
				return
			}

			if got == nil {
				t.Fatalf("wrong attribute name\ngot:  (none)\nwant: %#v", test.WantName)
			}

			if got.Name != test.WantName {
				t.Errorf("wrong attribute name\ngot:  %#v\nwant: %#v", got.Name, test.WantName)
			}
		})
	}
}

func TestOutermostExprAtPos(t *testing.T) {
	tests := map[string]struct {
		Src     string
		Pos     hcl.Pos
		WantSrc string
	}{
		"empty": {
			``,
			hcl.Pos{Byte: 0},
			``,
		},
		"simple bool": {
			`a = true`,
			hcl.Pos{Byte: 6},
			`true`,
		},
		"simple reference": {
			`a = blah`,
			hcl.Pos{Byte: 6},
			`blah`,
		},
		"attribute reference": {
			`a = blah.foo`,
			hcl.Pos{Byte: 6},
			`blah.foo`,
		},
		"parens": {
			`a = (1 + 1)`,
			hcl.Pos{Byte: 6},
			`(1 + 1)`,
		},
		"tuple cons": {
			`a = [1, 2, 3]`,
			hcl.Pos{Byte: 5},
			`[1, 2, 3]`,
		},
		"function call": {
			`a = foom("a")`,
			hcl.Pos{Byte: 10},
			`foom("a")`,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			inputSrc := []byte(test.Src)
			f, diags := ParseConfig(inputSrc, "", hcl.Pos{Line: 1, Column: 1})
			for _, diag := range diags {
				// We intentionally ignore diagnostics here because we should be
				// able to work with the incomplete configuration that results
				// when the parser does its recovery behavior. However, we do
				// log them in case it's helpful to someone debugging a failing
				// test.
				t.Log(diag.Error())
			}

			gotExpr := f.OutermostExprAtPos(test.Pos)
			var gotSrc string
			if gotExpr != nil {
				rng := gotExpr.Range()
				gotSrc = string(rng.SliceBytes(inputSrc))
			}

			if test.WantSrc == "" {
				if gotExpr != nil {
					t.Errorf("wrong expression source\ngot:  %s\nwant: (none)", gotSrc)
				}
				return
			}

			if gotExpr == nil {
				t.Fatalf("wrong expression source\ngot:  (none)\nwant: %s", test.WantSrc)
			}

			if gotSrc != test.WantSrc {
				t.Errorf("wrong expression source\ngot:  %#v\nwant: %#v", gotSrc, test.WantSrc)
			}
		})
	}
}
