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

package jsonplan

import (
	"encoding/json"
	"reflect"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/plans"
)

func TestOmitUnknowns(t *testing.T) {
	tests := []struct {
		Input cty.Value
		Want  cty.Value
	}{
		{
			cty.StringVal("hello"),
			cty.StringVal("hello"),
		},
		{
			cty.NullVal(cty.String),
			cty.NullVal(cty.String),
		},
		{
			cty.UnknownVal(cty.String),
			cty.NilVal,
		},
		{
			cty.ListValEmpty(cty.String),
			cty.EmptyTupleVal,
		},
		{
			cty.ListVal([]cty.Value{cty.StringVal("hello")}),
			cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
		},
		{
			cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
			cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
		},
		{
			cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
			cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
		},
		{
			cty.ListVal([]cty.Value{cty.StringVal("hello")}),
			cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
		},
		//
		{
			cty.ListVal([]cty.Value{
				cty.StringVal("hello"),
				cty.UnknownVal(cty.String)}),
			cty.TupleVal([]cty.Value{
				cty.StringVal("hello"),
				cty.NullVal(cty.String),
			}),
		},
		{
			cty.MapVal(map[string]cty.Value{
				"hello": cty.True,
				"world": cty.UnknownVal(cty.Bool),
			}),
			cty.ObjectVal(map[string]cty.Value{
				"hello": cty.True,
			}),
		},
		{
			cty.TupleVal([]cty.Value{
				cty.StringVal("alpha"),
				cty.UnknownVal(cty.String),
				cty.StringVal("charlie"),
			}),
			cty.TupleVal([]cty.Value{
				cty.StringVal("alpha"),
				cty.NullVal(cty.String),
				cty.StringVal("charlie"),
			}),
		},
		{
			cty.SetVal([]cty.Value{
				cty.StringVal("dev"),
				cty.StringVal("foo"),
				cty.StringVal("stg"),
				cty.UnknownVal(cty.String),
			}),
			cty.TupleVal([]cty.Value{
				cty.StringVal("dev"),
				cty.StringVal("foo"),
				cty.StringVal("stg"),
				cty.NullVal(cty.String),
			}),
		},
		{
			cty.SetVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.UnknownVal(cty.String),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.StringVal("known"),
				}),
			}),
			cty.TupleVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.StringVal("known"),
				}),
				cty.EmptyObjectVal,
			}),
		},
	}

	for _, test := range tests {
		got := omitUnknowns(test.Input)
		if !reflect.DeepEqual(got, test.Want) {
			t.Errorf(
				"wrong result\ninput: %#v\ngot:   %#v\nwant:  %#v",
				test.Input, got, test.Want,
			)
		}
	}
}

