// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: MPL-2.0

package reference

import (
	"context"
	"fmt"
	"testing"

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

func TestTargets_Match(t *testing.T) {
	testCases := []struct {
		name            string
		targets         Targets
		origin          MatchableOrigin
		expectedTargets Targets
		expectedFound   bool
	}{
		{
			"no targets",
			Targets{},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "var"},
					lang.AttrStep{Name: "test"},
				},
			},
			Targets{},
			false,
		},
		{
			"single match",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "test"},
					},
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "var"},
					lang.AttrStep{Name: "test"},
				},
				Constraints: OriginConstraints{{}},
			},
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "test"},
					},
				},
			},
			true,
		},
		{
			"first of two matches",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "data"},
						lang.AttrStep{Name: "foo"},
					},
					Type: cty.Bool,
				},
				{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "test"},
					},
					Type: cty.Bool,
				},
				{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "test"},
					},
					ScopeId: lang.ScopeId("variable"),
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "var"},
					lang.AttrStep{Name: "test"},
				},
				Constraints: OriginConstraints{
					{OfType: cty.Bool},
				},
			},
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "test"},
					},
					Type: cty.Bool,
				},
			},
			true,
		},
		{
			"match of unknown type",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "data"},
						lang.AttrStep{Name: "foo"},
					},
					Type: cty.Bool,
				},
				{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "foo"},
					},
					Type: cty.DynamicPseudoType,
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "var"},
					lang.AttrStep{Name: "foo"},
					lang.AttrStep{Name: "bar"},
				},
				Constraints: OriginConstraints{{}},
			},
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "foo"},
					},
					Type: cty.DynamicPseudoType,
				},
			},
			true,
		},
		{
			"match of nested target",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "data"},
						lang.AttrStep{Name: "foo"},
					},
					Type: cty.Bool,
				},
				{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "foo"},
					},
					Type: cty.Object(map[string]cty.Type{
						"bar": cty.String,
					}),
					NestedTargets: Targets{
						{
							Addr: lang.Address{
								lang.RootStep{Name: "var"},
								lang.AttrStep{Name: "foo"},
								lang.AttrStep{Name: "bar"},
							},
							Type: cty.String,
						},
					},
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "var"},
					lang.AttrStep{Name: "foo"},
					lang.AttrStep{Name: "bar"},
				},
				Constraints: OriginConstraints{
					{OfType: cty.String},
				},
			},
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "foo"},
						lang.AttrStep{Name: "bar"},
					},
					Type: cty.String,
				},
			},
			true,
		},
		{
			"match of global nested target with local addrs set",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "aws_acmpca_certificate"},
						lang.AttrStep{Name: "foo"},
					},
					LocalAddr: lang.Address{
						lang.RootStep{Name: "self"},
					},
					Type: cty.Object(map[string]cty.Type{
						"signing_algorithm": cty.String,
					}),
					ScopeId: "resource",
					NestedTargets: Targets{
						{
							Addr: lang.Address{
								lang.RootStep{Name: "aws_acmpca_certificate"},
								lang.AttrStep{Name: "foo"},
								lang.AttrStep{Name: "signing_algorithm"},
							},
							LocalAddr: lang.Address{
								lang.RootStep{Name: "self"},
								lang.AttrStep{Name: "signing_algorithm"},
							},
							Type: cty.String,
							TargetableFromRangePtr: &hcl.Range{
								Filename: "main.tf",
								Start:    hcl.Pos{Line: 26, Column: 41, Byte: 360},
								End:      hcl.Pos{Line: 41, Column: 2, Byte: 657},
							},
						},
					},
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "aws_acmpca_certificate"},
					lang.AttrStep{Name: "foo"},
					lang.AttrStep{Name: "signing_algorithm"},
				},

				Constraints: OriginConstraints{
					{OfType: cty.DynamicPseudoType},
				},
				Range: hcl.Range{
					Filename: "main.tf",
					Start:    hcl.Pos{Line: 44, Column: 11, Byte: 684},
					End:      hcl.Pos{Line: 44, Column: 55, Byte: 728},
				},
			},
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "aws_acmpca_certificate"},
						lang.AttrStep{Name: "foo"},
						lang.AttrStep{Name: "signing_algorithm"},
					},
					LocalAddr: lang.Address{
						lang.RootStep{Name: "self"},
						lang.AttrStep{Name: "signing_algorithm"},
					},
					Type: cty.String,
					TargetableFromRangePtr: &hcl.Range{
						Filename: "main.tf",
						Start:    hcl.Pos{Line: 26, Column: 41, Byte: 360},
						End:      hcl.Pos{Line: 41, Column: 2, Byte: 657},
					},
				},
			},
			true,
		},
		{
			"matches empty tuple",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "local"},
						lang.AttrStep{Name: "a"},
					},
					Type: cty.Tuple([]cty.Type{
						cty.String,
						cty.Bool,
					}),
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "local"},
					lang.AttrStep{Name: "a"},
				},
				Constraints: OriginConstraints{{
					OfType: cty.EmptyTuple,
				}},
			},
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "local"},
						lang.AttrStep{Name: "a"},
					},
					Type: cty.Tuple([]cty.Type{
						cty.String,
						cty.Bool,
					}),
				},
			},
			true,
		},
	}
	for i, tc := range testCases {
		t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) {
			refTarget, ok := tc.targets.Match(tc.origin)
			if !ok && tc.expectedFound {
				t.Fatalf("expected targetable to be found")
			}

			if diff := cmp.Diff(tc.expectedTargets, refTarget, ctydebug.CmpOptions); diff != "" {
				t.Fatalf("mismatch of reference target: %s", diff)
			}
		})
	}
}

