// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1

package configs

import (
	"fmt"
	"testing"

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

	"github.com/hashicorp/hcl/v2/ext/typeexpr"
	"github.com/hashicorp/terraform/internal/addrs"
)

func TestModuleOverrideVariable(t *testing.T) {
	mod, diags := testModuleFromDir("testdata/valid-modules/override-variable")
	assertNoDiagnostics(t, diags)
	if mod == nil {
		t.Fatalf("module is nil")
	}

	got := mod.Variables
	want := map[string]*Variable{
		"fully_overridden": {
			Name:           "fully_overridden",
			Description:    "b_override description",
			DescriptionSet: true,
			Default:        cty.StringVal("b_override"),
			Nullable:       false,
			NullableSet:    true,
			Type:           cty.String,
			ConstraintType: cty.String,
			ParsingMode:    VariableParseLiteral,
			DeclRange: hcl.Range{
				Filename: "testdata/valid-modules/override-variable/primary.tf",
				Start: hcl.Pos{
					Line:   1,
					Column: 1,
					Byte:   0,
				},
				End: hcl.Pos{
					Line:   1,
					Column: 28,
					Byte:   27,
				},
			},
		},
		"partially_overridden": {
			Name:           "partially_overridden",
			Description:    "base description",
			DescriptionSet: true,
			Default:        cty.StringVal("b_override partial"),
			Nullable:       true,
			NullableSet:    false,
			Type:           cty.String,
			ConstraintType: cty.String,
			ParsingMode:    VariableParseLiteral,
			DeclRange: hcl.Range{
				Filename: "testdata/valid-modules/override-variable/primary.tf",
				Start: hcl.Pos{
					Line:   7,
					Column: 1,
					Byte:   103,
				},
				End: hcl.Pos{
					Line:   7,
					Column: 32,
					Byte:   134,
				},
			},
		},
	}
	assertResultDeepEqual(t, got, want)
}

func TestModuleOverrideModule(t *testing.T) {
	mod, diags := testModuleFromDir("testdata/valid-modules/override-module")
	assertNoDiagnostics(t, diags)
	if mod == nil {
		t.Fatalf("module is nil")
	}

	if _, exists := mod.ModuleCalls["example"]; !exists {
		t.Fatalf("no module 'example'")
	}
	if len(mod.ModuleCalls) != 1 {
		t.Fatalf("wrong number of module calls in result %d; want 1", len(mod.ModuleCalls))
	}

	got := mod.ModuleCalls["example"]
	want := &ModuleCall{
		Name: "example",
		SourceExpr: mustExpr(hclsyntax.ParseExpression(
			[]byte("\"./example2-a_override\""), "testdata/valid-modules/override-module/a_override.tf",
			hcl.Pos{Line: 3, Column: 12, Byte: 31},
		)),
		DeclRange: hcl.Range{
			Filename: "testdata/valid-modules/override-module/primary.tf",
			Start: hcl.Pos{
				Line:   2,
				Column: 1,
				Byte:   1,
			},
			End: hcl.Pos{
				Line:   2,
				Column: 17,
				Byte:   17,
			},
		},
		DependsOn: []hcl.Traversal{
			{
				hcl.TraverseRoot{
					Name: "null_resource",
					SrcRange: hcl.Range{
						Filename: "testdata/valid-modules/override-module/primary.tf",
						Start:    hcl.Pos{Line: 11, Column: 17, Byte: 149},
						End:      hcl.Pos{Line: 11, Column: 30, Byte: 162},
					},
				},
				hcl.TraverseAttr{
					Name: "test",
					SrcRange: hcl.Range{
						Filename: "testdata/valid-modules/override-module/primary.tf",
						Start:    hcl.Pos{Line: 11, Column: 30, Byte: 162},
						End:      hcl.Pos{Line: 11, Column: 35, Byte: 167},
					},
				},
			},
		},
		Providers: []PassedProviderConfig{
			{
				InChild: &ProviderConfigRef{
					Name: "test",
					NameRange: hcl.Range{
						Filename: "testdata/valid-modules/override-module/b_override.tf",
						Start:    hcl.Pos{Line: 7, Column: 5, Byte: 97},
						End:      hcl.Pos{Line: 7, Column: 9, Byte: 101},
					},
				},
				InParent: &ProviderConfigRef{
					Name: "test",
					NameRange: hcl.Range{
						Filename: "testdata/valid-modules/override-module/b_override.tf",
						Start:    hcl.Pos{Line: 7, Column: 12, Byte: 104},
						End:      hcl.Pos{Line: 7, Column: 16, Byte: 108},
					},
					Alias: "b_override",
					AliasRange: &hcl.Range{
						Filename: "testdata/valid-modules/override-module/b_override.tf",
						Start:    hcl.Pos{Line: 7, Column: 16, Byte: 108},
						End:      hcl.Pos{Line: 7, Column: 27, Byte: 119},
					},
				},
			},
		},
	}

	// We're going to extract and nil out our hcl.Body here because DeepEqual
	// is not a useful way to assert on that.
	gotConfig := got.Config
	got.Config = nil

	assertResultDeepEqual(t, got, want)

	type content struct {
		Kept  *string `hcl:"kept"`
		Foo   *string `hcl:"foo"`
		New   *string `hcl:"new"`
		Newer *string `hcl:"newer"`
	}
	var gotArgs content
	diags = gohcl.DecodeBody(gotConfig, nil, &gotArgs)
	assertNoDiagnostics(t, diags)

	wantArgs := content{
		Kept:  stringPtr("primary kept"),
		Foo:   stringPtr("a_override foo"),
		New:   stringPtr("b_override new"),
		Newer: stringPtr("b_override newer"),
	}

	assertResultDeepEqual(t, gotArgs, wantArgs)
}