func TestUnknownAsBool(t *testing.T) {
	tests := []struct {
		Input cty.Value
		Want  cty.Value
	}{
		{
			cty.StringVal("hello"),
			cty.False,
		},
		{
			cty.NullVal(cty.String),
			cty.False,
		},
		{
			cty.UnknownVal(cty.String),
			cty.True,
		},

		{
			cty.NullVal(cty.DynamicPseudoType),
			cty.False,
		},
		{
			cty.NullVal(cty.Object(map[string]cty.Type{"test": cty.String})),
			cty.False,
		},
		{
			cty.DynamicVal,
			cty.True,
		},

		{
			cty.ListValEmpty(cty.String),
			cty.EmptyTupleVal,
		},
		{
			cty.ListVal([]cty.Value{cty.StringVal("hello")}),
			cty.TupleVal([]cty.Value{cty.False}),
		},
		{
			cty.ListVal([]cty.Value{cty.NullVal(cty.String)}),
			cty.TupleVal([]cty.Value{cty.False}),
		},
		{
			cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)}),
			cty.TupleVal([]cty.Value{cty.True}),
		},
		{
			cty.SetValEmpty(cty.String),
			cty.EmptyTupleVal,
		},
		{
			cty.SetVal([]cty.Value{cty.StringVal("hello")}),
			cty.TupleVal([]cty.Value{cty.False}),
		},
		{
			cty.SetVal([]cty.Value{cty.NullVal(cty.String)}),
			cty.TupleVal([]cty.Value{cty.False}),
		},
		{
			cty.SetVal([]cty.Value{cty.UnknownVal(cty.String)}),
			cty.TupleVal([]cty.Value{cty.True}),
		},
		{
			cty.EmptyTupleVal,
			cty.EmptyTupleVal,
		},
		{
			cty.TupleVal([]cty.Value{cty.StringVal("hello")}),
			cty.TupleVal([]cty.Value{cty.False}),
		},
		{
			cty.TupleVal([]cty.Value{cty.NullVal(cty.String)}),
			cty.TupleVal([]cty.Value{cty.False}),
		},
		{
			cty.TupleVal([]cty.Value{cty.UnknownVal(cty.String)}),
			cty.TupleVal([]cty.Value{cty.True}),
		},
		{
			cty.MapValEmpty(cty.String),
			cty.EmptyObjectVal,
		},
		{
			cty.MapVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}),
			cty.EmptyObjectVal,
		},
		{
			cty.MapVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}),
			cty.EmptyObjectVal,
		},
		{
			cty.MapVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}),
			cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}),
		},
		{
			cty.EmptyObjectVal,
			cty.EmptyObjectVal,
		},
		{
			cty.ObjectVal(map[string]cty.Value{"greeting": cty.StringVal("hello")}),
			cty.EmptyObjectVal,
		},
		{
			cty.ObjectVal(map[string]cty.Value{"greeting": cty.NullVal(cty.String)}),
			cty.EmptyObjectVal,
		},
		{
			cty.ObjectVal(map[string]cty.Value{"greeting": cty.UnknownVal(cty.String)}),
			cty.ObjectVal(map[string]cty.Value{"greeting": cty.True}),
		},
		{
			cty.SetVal([]cty.Value{
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.UnknownVal(cty.String),
				}),
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.StringVal("known"),
				}),
			}),
			cty.TupleVal([]cty.Value{
				cty.EmptyObjectVal,
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.True,
				}),
			}),
		},
		{
			cty.SetVal([]cty.Value{
				cty.MapValEmpty(cty.String),
				cty.MapVal(map[string]cty.Value{
					"a": cty.StringVal("known"),
				}),
				cty.MapVal(map[string]cty.Value{
					"a": cty.UnknownVal(cty.String),
				}),
			}),
			cty.TupleVal([]cty.Value{
				cty.EmptyObjectVal,
				cty.ObjectVal(map[string]cty.Value{
					"a": cty.True,
				}),
				cty.EmptyObjectVal,
			}),
		},
	}

	for _, test := range tests {
		got := unknownAsBool(test.Input)
		if !reflect.DeepEqual(got, test.Want) {
			t.Errorf(
				"wrong result\ninput: %#v\ngot:   %#v\nwant:  %#v",
				test.Input, got, test.Want,
			)
		}
	}
}

func TestEncodePaths(t *testing.T) {
	tests := map[string]struct {
		Input cty.PathSet
		Want  json.RawMessage
	}{
		"empty set": {
			cty.NewPathSet(),
			json.RawMessage(nil),
		},
		"index path with string and int steps": {
			cty.NewPathSet(cty.IndexStringPath("boop").IndexInt(0)),
			json.RawMessage(`[["boop",0]]`),
		},
		"get attr path with one step": {
			cty.NewPathSet(cty.GetAttrPath("triggers")),
			json.RawMessage(`[["triggers"]]`),
		},
		"multiple paths of different types": {
			cty.NewPathSet(
				cty.GetAttrPath("alpha").GetAttr("beta").GetAttr("gamma"),
				cty.GetAttrPath("triggers").IndexString("name"),
				cty.IndexIntPath(0).IndexInt(1).IndexInt(2).IndexInt(3),
			),
			json.RawMessage(`[["alpha","beta","gamma"],["triggers","name"],[0,1,2,3]]`),
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			got, err := encodePaths(test.Input)
			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}
			if !cmp.Equal(got, test.Want) {
				t.Errorf("wrong result:\n %v\n", cmp.Diff(got, test.Want))
			}
		})
	}
}