func TestTargets_Match_localRefs(t *testing.T) {
	testCases := []struct {
		name            string
		targets         Targets
		origin          MatchableOrigin
		expectedTargets Targets
		expectedMatch   bool
	}{
		{
			"no targets",
			Targets{},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "count"},
					lang.AttrStep{Name: "index"},
				},
			},
			Targets{},
			false,
		},
		{
			"local address",
			Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "count"},
						lang.AttrStep{Name: "index"},
					},
					Type: cty.Number,
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "count"},
					lang.AttrStep{Name: "index"},
				},
			},
			Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "count"},
						lang.AttrStep{Name: "index"},
					},
					Type: cty.Number,
				},
			},
			true,
		},
		{
			"local address with Constraint",
			Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "count"},
						lang.AttrStep{Name: "index"},
					},
					Type: cty.Number,
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "count"},
					lang.AttrStep{Name: "index"},
				},
				Constraints: OriginConstraints{
					{OfType: cty.Number},
				},
			},
			Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "count"},
						lang.AttrStep{Name: "index"},
					},
					Type: cty.Number,
				},
			},
			true,
		},
		{
			"local address with Type and TargetableFromRange positive",
			Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "count"},
						lang.AttrStep{Name: "index"},
					},
					Type: cty.Number,
					TargetableFromRangePtr: &hcl.Range{
						Filename: "test.tf",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 5, Column: 1, Byte: 50},
					},
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "count"},
					lang.AttrStep{Name: "index"},
				},
				Range: hcl.Range{
					Filename: "test.tf",
					Start:    hcl.Pos{Line: 2, Column: 1, Byte: 20},
					End:      hcl.Pos{Line: 2, Column: 10, Byte: 29},
				},
				Constraints: OriginConstraints{
					{OfType: cty.Number},
				},
			},
			Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "count"},
						lang.AttrStep{Name: "index"},
					},
					Type: cty.Number,
					TargetableFromRangePtr: &hcl.Range{
						Filename: "test.tf",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 5, Column: 1, Byte: 50},
					},
				},
			},
			true,
		},
		{
			"local address with Type and TargetableFromRange negative",
			Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "count"},
						lang.AttrStep{Name: "index"},
					},
					Type: cty.Number,
					TargetableFromRangePtr: &hcl.Range{
						Filename: "test.tf",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 5, Column: 1, Byte: 20},
					},
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "count"},
					lang.AttrStep{Name: "index"},
				},
				Range: hcl.Range{
					Filename: "test.tf",
					Start:    hcl.Pos{Line: 2, Column: 1, Byte: 20},
					End:      hcl.Pos{Line: 2, Column: 10, Byte: 29},
				},
				Constraints: OriginConstraints{
					{OfType: cty.Number},
				},
			},
			Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "count"},
						lang.AttrStep{Name: "index"},
					},
					Type: cty.Number,
					TargetableFromRangePtr: &hcl.Range{
						Filename: "test.tf",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 5, Column: 1, Byte: 20},
					},
				},
			},
			true,
		},
		{
			"local origin and global target",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "module"},
						lang.AttrStep{Name: "localmodd"},
						lang.AttrStep{Name: "someattribute"},
					},
					Type: cty.DynamicPseudoType,
				},
			},
			LocalOrigin{
				Addr: lang.Address{
					lang.RootStep{Name: "self"},
					lang.AttrStep{Name: "attribute"},
				},
				Constraints: OriginConstraints{
					{
						OfScopeId: "",
						OfType:    cty.String,
					},
				},
			},
			Targets{},
			false,
		},
	}
	for i, tc := range testCases {
		t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) {
			refTarget, ok := tc.targets.Match(tc.origin)
			if !ok && tc.expectedMatch {
				t.Fatalf("expected target to be matched")
			}

			if diff := cmp.Diff(tc.expectedTargets, refTarget, ctydebug.CmpOptions); diff != "" {
				t.Fatalf("mismatch of reference target: %s", diff)
			}
		})
	}
}