func TestModuleOverrideDynamic(t *testing.T) {
	schema := &hcl.BodySchema{
		Blocks: []hcl.BlockHeaderSchema{
			{Type: "foo"},
			{Type: "dynamic", LabelNames: []string{"type"}},
		},
	}

	t.Run("base is dynamic", func(t *testing.T) {
		mod, diags := testModuleFromDir("testdata/valid-modules/override-dynamic-block-base")
		assertNoDiagnostics(t, diags)
		if mod == nil {
			t.Fatalf("module is nil")
		}

		if _, exists := mod.ManagedResources["test.foo"]; !exists {
			t.Fatalf("no module 'example'")
		}
		if len(mod.ManagedResources) != 1 {
			t.Fatalf("wrong number of managed resources in result %d; want 1", len(mod.ManagedResources))
		}

		body := mod.ManagedResources["test.foo"].Config
		content, diags := body.Content(schema)
		assertNoDiagnostics(t, diags)

		if len(content.Blocks) != 1 {
			t.Fatalf("wrong number of blocks in result %d; want 1", len(content.Blocks))
		}
		if got, want := content.Blocks[0].Type, "foo"; got != want {
			t.Fatalf("wrong block type %q; want %q", got, want)
		}
	})
	t.Run("override is dynamic", func(t *testing.T) {
		mod, diags := testModuleFromDir("testdata/valid-modules/override-dynamic-block-override")
		assertNoDiagnostics(t, diags)
		if mod == nil {
			t.Fatalf("module is nil")
		}

		if _, exists := mod.ManagedResources["test.foo"]; !exists {
			t.Fatalf("no module 'example'")
		}
		if len(mod.ManagedResources) != 1 {
			t.Fatalf("wrong number of managed resources in result %d; want 1", len(mod.ManagedResources))
		}

		body := mod.ManagedResources["test.foo"].Config
		content, diags := body.Content(schema)
		assertNoDiagnostics(t, diags)

		if len(content.Blocks) != 1 {
			t.Fatalf("wrong number of blocks in result %d; want 1", len(content.Blocks))
		}
		if got, want := content.Blocks[0].Type, "dynamic"; got != want {
			t.Fatalf("wrong block type %q; want %q", got, want)
		}
		if got, want := content.Blocks[0].Labels[0], "foo"; got != want {
			t.Fatalf("wrong dynamic block label %q; want %q", got, want)
		}
	})
}

func TestModuleOverrideSensitiveVariable(t *testing.T) {
	type testCase struct {
		sensitive    bool
		sensitiveSet bool
	}
	cases := map[string]testCase{
		"false_true": {
			sensitive:    true,
			sensitiveSet: true,
		},
		"true_false": {
			sensitive:    false,
			sensitiveSet: true,
		},
		"false_false_true": {
			sensitive:    true,
			sensitiveSet: true,
		},
		"true_true_false": {
			sensitive:    false,
			sensitiveSet: true,
		},
		"false_true_false": {
			sensitive:    false,
			sensitiveSet: true,
		},
		"true_false_true": {
			sensitive:    true,
			sensitiveSet: true,
		},
	}

	mod, diags := testModuleFromDir("testdata/valid-modules/override-variable-sensitive")

	assertNoDiagnostics(t, diags)

	if mod == nil {
		t.Fatalf("module is nil")
	}

	got := mod.Variables

	for v, want := range cases {
		t.Run(fmt.Sprintf("variable %s", v), func(t *testing.T) {
			if got[v].Sensitive != want.sensitive {
				t.Errorf("wrong result for sensitive\ngot: %t want: %t", got[v].Sensitive, want.sensitive)
			}

			if got[v].SensitiveSet != want.sensitiveSet {
				t.Errorf("wrong result for sensitive set\ngot: %t want: %t", got[v].SensitiveSet, want.sensitiveSet)
			}
		})
	}
}

