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

package configs

import (
	"fmt"
	"path/filepath"
	"testing"

	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/gohcl"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/zclconf/go-cty/cty"
)

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"),
			Deprecated:     "b_override deprecated",
			Nullable:       false,
			NullableSet:    true,
			Ephemeral:      false,
			EphemeralSet:   true,
			Type:           cty.String,
			ConstraintType: cty.String,
			ParsingMode:    VariableParseLiteral,
			DeclRange: hcl.Range{
				Filename: filepath.FromSlash("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"),
			Deprecated:     "b_override deprecated",
			Nullable:       true,
			NullableSet:    false,
			Type:           cty.String,
			ConstraintType: cty.String,
			ParsingMode:    VariableParseLiteral,
			DeclRange: hcl.Range{
				Filename: filepath.FromSlash("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 TestModuleOverrideOutput(t *testing.T) {
	mod, diags := testModuleFromDir("testdata/valid-modules/override-output")
	assertNoDiagnostics(t, diags)
	if mod == nil {
		t.Fatalf("module is nil")
	}

	got := mod.Outputs
	want := map[string]*Output{
		"fully_overridden": {
			Name:           "fully_overridden",
			Description:    "b_override description",
			DescriptionSet: true,
			Expr: &hclsyntax.TemplateExpr{
				Parts: []hclsyntax.Expression{
					&hclsyntax.LiteralValueExpr{
						Val: cty.StringVal("b_override"),
						SrcRange: hcl.Range{
							Filename: filepath.FromSlash("testdata/valid-modules/override-output/b_override.tf"),
							Start: hcl.Pos{
								Line:   2,
								Column: 12,
								Byte:   39,
							},
							End: hcl.Pos{
								Line:   2,
								Column: 22,
								Byte:   49,
							},
						},
					},
				},
				SrcRange: hcl.Range{
					Filename: filepath.FromSlash("testdata/valid-modules/override-output/b_override.tf"),
					Start: hcl.Pos{
						Line:   2,
						Column: 11,
						Byte:   38,
					},
					End: hcl.Pos{
						Line:   2,
						Column: 23,
						Byte:   50,
					},
				},
			},
			Deprecated:   "b_override deprecated",
			Ephemeral:    false,
			EphemeralSet: true,
			DeclRange: hcl.Range{
				Filename: filepath.FromSlash("testdata/valid-modules/override-output/primary.tf"),
				Start: hcl.Pos{
					Line:   1,
					Column: 1,
					Byte:   0,
				},
				End: hcl.Pos{
					Line:   1,
					Column: 26,
					Byte:   25,
				},
			},
		},
		"partially_overridden": {
			Name:           "partially_overridden",
			Description:    "base description",
			DescriptionSet: true,
			Expr: &hclsyntax.TemplateExpr{
				Parts: []hclsyntax.Expression{
					&hclsyntax.LiteralValueExpr{
						Val: cty.StringVal("b_override partial"),
						SrcRange: hcl.Range{
							Filename: filepath.FromSlash("testdata/valid-modules/override-output/b_override.tf"),
							Start: hcl.Pos{
								Line:   9,
								Column: 12,
								Byte:   197,
							},
							End: hcl.Pos{
								Line:   9,
								Column: 30,
								Byte:   215,
							},
						},
					},
				},
				SrcRange: hcl.Range{
					Filename: filepath.FromSlash("testdata/valid-modules/override-output/b_override.tf"),
					Start: hcl.Pos{
						Line:   9,
						Column: 11,
						Byte:   196,
					},
					End: hcl.Pos{
						Line:   9,
						Column: 31,
						Byte:   216,
					},
				},
			},
			Deprecated: "b_override deprecated",
			DeclRange: hcl.Range{
				Filename: filepath.FromSlash("testdata/valid-modules/override-output/primary.tf"),
				Start: hcl.Pos{
					Line:   6,
					Column: 1,
					Byte:   83,
				},
				End: hcl.Pos{
					Line:   6,
					Column: 30,
					Byte:   112,
				},
			},
		},
	}
	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",
		SourceAddr:    addrs.ModuleSourceLocal("./example2-a_override"),
		SourceAddrRaw: "./example2-a_override",
		SourceSet:     true,
		DeclRange: hcl.Range{
			Filename: filepath.FromSlash("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: filepath.FromSlash("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: filepath.FromSlash("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: filepath.FromSlash("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: filepath.FromSlash("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: filepath.FromSlash("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
	got.Source = nil
	got.Variables = 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].Sensitive, want.sensitive)
			}
		})
	}
}

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].Sensitive, want.ephemeral)
			}

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

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: filepath.FromSlash("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")
	}
}
