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

package jsonplan

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

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

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/terraform"
)

func ptrOf[T any](v T) *T {
	return &v
}

func TestMarshalAttributeValues(t *testing.T) {
	tests := []struct {
		Attr   cty.Value
		Schema *configschema.Block
		Want   attributeValues
	}{
		{
			cty.NilVal,
			&configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			nil,
		},
		{
			cty.NullVal(cty.String),
			&configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			nil,
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.StringVal("bar"),
			}),
			&configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			attributeValues{"foo": json.RawMessage(`"bar"`)},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"foo": cty.NullVal(cty.String),
			}),
			&configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"foo": {
						Type:     cty.String,
						Optional: true,
					},
				},
			},
			attributeValues{"foo": json.RawMessage(`null`)},
		},
		{
			cty.ObjectVal(map[string]cty.Value{
				"bar": cty.MapVal(map[string]cty.Value{
					"hello": cty.StringVal("world"),
				}),
				"baz": cty.ListVal([]cty.Value{
					cty.StringVal("goodnight"),
					cty.StringVal("moon"),
				}),
			}),
			&configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"bar": {
						Type:     cty.Map(cty.String),
						Required: true,
					},
					"baz": {
						Type:     cty.List(cty.String),
						Optional: true,
					},
				},
			},
			attributeValues{
				"bar": json.RawMessage(`{"hello":"world"}`),
				"baz": json.RawMessage(`["goodnight","moon"]`),
			},
		},
	}

	for _, test := range tests {
		got := marshalAttributeValues(test.Attr)
		eq := reflect.DeepEqual(got, test.Want)
		if !eq {
			t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
		}
	}
}

func TestMarshalPlannedOutputs(t *testing.T) {
	after, _ := plans.NewDynamicValue(cty.StringVal("after"), cty.DynamicPseudoType)

	tests := []struct {
		Changes *plans.ChangesSrc
		Want    map[string]output
		Err     bool
	}{
		{
			&plans.ChangesSrc{},
			nil,
			false,
		},
		{
			&plans.ChangesSrc{
				Outputs: []*plans.OutputChangeSrc{
					{
						Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Create,
							After:  after,
						},
						Sensitive: false,
					},
				},
			},
			map[string]output{
				"bar": {
					Sensitive: false,
					Type:      json.RawMessage(`"string"`),
					Value:     json.RawMessage(`"after"`),
				},
			},
			false,
		},
		{ // Delete action
			&plans.ChangesSrc{
				Outputs: []*plans.OutputChangeSrc{
					{
						Addr: addrs.OutputValue{Name: "bar"}.Absolute(addrs.RootModuleInstance),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Delete,
						},
						Sensitive: false,
					},
				},
			},
			map[string]output{},
			false,
		},
	}

	for _, test := range tests {
		got, err := marshalPlannedOutputs(test.Changes)
		if test.Err {
			if err == nil {
				t.Fatal("succeeded; want error")
			}
			return
		} else if err != nil {
			t.Fatalf("unexpected error: %s", err)
		}

		eq := reflect.DeepEqual(got, test.Want)
		if !eq {
			t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
		}
	}
}

