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

package states

import (
	"fmt"
	"sync"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/zclconf/go-cty/cty"
)

func TestResourceInstanceObject_encode(t *testing.T) {
	value := cty.ObjectVal(map[string]cty.Value{
		"foo": cty.True,
		"obj": cty.ObjectVal(map[string]cty.Value{
			"sensitive": cty.StringVal("secret").Mark(marks.Sensitive),
		}),
		"sensitive_a": cty.StringVal("secret").Mark(marks.Sensitive),
		"sensitive_b": cty.StringVal("secret").Mark(marks.Sensitive),
	})
	schema := providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"foo": {
					Type: cty.Bool,
				},
				"obj": {
					Type: cty.Object(map[string]cty.Type{
						"sensitive": cty.String,
					}),
				},
				"sensitive_a": {
					Type: cty.String,
				},
				"sensitive_b": {
					Type: cty.String,
				},
			},
		},
		Version: 0,
	}
	// The in-memory order of resource dependencies is random, since they're an
	// unordered set.
	depsOne := []addrs.ConfigResource{
		addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "honk"),
		addrs.RootModule.Child("child").Resource(addrs.ManagedResourceMode, "test", "flub"),
		addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "boop"),
	}
	depsTwo := []addrs.ConfigResource{
		addrs.RootModule.Child("child").Resource(addrs.ManagedResourceMode, "test", "flub"),
		addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "boop"),
		addrs.RootModule.Resource(addrs.ManagedResourceMode, "test", "honk"),
	}

	// multiple instances may have been assigned the same deps slice
	objs := []*ResourceInstanceObject{
		{
			Value:        value,
			Status:       ObjectPlanned,
			Dependencies: depsOne,
		},
		{
			Value:        value,
			Status:       ObjectPlanned,
			Dependencies: depsTwo,
		},
		{
			Value:        value,
			Status:       ObjectPlanned,
			Dependencies: depsOne,
		},
		{
			Value:        value,
			Status:       ObjectPlanned,
			Dependencies: depsOne,
		},
	}

	var encoded []*ResourceInstanceObjectSrc

	// Encoding can happen concurrently, so we need to make sure the shared
	// Dependencies are safely handled
	var wg sync.WaitGroup
	var mu sync.Mutex

	for _, obj := range objs {
		wg.Go(func() {
			rios, err := obj.Encode(schema)
			if err != nil {
				t.Errorf("unexpected error: %s", err)
			}
			mu.Lock()
			encoded = append(encoded, rios)
			mu.Unlock()
		})
	}
	wg.Wait()

	// However, identical sets of dependencies should always be written to state
	// in an identical order, so we don't do meaningless state updates on refresh.
	for i := 0; i < len(encoded)-1; i++ {
		if diff := cmp.Diff(encoded[i].Dependencies, encoded[i+1].Dependencies); diff != "" {
			t.Errorf("identical dependencies got encoded in different orders:\n%s", diff)
		}
	}

	// sensitive paths must also be consistent got comparison
	for i := 0; i < len(encoded)-1; i++ {
		a, b := fmt.Sprintf("%#v", encoded[i].AttrSensitivePaths), fmt.Sprintf("%#v", encoded[i+1].AttrSensitivePaths)
		if diff := cmp.Diff(a, b); diff != "" {
			t.Errorf("sensitive paths got encoded in different orders:\n%s", diff)
		}
	}
}

func TestResourceInstanceObject_encodeInvalidMarks(t *testing.T) {
	value := cty.ObjectVal(map[string]cty.Value{
		// State only supports a subset of marks that we know how to persist
		// between plan/apply rounds. All values with other marks must be
		// replaced with unmarked placeholders before attempting to store the
		// value in the state.
		"foo": cty.True.Mark("unsupported"),
	})
	schema := providers.Schema{
		Body: &configschema.Block{
			Attributes: map[string]*configschema.Attribute{
				"foo": {
					Type: cty.Bool,
				},
			},
		},
		Version: 0,
	}

	obj := &ResourceInstanceObject{
		Value:  value,
		Status: ObjectReady,
	}
	_, err := obj.Encode(schema)
	if err == nil {
		t.Fatalf("unexpected success; want error")
	}
	got := err.Error()
	want := `.foo: cannot serialize value marked as cty.NewValueMarks("unsupported") for inclusion in a state snapshot (this is a bug in Terraform)`
	if got != want {
		t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
	}
}
