// 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 (
	"os"
	"runtime"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	version "github.com/hashicorp/go-version"
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclsyntax"
	"github.com/zclconf/go-cty-debug/ctydebug"
	"github.com/zclconf/go-cty/cty"

	"github.com/opentofu/opentofu/internal/addrs"
)

func TestLoadModuleCall(t *testing.T) {
	src, err := os.ReadFile("testdata/invalid-files/module-calls.tf")
	if err != nil {
		t.Fatal(err)
	}

	parser := testParser(map[string]string{
		"module-calls.tf": string(src),
	})

	file, diags := parser.LoadConfigFile("module-calls.tf")
	assertExactDiagnostics(t, diags, []string{
		`module-calls.tf:19,3-20,11: Invalid combination of "count" and "for_each"; The "count" and "for_each" meta-arguments are mutually-exclusive. Only one should be used to be explicit about the number of module instances to be created.`,
		`module-calls.tf:34,5-36,8: Invalid combination of "count" and "enabled"; The "count" and "enabled" meta-arguments are mutually-exclusive. Only one should be used to be explicit about the number of module instances to be created.`,
	})

	gotModules := file.ModuleCalls
	wantModules := []*ModuleCall{
		{
			Name:          "foo",
			SourceAddr:    addrs.ModuleSourceLocal("./foo"),
			SourceAddrRaw: "./foo",
			SourceSet:     true,
			DeclRange: hcl.Range{
				Filename: "module-calls.tf",
				Start:    hcl.Pos{Line: 2, Column: 1, Byte: 1},
				End:      hcl.Pos{Line: 2, Column: 13, Byte: 13},
			},
		},
		{
			Name: "bar",
			SourceAddr: addrs.ModuleSourceRegistry{
				Package: addrs.ModuleRegistryPackage{
					Host:         addrs.DefaultModuleRegistryHost,
					Namespace:    "hashicorp",
					Name:         "bar",
					TargetSystem: "aws",
				},
			},
			SourceAddrRaw: "hashicorp/bar/aws",
			SourceSet:     true,
			DeclRange: hcl.Range{
				Filename: "module-calls.tf",
				Start:    hcl.Pos{Line: 7, Column: 1, Byte: 87},
				End:      hcl.Pos{Line: 7, Column: 13, Byte: 99},
			},
		},
		{
			Name: "baz",
			SourceAddr: addrs.ModuleSourceRemote{
				Package: addrs.ModulePackage("git::https://example.com/"),
			},
			SourceAddrRaw: "git::https://example.com/",
			SourceSet:     true,
			DependsOn: []hcl.Traversal{
				{
					hcl.TraverseRoot{
						Name: "module",
						SrcRange: hcl.Range{
							Filename: "module-calls.tf",
							Start:    hcl.Pos{Line: 23, Column: 5, Byte: 298},
							End:      hcl.Pos{Line: 23, Column: 11, Byte: 304},
						},
					},
					hcl.TraverseAttr{
						Name: "bar",
						SrcRange: hcl.Range{
							Filename: "module-calls.tf",
							Start:    hcl.Pos{Line: 23, Column: 11, Byte: 304},
							End:      hcl.Pos{Line: 23, Column: 15, Byte: 308},
						},
					},
				},
			},
			Providers: []PassedProviderConfig{
				{
					InChild: &ProviderConfigRef{
						Name: "aws",
						NameRange: hcl.Range{
							Filename: "module-calls.tf",
							Start:    hcl.Pos{Line: 27, Column: 5, Byte: 335},
							End:      hcl.Pos{Line: 27, Column: 8, Byte: 338},
						},
					},
					InParent: &ProviderConfigRef{
						Name: "aws",
						NameRange: hcl.Range{
							Filename: "module-calls.tf",
							Start:    hcl.Pos{Line: 27, Column: 11, Byte: 341},
							End:      hcl.Pos{Line: 27, Column: 14, Byte: 344},
						},
						Alias: "foo",
						AliasRange: &hcl.Range{
							Filename: "module-calls.tf",
							Start:    hcl.Pos{Line: 27, Column: 14, Byte: 344},
							End:      hcl.Pos{Line: 27, Column: 18, Byte: 348},
						},
					},
				},
			},
			DeclRange: hcl.Range{
				Filename: "module-calls.tf",
				Start:    hcl.Pos{Line: 14, Column: 1, Byte: 167},
				End:      hcl.Pos{Line: 14, Column: 13, Byte: 179},
			},
		},
		{
			Name:          "enabled_test",
			SourceAddr:    addrs.ModuleSourceLocal("./foo"),
			SourceAddrRaw: "./foo",
			Enabled: &hclsyntax.LiteralValueExpr{
				Val: cty.BoolVal(true),
				SrcRange: hcl.Range{
					Filename: "module-calls.tf",
					Start:    hcl.Pos{Line: 34, Column: 15, Byte: 427},
					End:      hcl.Pos{Line: 34, Column: 19, Byte: 431},
				},
			},
			SourceSet: true,
			DeclRange: hcl.Range{
				Filename: "module-calls.tf",
				Start:    hcl.Pos{Line: 31, Column: 1, Byte: 356},
				End:      hcl.Pos{Line: 31, Column: 22, Byte: 377},
			},
		},
	}

	// We'll hide all of the bodies/exprs since we're treating them as opaque
	// here anyway... the point of this test is to ensure we handle everything
	// else properly.
	for _, m := range gotModules {
		// This is a structural issue which existed before static evaluation, but has been made worse by it
		// See https://github.com/opentofu/opentofu/issues/1467 for more details
		eval := NewStaticEvaluator(nil, RootModuleCallForTesting())
		diags := m.decodeStaticFields(t.Context(), eval)
		if diags.HasErrors() {
			t.Fatal(diags.Error())
		}
		m.Source = nil

		m.Config = nil
		m.Count = nil
		m.ForEach = nil
	}

	cmpOpts := cmp.Options{
		ctydebug.CmpOptions,
		cmp.AllowUnexported(ProviderConfigRef{}),
		cmpopts.IgnoreUnexported(hcl.TraverseAttr{}, hcl.TraverseIndex{}, hcl.TraverseRoot{}),
		cmpopts.IgnoreTypes(StaticModuleVariables(nil)), // function pointer type is not comparable
	}
	if diff := cmp.Diff(wantModules, gotModules, cmpOpts); diff != "" {
		t.Error("wrong result:\n" + diff)
	}
}

