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

package statemgr

import (
	"reflect"
	"testing"

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

	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/states"
	"github.com/opentofu/opentofu/internal/states/statefile"
)

// TestFull is a helper for testing full state manager implementations. It
// expects that the given implementation is pre-loaded with a snapshot of the
// result from TestFullInitialState.
//
// If the given state manager also implements PersistentMeta, this function
// will test that the snapshot metadata changes as expected between calls
// to the methods of Persistent.
func TestFull(t *testing.T, s Full) {
	t.Helper()

	if err := s.RefreshState(t.Context()); err != nil {
		t.Fatalf("err: %s", err)
	}

	// Check that the initial state is correct.
	// These do have different Lineages, but we will replace current below.
	initial := TestFullInitialState()
	if state := s.State(); !state.Equal(initial) {
		t.Fatalf("state does not match expected initial state\n\ngot:\n%s\nwant:\n%s", spew.Sdump(state), spew.Sdump(initial))
	}

	var initialMeta SnapshotMeta
	if sm, ok := s.(PersistentMeta); ok {
		initialMeta = sm.StateSnapshotMeta()
	}

	// Now we've proven that the state we're starting with is an initial
	// state, we'll complete our work here with that state, since otherwise
	// further writes would violate the invariant that we only try to write
	// states that share the same lineage as what was initially written.
	current := s.State()

	// Write a new state and verify that we have it
	current.RootModule().SetOutputValue("bar", cty.StringVal("baz"), false, "")

	if err := s.WriteState(current); err != nil {
		t.Fatalf("err: %s", err)
	}

	if actual := s.State(); !actual.Equal(current) {
		t.Fatalf("bad:\n%#v\n\n%#v", actual, current)
	}

	// Test persistence
	if err := s.PersistState(t.Context(), nil); err != nil {
		t.Fatalf("err: %s", err)
	}

	// Refresh if we got it
	if err := s.RefreshState(t.Context()); err != nil {
		t.Fatalf("err: %s", err)
	}

	var newMeta SnapshotMeta
	if sm, ok := s.(PersistentMeta); ok {
		newMeta = sm.StateSnapshotMeta()
		if got, want := newMeta.Lineage, initialMeta.Lineage; got != want {
			t.Errorf("Lineage changed from %q to %q", want, got)
		}
		if after, before := newMeta.Serial, initialMeta.Serial; after == before {
			t.Errorf("Serial didn't change from %d after new module added", before)
		}
	}

	// Same serial
	serial := newMeta.Serial
	if err := s.WriteState(current); err != nil {
		t.Fatalf("err: %s", err)
	}
	if err := s.PersistState(t.Context(), nil); err != nil {
		t.Fatalf("err: %s", err)
	}

	if sm, ok := s.(PersistentMeta); ok {
		newMeta = sm.StateSnapshotMeta()
		if newMeta.Serial != serial {
			t.Fatalf("serial changed after persisting with no changes: got %d, want %d", newMeta.Serial, serial)
		}
	}

	if sm, ok := s.(PersistentMeta); ok {
		newMeta = sm.StateSnapshotMeta()
	}

	// Change the serial
	current = current.DeepCopy()
	current.EnsureModule(addrs.RootModuleInstance).SetOutputValue(
		"serialCheck", cty.StringVal("true"), false, "",
	)
	if err := s.WriteState(current); err != nil {
		t.Fatalf("err: %s", err)
	}
	if err := s.PersistState(t.Context(), nil); err != nil {
		t.Fatalf("err: %s", err)
	}

	if sm, ok := s.(PersistentMeta); ok {
		oldMeta := newMeta
		newMeta = sm.StateSnapshotMeta()

		if newMeta.Serial <= serial {
			t.Fatalf("serial incorrect after persisting with changes: got %d, want > %d", newMeta.Serial, serial)
		}

		if newMeta.TerraformVersion != oldMeta.TerraformVersion {
			t.Fatalf("TFVersion changed from %s to %s", oldMeta.TerraformVersion, newMeta.TerraformVersion)
		}

		// verify that Lineage doesn't change along with Serial, or during copying.
		if newMeta.Lineage != oldMeta.Lineage {
			t.Fatalf("Lineage changed from %q to %q", oldMeta.Lineage, newMeta.Lineage)
		}
	}

	// Check that State() returns a copy by modifying the copy and comparing
	// to the current state.
	stateCopy := s.State()
	stateCopy.EnsureModule(addrs.RootModuleInstance.Child("another", addrs.NoKey))
	if reflect.DeepEqual(stateCopy, s.State()) {
		t.Fatal("State() should return a copy")
	}

	// our current expected state should also marshal identically to the persisted state
	if !statefile.StatesMarshalEqual(current, s.State()) {
		t.Fatalf("Persisted state altered unexpectedly.\n\ngot:\n%s\nwant:\n%s", spew.Sdump(s.State()), spew.Sdump(current))
	}
}

// TestFullInitialState is a state that should be snapshotted into a
// full state manager before passing it into TestFull.
func TestFullInitialState() *states.State {
	state := states.NewState()
	childMod := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
	rAddr := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "null_resource",
		Name: "foo",
	}
	providerAddr := addrs.AbsProviderConfig{
		Provider: addrs.NewDefaultProvider(rAddr.ImpliedProvider()),
		Module:   addrs.RootModule,
	}
	childMod.SetResourceProvider(rAddr, providerAddr)

	state.RootModule().SetOutputValue("sensitive_output", cty.StringVal("it's a secret"), true, "")
	state.RootModule().SetOutputValue("nonsensitive_output", cty.StringVal("hello, world!"), false, "")

	return state
}