func TestModuleOverrideEphemeralVariable(t *testing.T) {
	type testCase struct {
		ephemeral    bool
		ephemeralSet bool
	}
	cases := map[string]testCase{
		"false_true": {
			ephemeral:    true,
			ephemeralSet: true,
		},
		"true_false": {
			ephemeral:    false,
			ephemeralSet: true,
		},
		"false_false_true": {
			ephemeral:    true,
			ephemeralSet: true,
		},
		"true_true_false": {
			ephemeral:    false,
			ephemeralSet: true,
		},
		"false_true_false": {
			ephemeral:    false,
			ephemeralSet: true,
		},
		"true_false_true": {
			ephemeral:    true,
			ephemeralSet: true,
		},
	}

	mod, diags := testModuleFromDir("testdata/valid-modules/override-variable-ephemeral")

	assertNoDiagnostics(t, diags)

	if mod == nil {
		t.Fatalf("module is nil")
	}

	got := mod.Variables

	for v, want := range cases {
		t.Run(fmt.Sprintf("variable %s", v), func(t *testing.T) {
			if got[v].Ephemeral != want.ephemeral {
				t.Errorf("wrong result for ephemeral\ngot: %t want: %t", got[v].Ephemeral, want.ephemeral)
			}

			if got[v].EphemeralSet != want.ephemeralSet {
				t.Errorf("wrong result for ephemeral set\ngot: %t want: %t", got[v].EphemeralSet, want.ephemeralSet)
			}
		})
	}
}

func TestModuleOverrideConstVariable(t *testing.T) {
	type testCase struct {
		constV   bool
		constSet bool
	}
	cases := map[string]testCase{
		"false_true": {
			constV:   true,
			constSet: true,
		},
		"true_false": {
			constV:   false,
			constSet: true,
		},
		"false_false_true": {
			constV:   true,
			constSet: true,
		},
		"true_true_false": {
			constV:   false,
			constSet: true,
		},
		"false_true_false": {
			constV:   false,
			constSet: true,
		},
		"true_false_true": {
			constV:   true,
			constSet: true,
		},
	}

	mod, diags := testModuleFromDir("testdata/valid-modules/override-variable-const")

	assertNoDiagnostics(t, diags)

	if mod == nil {
		t.Fatalf("module is nil")
	}

	got := mod.Variables

	for v, want := range cases {
		t.Run(fmt.Sprintf("variable %s", v), func(t *testing.T) {
			if got[v].Const != want.constV {
				t.Errorf("wrong result for const\ngot: %t want: %t", got[v].Const, want.constV)
			}

			if got[v].ConstSet != want.constSet {
				t.Errorf("wrong result for const set\ngot: %t want: %t", got[v].ConstSet, want.constSet)
			}
		})
	}
}

func TestModuleOverrideOutputType(t *testing.T) {
	type testCase struct {
		constraintType cty.Type
		typeDefaults   *typeexpr.Defaults
		typeSet        bool
	}
	cases := map[string]testCase{
		"fully_overridden": {
			constraintType: cty.Number,
			typeDefaults:   nil,
			typeSet:        true,
		},
		"no_override": {
			constraintType: cty.String,
			typeDefaults:   nil,
			typeSet:        true,
		},
		"type_added_by_override": {
			constraintType: cty.List(cty.String),
			typeDefaults:   nil,
			typeSet:        true,
		},
	}

	mod, diags := testModuleFromDir("testdata/valid-modules/override-output-type")

	assertNoDiagnostics(t, diags)

	if mod == nil {
		t.Fatalf("module is nil")
	}

	for name, want := range cases {
		t.Run(fmt.Sprintf("output %s", name), func(t *testing.T) {
			got, exists := mod.Outputs[name]
			if !exists {
				t.Fatalf("output %q not found", name)
			}

			if !got.ConstraintType.Equals(want.constraintType) {
				t.Errorf("wrong result for constraint type\ngot:  %#v\nwant: %#v", got.ConstraintType, want.constraintType)
			}

			if got.TypeSet != want.typeSet {
				t.Errorf("wrong result for type set\ngot: %t want: %t", got.TypeSet, want.typeSet)
			}

			if got.TypeDefaults != want.typeDefaults {
				t.Errorf("wrong result for type defaults\ngot: %#v want: %#v", got.TypeDefaults, want.typeDefaults)
			}
		})
	}
}