func TestOutputs(t *testing.T) {
	root := addrs.RootModuleInstance

	child, diags := addrs.ParseModuleInstanceStr("module.child")
	if diags.HasErrors() {
		t.Fatalf("unexpected errors: %s", diags.Err())
	}

	tests := map[string]struct {
		changes  *plans.ChangesSrc
		expected map[string]Change
	}{
		"copies all outputs": {
			changes: &plans.ChangesSrc{
				Outputs: []*plans.OutputChangeSrc{
					{
						Addr: root.OutputValue("first"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Create,
						},
					},
					{
						Addr: root.OutputValue("second"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Create,
						},
					},
				},
			},
			expected: map[string]Change{
				"first": {
					Actions:         []string{"create"},
					Before:          json.RawMessage("null"),
					After:           json.RawMessage("null"),
					AfterUnknown:    json.RawMessage("false"),
					BeforeSensitive: json.RawMessage("false"),
					AfterSensitive:  json.RawMessage("false"),
				},
				"second": {
					Actions:         []string{"create"},
					Before:          json.RawMessage("null"),
					After:           json.RawMessage("null"),
					AfterUnknown:    json.RawMessage("false"),
					BeforeSensitive: json.RawMessage("false"),
					AfterSensitive:  json.RawMessage("false"),
				},
			},
		},
		"skips non root modules": {
			changes: &plans.ChangesSrc{
				Outputs: []*plans.OutputChangeSrc{
					{
						Addr: root.OutputValue("first"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Create,
						},
					},
					{
						Addr: child.OutputValue("second"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Create,
						},
					},
				},
			},
			expected: map[string]Change{
				"first": {
					Actions:         []string{"create"},
					Before:          json.RawMessage("null"),
					After:           json.RawMessage("null"),
					AfterUnknown:    json.RawMessage("false"),
					BeforeSensitive: json.RawMessage("false"),
					AfterSensitive:  json.RawMessage("false"),
				},
			},
		},
	}
	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			changes, err := MarshalOutputChanges(test.changes)
			if err != nil {
				t.Fatalf("unexpected err: %s", err)
			}

			if !cmp.Equal(changes, test.expected) {
				t.Errorf("wrong result:\n %v\n", cmp.Diff(changes, test.expected))
			}
		})
	}
}

func deepObjectValue(depth int) cty.Value {
	v := cty.ObjectVal(map[string]cty.Value{
		"a": cty.StringVal("a"),
		"b": cty.NumberIntVal(2),
		"c": cty.True,
		"d": cty.UnknownVal(cty.String),
	})

	result := v

	for i := 0; i < depth; i++ {
		result = cty.ObjectVal(map[string]cty.Value{
			"a": result,
			"b": result,
			"c": result,
		})
	}

	return result
}

func BenchmarkUnknownAsBool_2(b *testing.B) {
	value := deepObjectValue(2)
	for n := 0; n < b.N; n++ {
		unknownAsBool(value)
	}
}

func BenchmarkUnknownAsBool_3(b *testing.B) {
	value := deepObjectValue(3)
	for n := 0; n < b.N; n++ {
		unknownAsBool(value)
	}
}

func BenchmarkUnknownAsBool_5(b *testing.B) {
	value := deepObjectValue(5)
	for n := 0; n < b.N; n++ {
		unknownAsBool(value)
	}
}

func BenchmarkUnknownAsBool_7(b *testing.B) {
	value := deepObjectValue(7)
	for n := 0; n < b.N; n++ {
		unknownAsBool(value)
	}
}

func BenchmarkUnknownAsBool_9(b *testing.B) {
	value := deepObjectValue(9)
	for n := 0; n < b.N; n++ {
		unknownAsBool(value)
	}
}