func TestMarshalPlanResources(t *testing.T) {
	tests := map[string]struct {
		Action         plans.Action
		Before         cty.Value
		After          cty.Value
		Want           []resource
		Err            bool
		BeforeIdentity cty.Value
		AfterIdentity  cty.Value
	}{
		"create with unknowns": {
			Action: plans.Create,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"woozles": cty.UnknownVal(cty.String),
				"foozles": cty.UnknownVal(cty.String),
			}),
			Want: []resource{{
				Address:         "test_thing.example",
				Mode:            "managed",
				Type:            "test_thing",
				Name:            "example",
				Index:           addrs.InstanceKey(nil),
				ProviderName:    "registry.terraform.io/hashicorp/test",
				SchemaVersion:   1,
				AttributeValues: attributeValues{},
				SensitiveValues: json.RawMessage("{}"),
			}},
			Err: false,
		},
		"delete with null and nil": {
			Action: plans.Delete,
			Before: cty.NullVal(cty.EmptyObject),
			After:  cty.NilVal,
			Want:   nil,
			Err:    false,
		},
		"delete": {
			Action: plans.Delete,
			Before: cty.ObjectVal(map[string]cty.Value{
				"woozles": cty.StringVal("foo"),
				"foozles": cty.StringVal("bar"),
			}),
			After: cty.NullVal(cty.Object(map[string]cty.Type{
				"woozles": cty.String,
				"foozles": cty.String,
			})),
			Want: nil,
			Err:  false,
		},
		"forget": {
			Action: plans.Forget,
			Before: cty.ObjectVal(map[string]cty.Value{
				"woozles": cty.StringVal("foo"),
				"foozles": cty.StringVal("bar"),
			}),
			After: cty.NullVal(cty.Object(map[string]cty.Type{
				"woozles": cty.String,
				"foozles": cty.String,
			})),
			Want: nil,
			Err:  false,
		},
		"update without unknowns": {
			Action: plans.Update,
			Before: cty.ObjectVal(map[string]cty.Value{
				"woozles": cty.StringVal("foo"),
				"foozles": cty.StringVal("bar"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"woozles": cty.StringVal("baz"),
				"foozles": cty.StringVal("bat"),
			}),
			Want: []resource{{
				Address:       "test_thing.example",
				Mode:          "managed",
				Type:          "test_thing",
				Name:          "example",
				Index:         addrs.InstanceKey(nil),
				ProviderName:  "registry.terraform.io/hashicorp/test",
				SchemaVersion: 1,
				AttributeValues: attributeValues{
					"woozles": json.RawMessage(`"baz"`),
					"foozles": json.RawMessage(`"bat"`),
				},
				SensitiveValues: json.RawMessage("{}"),
			}},
			Err: false,
		},
		"with identity": {
			Action: plans.Create,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"woozles": cty.StringVal("woo"),
				"foozles": cty.NullVal(cty.String),
			}),
			BeforeIdentity: cty.NullVal(cty.EmptyObject),
			AfterIdentity: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("someId"),
			}),
			Want: []resource{{
				Address:       "test_thing.example",
				Mode:          "managed",
				Type:          "test_thing",
				Name:          "example",
				Index:         addrs.InstanceKey(nil),
				ProviderName:  "registry.terraform.io/hashicorp/test",
				SchemaVersion: 1,
				AttributeValues: attributeValues{
					"woozles": json.RawMessage(`"woo"`),
					"foozles": json.RawMessage(`null`),
				},
				SensitiveValues:       json.RawMessage("{}"),
				IdentitySchemaVersion: ptrOf[uint64](2),
				IdentityValues: attributeValues{
					"id": json.RawMessage(`"someId"`),
				},
			}},
			Err: false,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			before, err := plans.NewDynamicValue(test.Before, test.Before.Type())
			if err != nil {
				t.Fatal(err)
			}

			after, err := plans.NewDynamicValue(test.After, test.After.Type())
			if err != nil {
				t.Fatal(err)
			}

			var beforeIdentity, afterIdentity plans.DynamicValue
			if !test.BeforeIdentity.IsNull() {
				var err error
				beforeIdentity, err = plans.NewDynamicValue(test.BeforeIdentity, test.BeforeIdentity.Type())
				if err != nil {
					t.Fatal(err)
				}
			}
			if !test.AfterIdentity.IsNull() {
				var err error
				afterIdentity, err = plans.NewDynamicValue(test.AfterIdentity, test.AfterIdentity.Type())
				if err != nil {
					t.Fatal(err)
				}
			}

			testChange := &plans.ChangesSrc{
				Resources: []*plans.ResourceInstanceChangeSrc{
					{
						Addr: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "example",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
						ProviderAddr: addrs.AbsProviderConfig{
							Provider: addrs.NewDefaultProvider("test"),
							Module:   addrs.RootModule,
						},
						ChangeSrc: plans.ChangeSrc{
							Action:         test.Action,
							Before:         before,
							After:          after,
							BeforeIdentity: beforeIdentity,
							AfterIdentity:  afterIdentity,
						},
					},
				},
			}

			ris := testResourceAddrs()

			got, err := marshalPlanResources(testChange, ris, testSchemas())
			if test.Err {
				if err == nil {
					t.Fatal("succeeded; want error")
				}
				return
			} else if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}

			eq := reflect.DeepEqual(got, test.Want)
			if !eq {
				t.Fatalf("wrong result:\nGot: %#v\nWant: %#v\n", got, test.Want)
			}
		})
	}
}

func TestMarshalPlanValuesNoopDeposed(t *testing.T) {
	dynamicNull, err := plans.NewDynamicValue(cty.NullVal(cty.DynamicPseudoType), cty.DynamicPseudoType)
	if err != nil {
		t.Fatal(err)
	}
	testChange := &plans.ChangesSrc{
		Resources: []*plans.ResourceInstanceChangeSrc{
			{
				Addr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "test_thing",
					Name: "example",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				DeposedKey: "12345678",
				ProviderAddr: addrs.AbsProviderConfig{
					Provider: addrs.NewDefaultProvider("test"),
					Module:   addrs.RootModule,
				},
				ChangeSrc: plans.ChangeSrc{
					Action: plans.NoOp,
					Before: dynamicNull,
					After:  dynamicNull,
				},
			},
		},
	}

	_, err = marshalPlannedValues(testChange, testSchemas())
	if err != nil {
		t.Fatal(err)
	}
}

func testSchemas() *terraform.Schemas {
	return &terraform.Schemas{
		Providers: map[addrs.Provider]providers.ProviderSchema{
			addrs.NewDefaultProvider("test"): providers.ProviderSchema{
				ResourceTypes: map[string]providers.Schema{
					"test_thing": {
						Version: 1,
						Body: &configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"woozles": {Type: cty.String, Optional: true, Computed: true},
								"foozles": {Type: cty.String, Optional: true},
							},
						},
						IdentityVersion: 2,
						Identity: &configschema.Object{
							Attributes: map[string]*configschema.Attribute{
								"id": {Type: cty.String, Required: true},
							},
							Nesting: configschema.NestingSingle,
						},
					},
				},
			},
		},
	}
}

func testResourceAddrs() []addrs.AbsResourceInstance {
	return []addrs.AbsResourceInstance{
		mustAddr("test_thing.example"),
	}
}

func mustAddr(str string) addrs.AbsResourceInstance {
	addr, diags := addrs.ParseAbsResourceInstanceStr(str)
	if diags.HasErrors() {
		panic(diags.Err())
	}
	return addr
}
