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

package tofu

import (
	"testing"

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

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/checks"
	"github.com/opentofu/opentofu/internal/configs"
	"github.com/opentofu/opentofu/internal/lang"
)

func TestNodeRootVariableExecute(t *testing.T) {
	t.Run("type conversion", func(t *testing.T) {
		ctx := new(MockEvalContext)

		n := &NodeRootVariable{
			Addr: addrs.InputVariable{Name: "foo"},
			Config: &configs.Variable{
				Name:           "foo",
				Type:           cty.String,
				ConstraintType: cty.String,
			},
			RawValue: &InputValue{
				Value:      cty.True,
				SourceType: ValueFromUnknown,
			},
		}

		ctx.ChecksState = checks.NewState(&configs.Config{
			Module: &configs.Module{
				Variables: map[string]*configs.Variable{
					"foo": n.Config,
				},
			},
		})

		diags := n.Execute(ctx, walkApply)
		if diags.HasErrors() {
			t.Fatalf("unexpected error: %s", diags.Err())
		}

		if !ctx.SetRootModuleArgumentCalled {
			t.Fatalf("ctx.SetRootModuleArgument wasn't called")
		}
		if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
			t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot:  %s\nwant: %s", got, want)
		}
		if got, want := ctx.SetRootModuleArgumentValue, cty.StringVal("true"); !want.RawEquals(got) {
			// NOTE: The given value was cty.Bool but the type constraint was
			// cty.String, so it was NodeRootVariable's responsibility to convert
			// as part of preparing the "final value".
			t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot:  %#v\nwant: %#v", got, want)
		}
	})
	t.Run("validation", func(t *testing.T) {
		ctx := new(MockEvalContext)

		// The variable validation function gets called with OpenTofu's
		// built-in functions available, so we need a minimal scope just for
		// it to get the functions from.
		ctx.EvaluationScopeScope = &lang.Scope{}

		// We need to reimplement a _little_ bit of EvalContextBuiltin logic
		// here to get a similar effect with EvalContextMock just to get the
		// value to flow through here in a realistic way that'll make this test
		// useful.
		var finalVal cty.Value
		ctx.SetRootModuleArgumentFunc = func(addr addrs.InputVariable, v cty.Value) {
			if addr.Name == "foo" {
				t.Logf("set %s to %#v", addr.String(), v)
				finalVal = v
			}
		}
		ctx.GetVariableValueFunc = func(addr addrs.AbsInputVariableInstance) cty.Value {
			if addr.String() != "var.foo" {
				return cty.NilVal
			}
			t.Logf("reading final val for %s (%#v)", addr.String(), finalVal)
			return finalVal
		}

		n := &NodeRootVariable{
			Addr: addrs.InputVariable{Name: "foo"},
			Config: &configs.Variable{
				Name:           "foo",
				Type:           cty.Number,
				ConstraintType: cty.Number,
				Validations: []*configs.CheckRule{
					{
						Condition: fakeHCLExpressionFunc(func(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
							// This returns true only if the given variable value
							// is exactly cty.Number, which allows us to verify
							// that we were given the value _after_ type
							// conversion.
							// This had previously not been handled correctly,
							// as reported in:
							//     https://github.com/hashicorp/terraform/issues/29899
							vars := ctx.Variables["var"]
							if vars == cty.NilVal || !vars.Type().IsObjectType() || !vars.Type().HasAttribute("foo") {
								t.Logf("var.foo isn't available")
								return cty.False, nil
							}
							val := vars.GetAttr("foo")
							if val == cty.NilVal || val.Type() != cty.Number {
								t.Logf("var.foo is %#v; want a number", val)
								return cty.False, nil
							}
							return cty.True, nil
						}),
						ErrorMessage: hcltest.MockExprLiteral(cty.StringVal("Must be a number.")),
					},
				},
			},
			RawValue: &InputValue{
				// Note: This is a string, but the variable's type constraint
				// is number so it should be converted before use.
				Value:      cty.StringVal("5"),
				SourceType: ValueFromUnknown,
			},
		}

		ctx.ChecksState = checks.NewState(&configs.Config{
			Module: &configs.Module{
				Variables: map[string]*configs.Variable{
					"foo": n.Config,
				},
			},
		})

		diags := n.Execute(ctx, walkApply)
		if diags.HasErrors() {
			t.Fatalf("unexpected error: %s", diags.Err())
		}

		if !ctx.SetRootModuleArgumentCalled {
			t.Fatalf("ctx.SetRootModuleArgument wasn't called")
		}
		if got, want := ctx.SetRootModuleArgumentAddr.String(), "var.foo"; got != want {
			t.Errorf("wrong address for ctx.SetRootModuleArgument\ngot:  %s\nwant: %s", got, want)
		}
		if got, want := ctx.SetRootModuleArgumentValue, cty.NumberIntVal(5); !want.RawEquals(got) {
			// NOTE: The given value was cty.Bool but the type constraint was
			// cty.String, so it was NodeRootVariable's responsibility to convert
			// as part of preparing the "final value".
			t.Errorf("wrong value for ctx.SetRootModuleArgument\ngot:  %#v\nwant: %#v", got, want)
		}
		if status := ctx.Checks().ObjectCheckStatus(n.Addr.Absolute(addrs.RootModuleInstance)); status != checks.StatusPass {
			t.Errorf("expected checks to pass but go %s instead", status)
		}
	})
}

// fakeHCLExpressionFunc is a fake implementation of hcl.Expression that just
// directly produces a value with direct Go code.
//
// An expression of this type has no references and so it cannot access any
// variables from the EvalContext unless something else arranges for them
// to be guaranteed available. For example, custom variable validations just
// unconditionally have access to the variable they are validating regardless
// of references.
type fakeHCLExpressionFunc func(*hcl.EvalContext) (cty.Value, hcl.Diagnostics)

var _ hcl.Expression = fakeHCLExpressionFunc(nil)

func (f fakeHCLExpressionFunc) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) {
	return f(ctx)
}

func (f fakeHCLExpressionFunc) Variables() []hcl.Traversal {
	return nil
}

func (f fakeHCLExpressionFunc) Range() hcl.Range {
	return hcl.Range{
		Filename: "fake",
		Start:    hcl.InitialPos,
		End:      hcl.InitialPos,
	}
}

func (f fakeHCLExpressionFunc) StartRange() hcl.Range {
	return f.Range()
}