func TestModuleSourceAddrEntersNewPackage(t *testing.T) {
	absolutePath := "/absolute/path"
	if runtime.GOOS == "windows" {
		absolutePath = "C:\\absolute\\path"
	}

	tests := []struct {
		Addr string
		Want bool
	}{
		{
			"./",
			false,
		},
		{
			"../bork",
			false,
		},
		{
			absolutePath,
			true,
		},
		{
			"github.com/example/foo",
			true,
		},
		{
			"hashicorp/subnets/cidr", // registry module
			true,
		},
		{
			"registry.opentofu.org/hashicorp/subnets/cidr", // registry module
			true,
		},
	}

	for _, test := range tests {
		t.Run(test.Addr, func(t *testing.T) {
			addr, err := addrs.ParseModuleSource(test.Addr)
			if err != nil {
				t.Fatalf("parsing failed for %q: %s", test.Addr, err)
			}

			got := moduleSourceAddrEntersNewPackage(addr)
			if got != test.Want {
				t.Errorf("wrong result for %q\ngot:  %#v\nwant:  %#v", addr, got, test.Want)
			}
		})
	}
}

func TestModuleCallWithVersion(t *testing.T) {
	src, err := os.ReadFile("testdata/valid-files/modules-with-version.tf")
	if err != nil {
		t.Fatal(err)
	}

	parser := testParser(map[string]string{
		"modules-with-version.tf": string(src),
	})

	file, diags := parser.LoadConfigFile("modules-with-version.tf")
	if diags.HasErrors() {
		t.Fatalf("unexpected errors: %s", diags.Error())
	}

	// Create a module from the loaded file
	mod, diags := NewModule([]*File{file}, nil, RootModuleCallForTesting(), "testdata", SelectiveLoadAll)
	if diags.HasErrors() {
		t.Fatalf("unexpected errors creating module: %s", diags.Error())
	}

	gotModules := file.ModuleCalls
	wantModules := []*ModuleCall{
		{
			Name:          "foo",
			SourceAddr:    addrs.ModuleSourceLocal("./foo"),
			SourceAddrRaw: "./foo",
			SourceSet:     true,
			DeclRange: hcl.Range{
				Filename: "modules-with-version.tf",
				Start:    hcl.Pos{Line: 4, Column: 1, Byte: 35},
				End:      hcl.Pos{Line: 4, Column: 13, Byte: 47},
			},
		},
		{
			Name: "foo_remote",
			SourceAddr: addrs.ModuleSourceRegistry{
				Package: addrs.ModuleRegistryPackage{
					Host:         addrs.DefaultModuleRegistryHost,
					Namespace:    "hashicorp",
					Name:         "foo",
					TargetSystem: "bar",
				},
			},
			SourceAddrRaw: "hashicorp/foo/bar",
			SourceSet:     true,
			VersionAttr: &hcl.Attribute{
				Name: "version",
				Expr: &hclsyntax.ScopeTraversalExpr{
					SrcRange: hcl.Range{
						Filename: "modules-with-version.tf",
						Start:    hcl.Pos{Line: 14, Column: 13, Byte: 214},
						End:      hcl.Pos{Line: 14, Column: 37, Byte: 238},
					},
					Traversal: hcl.Traversal{
						hcl.TraverseRoot{
							Name: "local",
							SrcRange: hcl.Range{
								Filename: "modules-with-version.tf",
								Start:    hcl.Pos{Line: 14, Column: 13, Byte: 214},
								End:      hcl.Pos{Line: 14, Column: 18, Byte: 219},
							},
						},
						hcl.TraverseAttr{
							Name: "module_version_set",
							SrcRange: hcl.Range{
								Filename: "modules-with-version.tf",
								Start:    hcl.Pos{Line: 14, Column: 18, Byte: 219},
								End:      hcl.Pos{Line: 14, Column: 37, Byte: 238},
							},
						},
					},
				},
				Range: hcl.Range{
					Filename: "modules-with-version.tf",
					Start:    hcl.Pos{Line: 14, Column: 3, Byte: 204},
					End:      hcl.Pos{Line: 14, Column: 37, Byte: 238},
				},
				NameRange: hcl.Range{
					Filename: "modules-with-version.tf",
					Start:    hcl.Pos{Line: 14, Column: 3, Byte: 204},
					End:      hcl.Pos{Line: 14, Column: 10, Byte: 211},
				},
			},
			Version: VersionConstraint{
				Required: version.MustConstraints(version.NewConstraint("1.0.0")),
				DeclRange: hcl.Range{
					Filename: "modules-with-version.tf",
					Start:    hcl.Pos{Line: 14, Column: 3, Byte: 204},
					End:      hcl.Pos{Line: 14, Column: 37, Byte: 238},
				},
			},
			DeclRange: hcl.Range{
				Filename: "modules-with-version.tf",
				Start:    hcl.Pos{Line: 12, Column: 1, Byte: 148},
				End:      hcl.Pos{Line: 12, Column: 20, Byte: 167},
			},
		},
	}

	for _, m := range gotModules {
		// Create a static evaluator with the module context
		eval := NewStaticEvaluator(mod, RootModuleCallForTesting())
		diags := m.decodeStaticFields(t.Context(), eval)
		if diags.HasErrors() {
			t.Fatal(diags.Error())
		}
		m.Source = nil
		m.Config = nil
		m.Count = nil
		m.ForEach = nil
	}

	cmpOpts := cmp.Options{
		ctydebug.CmpOptions,
		cmp.AllowUnexported(ProviderConfigRef{}),
		cmpopts.IgnoreUnexported(hcl.TraverseAttr{}, hcl.TraverseIndex{}, hcl.TraverseRoot{}),
		cmpopts.IgnoreTypes(StaticModuleVariables(nil)), // function pointer type is not comparable
		cmp.Comparer(func(a, b *version.Constraint) bool {
			return a.Equals(b)
		}),
	}
	if diff := cmp.Diff(wantModules, gotModules, cmpOpts); diff != "" {
		t.Error("wrong result:\n" + diff)
	}
}