func TestModuleOverrideResourceFQNs(t *testing.T) {
	mod, diags := testModuleFromDir("testdata/valid-modules/override-resource-provider")
	assertNoDiagnostics(t, diags)

	got := mod.ManagedResources["test_instance.explicit"]
	wantProvider := addrs.NewProvider(addrs.DefaultProviderRegistryHost, "bar", "test")
	wantProviderCfg := &ProviderConfigRef{
		Name: "bar-test",
		NameRange: hcl.Range{
			Filename: "testdata/valid-modules/override-resource-provider/a_override.tf",
			Start:    hcl.Pos{Line: 2, Column: 14, Byte: 51},
			End:      hcl.Pos{Line: 2, Column: 22, Byte: 59},
		},
	}

	if !got.Provider.Equals(wantProvider) {
		t.Fatalf("wrong provider %s, want %s", got.Provider, wantProvider)
	}
	assertResultDeepEqual(t, got.ProviderConfigRef, wantProviderCfg)

	// now verify that a resource with no provider config falls back to default
	got = mod.ManagedResources["test_instance.default"]
	wantProvider = addrs.NewDefaultProvider("test")
	if !got.Provider.Equals(wantProvider) {
		t.Fatalf("wrong provider %s, want %s", got.Provider, wantProvider)
	}
	if got.ProviderConfigRef != nil {
		t.Fatalf("wrong result: found provider config ref %s, expected nil", got.ProviderConfigRef)
	}
}

func TestModuleOverrideIgnoreAllChanges(t *testing.T) {
	mod, diags := testModuleFromDir("testdata/valid-modules/override-ignore-changes")
	assertNoDiagnostics(t, diags)

	r := mod.ManagedResources["test_instance.foo"]
	if !r.Managed.IgnoreAllChanges {
		t.Fatalf("wrong result: expected r.Managed.IgnoreAllChanges to be true")
	}
}

// This tests the override behavior of action blocks and action_triggers inside resources.
func TestModuleOverride_action_and_trigger(t *testing.T) {
	mod, diags := testModuleFromDirWithExperiments("testdata/valid-modules/override-action-and-trigger")
	assertNoDiagnostics(t, diags)

	if len(mod.Actions) != 2 {
		t.Fatalf("wrong number of actions: %d\n", len(mod.Actions))
	}

	// verify that the action has attr foo = baz (override)
	got := mod.Actions["action.test_action.test"]
	want := &Action{
		Name:              "test",
		Type:              "test_action",
		Config:            nil,
		Count:             nil,
		ForEach:           nil,
		ProviderConfigRef: nil,
		Provider:          addrs.NewProvider(addrs.DefaultProviderRegistryHost, "hashicorp", "test"),
		DeclRange: hcl.Range{
			Filename: "testdata/valid-modules/override-action-and-trigger/main.tf",
			Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
			End:      hcl.Pos{Line: 1, Column: 28, Byte: 27},
		},
		TypeRange: hcl.Range{
			Filename: "testdata/valid-modules/override-action-and-trigger/main.tf",
			Start:    hcl.Pos{Line: 1, Column: 8, Byte: 7},
			End:      hcl.Pos{Line: 1, Column: 21, Byte: 20},
		},
	}

	// We're going to extract and nil out our hcl.Body here because DeepEqual
	// is not a useful way to assert on that.
	gotConfig := got.Config
	got.Config = nil

	assertResultDeepEqual(t, got, want)

	// now to check that config
	type content struct {
		Foo *string `hcl:"foo"`
	}
	var gotArgs content
	diags = gohcl.DecodeBody(gotConfig, nil, &gotArgs)
	assertNoDiagnostics(t, diags)

	wantArgs := content{
		Foo: stringPtr("baz"),
	}
	assertResultDeepEqual(t, gotArgs, wantArgs)

	if _, exists := mod.ManagedResources["test_instance.test"]; !exists {
		t.Fatalf("no resource 'test_instance.test'")
	}
	if len(mod.ManagedResources) != 1 {
		t.Fatalf("wrong number of managed resources in result %d; want 1", len(mod.ManagedResources))
	}

	r := mod.ManagedResources["test_instance.test"].Managed
	assertResultDeepEqual(t, len(r.ActionTriggers), 1)

	// verify the resource action trigger event changed
	at := mod.ManagedResources["test_instance.test"].Managed.ActionTriggers[0]
	assertResultDeepEqual(t, at.Events, []ActionTriggerEvent{BeforeCreate})
}