func TestTargets_MatchWalk_nestedBlockFiltering(t *testing.T) {
	testCases := []struct {
		name              string
		targets           Targets
		ref               schema.Reference
		prefix            string
		outermostBodyRng  hcl.Range
		originRng         hcl.Range
		innermostBlockRng hcl.Range
		expectedTargets   Targets
		expectedMatch     bool
	}{
		{
			name: "reject block-scoped local from same nested block",
			targets: Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "local"},
						lang.AttrStep{Name: "key1"},
					},
					BlockScoped: true,
					Type:        cty.Bool,
					RangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 3, Column: 5, Byte: 50},
						End:      hcl.Pos{Line: 3, Column: 17, Byte: 62},
					},
					TargetableFromRangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 10, Column: 2, Byte: 200},
					},
				},
			},
			ref:    schema.Reference{OfType: cty.Bool},
			prefix: "local",
			// outermostBodyRng is the resource_policy block body
			outermostBodyRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:      hcl.Pos{Line: 10, Column: 2, Byte: 200},
			},
			// origin is inside the same locals block where key1 is defined
			originRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 4, Column: 13, Byte: 70},
				End:      hcl.Pos{Line: 4, Column: 19, Byte: 76},
			},
			// innermostBlockRng is the locals block containing both target and origin
			innermostBlockRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 2, Column: 3, Byte: 30},
				End:      hcl.Pos{Line: 5, Column: 4, Byte: 90},
			},
			expectedTargets: Targets{},
			expectedMatch:   false,
		},
		{
			name: "accept block-scoped local from sibling block",
			targets: Targets{
				{
					// Block-scoped local defined in locals block
					LocalAddr: lang.Address{
						lang.RootStep{Name: "local"},
						lang.AttrStep{Name: "key1"},
					},
					BlockScoped: true,
					Type:        cty.Bool,
					RangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 3, Column: 5, Byte: 50},
						End:      hcl.Pos{Line: 3, Column: 17, Byte: 62},
					},
					TargetableFromRangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 10, Column: 2, Byte: 200},
					},
				},
			},
			ref:    schema.Reference{OfType: cty.Bool},
			prefix: "local",
			// outermostBodyRng is the resource_policy block body
			outermostBodyRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
				End:      hcl.Pos{Line: 10, Column: 2, Byte: 200},
			},
			// origin is inside an enforce block (sibling of locals)
			originRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 8, Column: 13, Byte: 150},
				End:      hcl.Pos{Line: 8, Column: 25, Byte: 162},
			},
			// innermostBlockRng is the enforce block (different from locals block)
			innermostBlockRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 6, Column: 3, Byte: 100},
				End:      hcl.Pos{Line: 9, Column: 4, Byte: 180},
			},
			expectedTargets: Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "local"},
						lang.AttrStep{Name: "key1"},
					},
					BlockScoped: true,
					Type:        cty.Bool,
					RangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 3, Column: 5, Byte: 50},
						End:      hcl.Pos{Line: 3, Column: 17, Byte: 62},
					},
					TargetableFromRangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 10, Column: 2, Byte: 200},
					},
				},
			},
			expectedMatch: true,
		},
		{
			name: "reject block-scoped local from outside parent block",
			targets: Targets{
				{
					LocalAddr: lang.Address{
						lang.RootStep{Name: "local"},
						lang.AttrStep{Name: "key1"},
					},
					BlockScoped: true,
					Type:        cty.Bool,
					RangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 3, Column: 5, Byte: 50},
						End:      hcl.Pos{Line: 3, Column: 17, Byte: 62},
					},
					TargetableFromRangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
						End:      hcl.Pos{Line: 10, Column: 2, Byte: 200},
					},
				},
			},
			ref:    schema.Reference{OfType: cty.Bool},
			prefix: "local",
			// outermostBodyRng is a different resource_policy block
			outermostBodyRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 12, Column: 1, Byte: 210},
				End:      hcl.Pos{Line: 20, Column: 2, Byte: 400},
			},
			// origin is outside the TargetableFromRangePtr
			originRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 15, Column: 13, Byte: 300},
				End:      hcl.Pos{Line: 15, Column: 25, Byte: 312},
			},
			innermostBlockRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 12, Column: 1, Byte: 210},
				End:      hcl.Pos{Line: 20, Column: 2, Byte: 400},
			},
			expectedTargets: Targets{},
			expectedMatch:   false,
		},
		{
			name: "accept non-block-scoped local (root-level)",
			targets: Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "local"},
						lang.AttrStep{Name: "rootKey1"},
					},
					Type: cty.Bool,
					RangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 2, Column: 3, Byte: 15},
						End:      hcl.Pos{Line: 2, Column: 19, Byte: 31},
					},
				},
			},
			ref:    schema.Reference{OfType: cty.Bool},
			prefix: "local",
			outermostBodyRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 10, Column: 1, Byte: 200},
				End:      hcl.Pos{Line: 20, Column: 2, Byte: 400},
			},
			originRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 18, Column: 13, Byte: 350},
				End:      hcl.Pos{Line: 18, Column: 25, Byte: 362},
			},
			innermostBlockRng: hcl.Range{
				Filename: "test.policy.hcl",
				Start:    hcl.Pos{Line: 12, Column: 10, Byte: 230},
				End:      hcl.Pos{Line: 19, Column: 3, Byte: 380},
			},
			expectedTargets: Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "local"},
						lang.AttrStep{Name: "rootKey1"},
					},
					Type: cty.Bool,
					RangePtr: &hcl.Range{
						Filename: "test.policy.hcl",
						Start:    hcl.Pos{Line: 2, Column: 3, Byte: 15},
						End:      hcl.Pos{Line: 2, Column: 19, Byte: 31},
					},
				},
			},
			expectedMatch: true,
		},
	}

	for i, tc := range testCases {
		t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) {
			ctx := context.Background()
			matchedTargets := Targets{}

			tc.targets.MatchWalk(ctx, tc.ref, tc.prefix, tc.outermostBodyRng, tc.originRng, tc.innermostBlockRng, func(target Target) error {
				matchedTargets = append(matchedTargets, target)
				return nil
			})

			// Check if we got a match
			gotMatch := len(matchedTargets) > 0
			if gotMatch != tc.expectedMatch {
				t.Fatalf("expected match=%v, got match=%v (matched %d targets)", tc.expectedMatch, gotMatch, len(matchedTargets))
			}

			// Compare the actual targets returned
			if diff := cmp.Diff(tc.expectedTargets, matchedTargets, ctydebug.CmpOptions); diff != "" {
				t.Fatalf("mismatch of matched targets: %s", diff)
			}
		})
	}
}

