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

package planning

import (
	"strings"
	"testing"

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

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/engine/internal/exec"
	"github.com/opentofu/opentofu/internal/engine/internal/execgraph"
	"github.com/opentofu/opentofu/internal/plans"
)

// TestExecGraphBuilder_ManagedResourceInstanceSubgraph is a unit test for
// the ManagedResourceInstanceSubgraph method in particular, focused only on
// the items and relationships that function produces.
//
// Interactions between this method and others should be tested elsewhere.
func TestExecGraphBuilder_ManagedResourceInstanceSubgraph(t *testing.T) {
	// instAddr is the resource instance address that each test should use
	// for the resource instance object whose result is returned from the
	// "Build" function. We set the return value as the result for this
	// resource instance so that it'll appear in the graph DebugRepr for
	// comparison.
	instAddr := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test",
		Name: "placeholder",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	tests := map[string]struct {
		// Build has an awkward signature because this test was written for
		// an older prototype design of ManagedResourceInstanceSubgraph that
		// didn't return as many results.
		// TODO: Find a different way to structure this test so that we can
		// confine this complexity only to the testing loop below and not
		// to each individual test case.
		Build func(
			b *execGraphBuilder,
			providerClientRef execgraph.ResultRef[*exec.ProviderClient],
		) (
			execgraph.ResourceInstanceResultRef,
			execgraph.ResourceInstanceResultRef,
			func(execgraph.AnyResultRef),
			func(execgraph.AnyResultRef),
		)
		WantRepr string
	}{
		"create": {
			func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
				return b.ManagedResourceInstanceSubgraph(
					&plans.ResourceInstanceChange{
						Addr:        instAddr,
						PrevRunAddr: instAddr,
						Change: plans.Change{
							Action: plans.Create,
							Before: cty.NullVal(cty.EmptyObject),
							After:  cty.EmptyObjectVal,
						},
					},
					replaceDestroyThenCreate,
					providerClientRef,
				)
			},
			`
				v[0] = cty.EmptyObjectVal;
				
				r[0] = ResourceInstanceDesired(test.placeholder, await());
				r[1] = ManagedFinalPlan(r[0], nil, v[0], nil);
				r[2] = ManagedApply(r[1], nil, nil, await());

				test.placeholder = r[2];
			`,
		},
		"update": {
			func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
				return b.ManagedResourceInstanceSubgraph(
					&plans.ResourceInstanceChange{
						Addr:        instAddr,
						PrevRunAddr: instAddr,
						Change: plans.Change{
							Action: plans.Update,
							Before: cty.StringVal("before"),
							After:  cty.StringVal("after"),
						},
					},
					replaceDestroyThenCreate,
					providerClientRef,
				)
			},
			`
				v[0] = cty.StringVal("after");
				
				r[0] = ResourceInstancePrior(test.placeholder);
				r[1] = ResourceInstanceDesired(test.placeholder, await());
				r[2] = ManagedFinalPlan(r[1], r[0], v[0], nil);
				r[3] = ManagedApply(r[2], nil, nil, await());

				test.placeholder = r[3];
			`,
		},
		"update with move": {
			func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
				oldInstAddr := addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "test",
					Name: "old",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
				return b.ManagedResourceInstanceSubgraph(
					&plans.ResourceInstanceChange{
						Addr:        instAddr,
						PrevRunAddr: oldInstAddr,
						Change: plans.Change{
							Action: plans.Update,
							Before: cty.StringVal("before"),
							After:  cty.StringVal("after"),
						},
					},
					replaceDestroyThenCreate,
					providerClientRef,
				)
			},
			`
				v[0] = cty.StringVal("after");

				r[0] = ResourceInstancePrior(test.old);
				r[1] = ManagedChangeAddr(r[0], test.placeholder);
				r[2] = ResourceInstanceDesired(test.placeholder, await());
				r[3] = ManagedFinalPlan(r[2], r[1], v[0], nil);
				r[4] = ManagedApply(r[3], nil, nil, await());

				test.placeholder = r[4];
			`,
		},
		"delete": {
			func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
				return b.ManagedResourceInstanceSubgraph(
					&plans.ResourceInstanceChange{
						Addr:        instAddr,
						PrevRunAddr: instAddr,
						Change: plans.Change{
							Action: plans.Delete,
							Before: cty.EmptyObjectVal,
							After:  cty.NullVal(cty.EmptyObject),
						},
					},
					replaceDestroyThenCreate,
					providerClientRef,
				)
			},
			`
				v[0] = cty.NullVal(cty.EmptyObject);
				
				r[0] = ResourceInstancePrior(test.placeholder);
				r[1] = ManagedFinalPlan(nil, r[0], v[0], nil);
				r[2] = ManagedApply(r[1], nil, nil, await());

				test.placeholder = nil;
			`,
		},
		"delete then create": {
			func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
				return b.ManagedResourceInstanceSubgraph(
					&plans.ResourceInstanceChange{
						Addr:        instAddr,
						PrevRunAddr: instAddr,
						Change: plans.Change{
							Action: plans.DeleteThenCreate,
							Before: cty.StringVal("before"),
							After:  cty.StringVal("after"),
						},
					},
					replaceDestroyThenCreate,
					providerClientRef,
				)
			},
			`
				v[0] = cty.StringVal("after");
				v[1] = cty.NullVal(cty.String);
				
				r[0] = ResourceInstancePrior(test.placeholder);
				r[1] = ResourceInstanceDesired(test.placeholder, await());
				r[2] = ManagedFinalPlan(r[1], nil, v[0], nil);
				r[3] = ManagedFinalPlan(nil, r[0], v[1], nil);
				r[4] = ManagedApply(r[3], nil, nil, await());
				r[5] = ManagedApply(r[2], nil, nil, await(r[4]));

				test.placeholder = r[5];
			`,
		},
		"delete then create with move": {
			func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
				oldInstAddr := addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "test",
					Name: "old",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
				return b.ManagedResourceInstanceSubgraph(
					&plans.ResourceInstanceChange{
						Addr:        instAddr,
						PrevRunAddr: oldInstAddr,
						Change: plans.Change{
							Action: plans.DeleteThenCreate,
							Before: cty.StringVal("before"),
							After:  cty.StringVal("after"),
						},
					},
					replaceDestroyThenCreate,
					providerClientRef,
				)
			},
			`
				v[0] = cty.StringVal("after");
				v[1] = cty.NullVal(cty.String);

				r[0] = ResourceInstancePrior(test.old);
				r[1] = ManagedChangeAddr(r[0], test.placeholder);
				r[2] = ResourceInstanceDesired(test.placeholder, await());
				r[3] = ManagedFinalPlan(r[2], nil, v[0], nil);
				r[4] = ManagedFinalPlan(nil, r[1], v[1], nil);
				r[5] = ManagedApply(r[4], nil, nil, await());
				r[6] = ManagedApply(r[3], nil, nil, await(r[5]));

				test.placeholder = r[6];
			`,
		},
		"create then delete": {
			func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
				return b.ManagedResourceInstanceSubgraph(
					&plans.ResourceInstanceChange{
						Addr:        instAddr,
						PrevRunAddr: instAddr,
						Change: plans.Change{
							Action: plans.CreateThenDelete,
							Before: cty.StringVal("before"),
							After:  cty.StringVal("after"),
						},
					},
					replaceCreateThenDestroy,
					providerClientRef,
				)
			},
			`
				v[0] = cty.StringVal("after");
				v[1] = cty.NullVal(cty.String);

				r[0] = ResourceInstancePrior(test.placeholder);
				r[1] = ResourceInstanceDesired(test.placeholder, await());
				r[2] = ManagedFinalPlan(r[1], nil, v[0], nil);
				r[3] = ManagedFinalPlan(nil, r[0], v[1], nil);
				r[4] = ManagedDepose(r[0], await(r[2], r[3]));
				r[5] = ManagedApply(r[2], r[4], nil, await());
				r[6] = ManagedApply(r[3], nil, nil, await(r[5]));

				test.placeholder = r[5];
			`,
		},
		"create then delete with move": {
			func(b *execGraphBuilder, providerClientRef execgraph.ResultRef[*exec.ProviderClient]) (execgraph.ResourceInstanceResultRef, execgraph.ResourceInstanceResultRef, func(execgraph.AnyResultRef), func(execgraph.AnyResultRef)) {
				oldInstAddr := addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "test",
					Name: "old",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
				return b.ManagedResourceInstanceSubgraph(
					&plans.ResourceInstanceChange{
						Addr:        instAddr,
						PrevRunAddr: oldInstAddr,
						Change: plans.Change{
							Action: plans.CreateThenDelete,
							Before: cty.StringVal("before"),
							After:  cty.StringVal("after"),
						},
					},
					replaceCreateThenDestroy,
					providerClientRef,
				)
			},
			`
				v[0] = cty.StringVal("after");
				v[1] = cty.NullVal(cty.String);

				r[0] = ResourceInstancePrior(test.old);
				r[1] = ManagedChangeAddr(r[0], test.placeholder);
				r[2] = ResourceInstanceDesired(test.placeholder, await());
				r[3] = ManagedFinalPlan(r[2], nil, v[0], nil);
				r[4] = ManagedFinalPlan(nil, r[1], v[1], nil);
				r[5] = ManagedDepose(r[1], await(r[3], r[4]));
				r[6] = ManagedApply(r[3], r[5], nil, await());
				r[7] = ManagedApply(r[4], nil, nil, await(r[6]));

				test.placeholder = r[6];
			`,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			builder := newExecGraphBuilder()
			// This test is focused only on the resource instance subgraphs,
			// so we just use a placeholder nil result for the provider client.
			providerClientRef := execgraph.NilResultRef[*exec.ProviderClient]()
			// FIXME: We're currently ignoring all but the first result
			// because this test was originally written for an older variant
			// of this function which only had one result. We should find a
			// nice way to restructure this test so that it can check whether
			// _all_ of the return values are correct.
			resultRef, _, _, _ := test.Build(builder, providerClientRef)
			builder.lower.SetResourceInstanceFinalStateResult(instAddr, resultRef)

			graph := builder.Finish()
			gotGraphRepr := strings.TrimSpace(graph.DebugRepr())
			wantGraphRepr := strings.TrimSpace(stripCommonLeadingTabs(test.WantRepr))
			if diff := cmp.Diff(wantGraphRepr, gotGraphRepr); diff != "" {
				t.Error("wrong result\n" + diff)
			}
		})
	}
}