func TestModuleCallWithSource(t *testing.T) {
	sources := map[string]string{
		"mod.tf": `
module "foo" {
  source = var.path
}`,
		"path.tf": `
variable "path" {
  type = string
  default = "./path"
}`,
		"path_const.tf": `
variable "path" {
  type = string
  const = true
  default = "./path"
}`,
		"path_notconst.tf": `
variable "path" {
  type = string
  const = false
  default = "./path"
}`,
	}

	parser := testParser(sources)

	files := map[string]*File{}
	for fn := range sources {
		file, diags := parser.LoadConfigFile(fn)
		if diags.HasErrors() {
			t.Fatalf("unexpected errors: %s", diags.Error())
		}
		files[fn] = file
	}

	call := NewStaticModuleCall(nil, hcl.Range{}, func(v *Variable) (cty.Value, hcl.Diagnostics) {
		return v.Default, nil
	}, "<testing>", "")

	cases := []struct {
		name  string
		files []string
		err   string
	}{{
		name:  "success, pre-const",
		files: []string{"mod.tf", "path.tf"},
	}, {
		name:  "success, post-const",
		files: []string{"mod.tf", "path_const.tf"},
	}, {
		name:  "failure, post-const",
		files: []string{"mod.tf", "path_notconst.tf"},
		err:   `The variable "path" cannot be used in a static context, because it is declared as "const = false`,
	}}

	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			var tFiles []*File
			for _, fn := range tc.files {
				f, ok := files[fn]
				if !ok {
					t.Fatalf("not ok %s %#v", fn, files)
				}
				tFiles = append(tFiles, f)
			}
			_, diags := NewModule(tFiles, nil, call, "testdata", SelectiveLoadAll)
			if tc.err == "" {
				if diags.HasErrors() {
					t.Errorf("unexpected errors creating module: %s", diags.Error())
				}
			} else {
				if !strings.Contains(diags.Error(), tc.err) {
					t.Errorf("expected %s, got %s", tc.err, diags.Error())
				}
			}
		})
	}
}