func TestTargets_OutermostInFile(t *testing.T) {
	testCases := []struct {
		name            string
		targets         Targets
		filename        string
		expectedTargets Targets
	}{
		{
			"no targets",
			Targets{},
			"test.tf",
			Targets{},
		},
		{
			"mismatching filename",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "foo"},
					},
					RangePtr: &hcl.Range{
						Filename: "bar.tf",
						Start:    hcl.InitialPos,
						End: hcl.Pos{
							Line:   2,
							Column: 1,
							Byte:   10,
						},
					},
				},
			},
			"test.tf",
			Targets{},
		},
		{
			"matching file",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "foo"},
					},
					RangePtr: &hcl.Range{
						Filename: "test.tf",
						Start:    hcl.InitialPos,
						End: hcl.Pos{
							Line:   2,
							Column: 1,
							Byte:   10,
						},
					},
					NestedTargets: Targets{
						{
							Addr: lang.Address{
								lang.RootStep{Name: "foo"},
								lang.AttrStep{Name: "bar"},
							},
							RangePtr: &hcl.Range{
								Filename: "test.tf",
								Start:    hcl.InitialPos,
								End: hcl.Pos{
									Line:   1,
									Column: 5,
									Byte:   4,
								},
							},
						},
					},
				},
			},
			"test.tf",
			Targets{
				{
					Addr: lang.Address{
						lang.RootStep{Name: "foo"},
					},
					RangePtr: &hcl.Range{
						Filename: "test.tf",
						Start:    hcl.InitialPos,
						End: hcl.Pos{
							Line:   2,
							Column: 1,
							Byte:   10,
						},
					},
					NestedTargets: Targets{
						{
							Addr: lang.Address{
								lang.RootStep{Name: "foo"},
								lang.AttrStep{Name: "bar"},
							},
							RangePtr: &hcl.Range{
								Filename: "test.tf",
								Start:    hcl.InitialPos,
								End: hcl.Pos{
									Line:   1,
									Column: 5,
									Byte:   4,
								},
							},
						},
					},
				},
			},
		},
	}
	for i, tc := range testCases {
		t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) {
			targets := tc.targets.OutermostInFile(tc.filename)

			if diff := cmp.Diff(tc.expectedTargets, targets, ctydebug.CmpOptions); diff != "" {
				t.Fatalf("mismatch of targets: %s", diff)
			}
		})
	}
}

