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

package states

import (
	"sync"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/lang/marks"
	"github.com/zclconf/go-cty/cty"
)

func TestResourceInstanceObject_encode(t *testing.T) {
	value := cty.ObjectVal(map[string]cty.Value{
		"foo": cty.True,
	})
	// 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(value.Type(), 0, 0)
			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)
		}
	}
}

func TestResourceInstanceObject_encode_sensitivity(t *testing.T) {
	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"),
	}

	tests := []struct {
		inputObj           *ResourceInstanceObject
		wantSensitivePaths bool
		wantTransientPaths bool
	}{
		{
			inputObj: &ResourceInstanceObject{
				Value: cty.ObjectVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.BoolVal(true).Mark(marks.Sensitive),
					}),
				}),
				Status:       ObjectPlanned,
				Dependencies: depsOne,
			},
			wantSensitivePaths: true,
			wantTransientPaths: true,
		},
		{
			inputObj: &ResourceInstanceObject{
				Value: cty.ObjectVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.BoolVal(true).Mark("non-sensitive"),
					}),
				}),
				Status:       ObjectPlanned,
				Dependencies: depsOne,
			},
			wantSensitivePaths: false,
			wantTransientPaths: true,
		},
		{
			inputObj: &ResourceInstanceObject{
				Value: cty.ObjectVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"bar": cty.BoolVal(true),
					}),
				}),
				Status:       ObjectPlanned,
				Dependencies: depsOne,
			},
			wantSensitivePaths: false,
			wantTransientPaths: false,
		},
	}

	for _, test := range tests {
		encoded, err := test.inputObj.Encode(test.inputObj.Value.Type(), 0, 0)
		if err != nil {
			t.Fatalf("Unexpected error: %v", err)
		}

		if test.wantSensitivePaths && len(encoded.AttrSensitivePaths) == 0 {
			t.Fatalf("No AttrSensitivePaths found")
		}

		if !test.wantSensitivePaths && len(encoded.AttrSensitivePaths) != 0 {
			t.Fatalf("Got unexpected AttrSensitivePaths: %v", encoded.AttrSensitivePaths)
		}

		if test.wantTransientPaths && len(encoded.TransientPathValueMarks) == 0 {
			t.Fatalf("No TransientPathValueMarks found")
		}

		if !test.wantTransientPaths && len(encoded.TransientPathValueMarks) != 0 {
			t.Fatalf("Got unexpected TransientPathValueMarks: %v", encoded.TransientPathValueMarks)
		}
	}
}