func TestTargets_InnermostAtPos(t *testing.T) {
	testCases := []struct {
		name            string
		targets         Targets
		file            string
		pos             hcl.Pos
		expectedTargets Targets
		expectedFound   bool
	}{
		{
			"no targets",
			Targets{},
			"test.tf",
			hcl.InitialPos,
			Targets{},
			false,
		},
		{
			"target without range",
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "test"},
					},
				},
			},
			"test.tf",
			hcl.InitialPos,
			Targets{},
			false,
		},
		{
			"top level match",
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "first"},
					},
					RangePtr: &hcl.Range{
						Filename: "test.tf",
						Start: hcl.Pos{
							Line:   1,
							Column: 1,
							Byte:   0,
						},
						End: hcl.Pos{
							Line:   3,
							Column: 2,
							Byte:   30,
						},
					},
				},
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "second"},
					},
					RangePtr: &hcl.Range{
						Filename: "another.tf",
						Start: hcl.Pos{
							Line:   1,
							Column: 1,
							Byte:   0,
						},
						End: hcl.Pos{
							Line:   3,
							Column: 2,
							Byte:   30,
						},
					},
				},
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "third"},
					},
					RangePtr: &hcl.Range{
						Filename: "test.tf",
						Start: hcl.Pos{
							Line:   5,
							Column: 1,
							Byte:   32,
						},
						End: hcl.Pos{
							Line:   7,
							Column: 2,
							Byte:   60,
						},
					},
				},
			},
			"test.tf",
			hcl.Pos{
				Line:   6,
				Column: 1,
				Byte:   50,
			},
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "third"},
					},
					RangePtr: &hcl.Range{
						Filename: "test.tf",
						Start: hcl.Pos{
							Line:   5,
							Column: 1,
							Byte:   32,
						},
						End: hcl.Pos{
							Line:   7,
							Column: 2,
							Byte:   60,
						},
					},
				},
			},
			true,
		},
		{
			"nested target match",
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "first"},
					},
					RangePtr: &hcl.Range{
						Filename: "test.tf",
						Start: hcl.Pos{
							Line:   1,
							Column: 1,
							Byte:   0,
						},
						End: hcl.Pos{
							Line:   6,
							Column: 1,
							Byte:   60,
						},
					},
					NestedTargets: Targets{
						Target{
							Addr: lang.Address{
								lang.RootStep{Name: "var"},
								lang.AttrStep{Name: "first"},
								lang.AttrStep{Name: "alpha"},
							},
							RangePtr: &hcl.Range{
								Filename: "test.tf",
								Start: hcl.Pos{
									Line:   2,
									Column: 1,
									Byte:   10,
								},
								End: hcl.Pos{
									Line:   2,
									Column: 11,
									Byte:   20,
								},
							},
						},
						Target{
							Addr: lang.Address{
								lang.RootStep{Name: "var"},
								lang.AttrStep{Name: "first"},
								lang.AttrStep{Name: "beta"},
							},
							RangePtr: &hcl.Range{
								Filename: "test.tf",
								Start: hcl.Pos{
									Line:   3,
									Column: 1,
									Byte:   21,
								},
								End: hcl.Pos{
									Line:   3,
									Column: 11,
									Byte:   31,
								},
							},
						},
					},
				},
			},
			"test.tf",
			hcl.Pos{
				Line:   3,
				Column: 5,
				Byte:   25,
			},
			Targets{
				Target{
					Addr: lang.Address{
						lang.RootStep{Name: "var"},
						lang.AttrStep{Name: "first"},
						lang.AttrStep{Name: "beta"},
					},
					RangePtr: &hcl.Range{
						Filename: "test.tf",
						Start: hcl.Pos{
							Line:   3,
							Column: 1,
							Byte:   21,
						},
						End: hcl.Pos{
							Line:   3,
							Column: 11,
							Byte:   31,
						},
					},
				},
			},
			true,
		},
	}

	for i, tc := range testCases {
		t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) {
			targets, found := tc.targets.InnermostAtPos(tc.file, tc.pos)
			if !found && tc.expectedFound {
				t.Fatal("expected to find targets")
			}
			if diff := cmp.Diff(tc.expectedTargets, targets, ctydebug.CmpOptions); diff != "" {
				t.Fatalf("mismatch of targets: %s", diff)
			}
		})
	}
}
