package states

import (
	"fmt"
	"reflect"
	"testing"

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

	"github.com/camptocamp/terraboard/internal/terraform/addrs"
	"github.com/camptocamp/terraboard/internal/terraform/lang/marks"
)

func TestState(t *testing.T) {
	// This basic tests exercises the main mutation methods to construct
	// a state. It is not fully comprehensive, so other tests should visit
	// more esoteric codepaths.

	state := NewState()

	rootModule := state.RootModule()
	if rootModule == nil {
		t.Errorf("root module is nil; want valid object")
	}

	rootModule.SetLocalValue("foo", cty.StringVal("foo value"))
	rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false)
	rootModule.SetOutputValue("secret", cty.StringVal("secret value"), true)
	rootModule.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "baz",
		}.Instance(addrs.IntKey(0)),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)

	childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
	childModule.SetOutputValue("pizza", cty.StringVal("hawaiian"), false)
	multiModA := state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")))
	multiModA.SetOutputValue("pizza", cty.StringVal("cheese"), false)
	multiModB := state.EnsureModule(addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")))
	multiModB.SetOutputValue("pizza", cty.StringVal("sausage"), false)

	want := &State{
		Modules: map[string]*Module{
			"": {
				Addr: addrs.RootModuleInstance,
				LocalValues: map[string]cty.Value{
					"foo": cty.StringVal("foo value"),
				},
				OutputValues: map[string]*OutputValue{
					"bar": {
						Addr: addrs.AbsOutputValue{
							OutputValue: addrs.OutputValue{
								Name: "bar",
							},
						},
						Value:     cty.StringVal("bar value"),
						Sensitive: false,
					},
					"secret": {
						Addr: addrs.AbsOutputValue{
							OutputValue: addrs.OutputValue{
								Name: "secret",
							},
						},
						Value:     cty.StringVal("secret value"),
						Sensitive: true,
					},
				},
				Resources: map[string]*Resource{
					"test_thing.baz": {
						Addr: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_thing",
							Name: "baz",
						}.Absolute(addrs.RootModuleInstance),

						Instances: map[addrs.InstanceKey]*ResourceInstance{
							addrs.IntKey(0): {
								Current: &ResourceInstanceObjectSrc{
									SchemaVersion: 1,
									Status:        ObjectReady,
									AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
								},
								Deposed: map[DeposedKey]*ResourceInstanceObjectSrc{},
							},
						},
						ProviderConfig: addrs.AbsProviderConfig{
							Provider: addrs.NewDefaultProvider("test"),
							Module:   addrs.RootModule,
						},
					},
				},
			},
			"module.child": {
				Addr:        addrs.RootModuleInstance.Child("child", addrs.NoKey),
				LocalValues: map[string]cty.Value{},
				OutputValues: map[string]*OutputValue{
					"pizza": {
						Addr: addrs.AbsOutputValue{
							Module: addrs.RootModuleInstance.Child("child", addrs.NoKey),
							OutputValue: addrs.OutputValue{
								Name: "pizza",
							},
						},
						Value:     cty.StringVal("hawaiian"),
						Sensitive: false,
					},
				},
				Resources: map[string]*Resource{},
			},
			`module.multi["a"]`: {
				Addr:        addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")),
				LocalValues: map[string]cty.Value{},
				OutputValues: map[string]*OutputValue{
					"pizza": {
						Addr: addrs.AbsOutputValue{
							Module: addrs.RootModuleInstance.Child("multi", addrs.StringKey("a")),
							OutputValue: addrs.OutputValue{
								Name: "pizza",
							},
						},
						Value:     cty.StringVal("cheese"),
						Sensitive: false,
					},
				},
				Resources: map[string]*Resource{},
			},
			`module.multi["b"]`: {
				Addr:        addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")),
				LocalValues: map[string]cty.Value{},
				OutputValues: map[string]*OutputValue{
					"pizza": {
						Addr: addrs.AbsOutputValue{
							Module: addrs.RootModuleInstance.Child("multi", addrs.StringKey("b")),
							OutputValue: addrs.OutputValue{
								Name: "pizza",
							},
						},
						Value:     cty.StringVal("sausage"),
						Sensitive: false,
					},
				},
				Resources: map[string]*Resource{},
			},
		},
	}

	{
		// Our structure goes deep, so we need to temporarily override the
		// deep package settings to ensure that we visit the full structure.
		oldDeepDepth := deep.MaxDepth
		oldDeepCompareUnexp := deep.CompareUnexportedFields
		deep.MaxDepth = 50
		deep.CompareUnexportedFields = true
		defer func() {
			deep.MaxDepth = oldDeepDepth
			deep.CompareUnexportedFields = oldDeepCompareUnexp
		}()
	}

	for _, problem := range deep.Equal(state, want) {
		t.Error(problem)
	}

	expectedOutputs := map[string]string{
		`module.multi["a"].output.pizza`: "cheese",
		`module.multi["b"].output.pizza`: "sausage",
	}

	for _, o := range state.ModuleOutputs(addrs.RootModuleInstance, addrs.ModuleCall{Name: "multi"}) {
		addr := o.Addr.String()
		expected := expectedOutputs[addr]
		delete(expectedOutputs, addr)

		if expected != o.Value.AsString() {
			t.Fatalf("expected %q:%q, got %q", addr, expected, o.Value.AsString())
		}
	}

	for addr, o := range expectedOutputs {
		t.Fatalf("missing output %q:%q", addr, o)
	}
}

func TestStateDeepCopyObject(t *testing.T) {
	obj := &ResourceInstanceObject{
		Value: cty.ObjectVal(map[string]cty.Value{
			"id": cty.StringVal("id"),
		}),
		Private: []byte("private"),
		Status:  ObjectReady,
		Dependencies: []addrs.ConfigResource{
			{
				Module: addrs.RootModule,
				Resource: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "test_instance",
					Name: "bar",
				},
			},
		},
		CreateBeforeDestroy: true,
	}

	objCopy := obj.DeepCopy()
	if !reflect.DeepEqual(obj, objCopy) {
		t.Fatalf("not equal\n%#v\n%#v", obj, objCopy)
	}
}

func TestStateDeepCopy(t *testing.T) {
	state := NewState()

	rootModule := state.RootModule()
	if rootModule == nil {
		t.Errorf("root module is nil; want valid object")
	}

	rootModule.SetLocalValue("foo", cty.StringVal("foo value"))
	rootModule.SetOutputValue("bar", cty.StringVal("bar value"), false)
	rootModule.SetOutputValue("secret", cty.StringVal("secret value"), true)
	rootModule.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "baz",
		}.Instance(addrs.IntKey(0)),
		&ResourceInstanceObjectSrc{
			Status:              ObjectReady,
			SchemaVersion:       1,
			AttrsJSON:           []byte(`{"woozles":"confuzles"}`),
			Private:             []byte("private data"),
			Dependencies:        []addrs.ConfigResource{},
			CreateBeforeDestroy: true,
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)
	rootModule.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "bar",
		}.Instance(addrs.IntKey(0)),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
			// Sensitive path at "woozles"
			AttrSensitivePaths: []cty.PathValueMarks{
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "woozles"}},
					Marks: cty.NewValueMarks(marks.Sensitive),
				},
			},
			Private: []byte("private data"),
			Dependencies: []addrs.ConfigResource{
				{
					Module: addrs.RootModule,
					Resource: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "test_thing",
						Name: "baz",
					},
				},
			},
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)

	childModule := state.EnsureModule(addrs.RootModuleInstance.Child("child", addrs.NoKey))
	childModule.SetOutputValue("pizza", cty.StringVal("hawaiian"), false)

	stateCopy := state.DeepCopy()
	if !state.Equal(stateCopy) {
		t.Fatalf("\nexpected:\n%q\ngot:\n%q\n", state, stateCopy)
	}
}

func TestStateHasResourceInstanceObjects(t *testing.T) {
	providerConfig := addrs.AbsProviderConfig{
		Module:   addrs.RootModule,
		Provider: addrs.MustParseProviderSourceString("test/test"),
	}
	childModuleProviderConfig := addrs.AbsProviderConfig{
		Module:   addrs.RootModule.Child("child"),
		Provider: addrs.MustParseProviderSourceString("test/test"),
	}

	tests := map[string]struct {
		Setup func(ss *SyncState)
		Want  bool
	}{
		"empty": {
			func(ss *SyncState) {},
			false,
		},
		"one current, ready object in root module": {
			func(ss *SyncState) {
				ss.SetResourceInstanceCurrent(
					mustAbsResourceAddr("test.foo").Instance(addrs.NoKey),
					&ResourceInstanceObjectSrc{
						AttrsJSON: []byte(`{}`),
						Status:    ObjectReady,
					},
					providerConfig,
				)
			},
			true,
		},
		"one current, ready object in child module": {
			func(ss *SyncState) {
				ss.SetResourceInstanceCurrent(
					mustAbsResourceAddr("module.child.test.foo").Instance(addrs.NoKey),
					&ResourceInstanceObjectSrc{
						AttrsJSON: []byte(`{}`),
						Status:    ObjectReady,
					},
					childModuleProviderConfig,
				)
			},
			true,
		},
		"one current, tainted object in root module": {
			func(ss *SyncState) {
				ss.SetResourceInstanceCurrent(
					mustAbsResourceAddr("test.foo").Instance(addrs.NoKey),
					&ResourceInstanceObjectSrc{
						AttrsJSON: []byte(`{}`),
						Status:    ObjectTainted,
					},
					providerConfig,
				)
			},
			true,
		},
		"one deposed, ready object in root module": {
			func(ss *SyncState) {
				ss.SetResourceInstanceDeposed(
					mustAbsResourceAddr("test.foo").Instance(addrs.NoKey),
					DeposedKey("uhoh"),
					&ResourceInstanceObjectSrc{
						AttrsJSON: []byte(`{}`),
						Status:    ObjectTainted,
					},
					providerConfig,
				)
			},
			true,
		},
		"one empty resource husk in root module": {
			func(ss *SyncState) {
				// Current Terraform doesn't actually create resource husks
				// as part of its everyday work, so this is a "should never
				// happen" case but we'll test to make sure we're robust to
				// it anyway, because this was a historical bug blocking
				// "terraform workspace delete" and similar.
				ss.SetResourceInstanceCurrent(
					mustAbsResourceAddr("test.foo").Instance(addrs.NoKey),
					&ResourceInstanceObjectSrc{
						AttrsJSON: []byte(`{}`),
						Status:    ObjectTainted,
					},
					providerConfig,
				)
				s := ss.Lock()
				delete(s.Modules[""].Resources["test.foo"].Instances, addrs.NoKey)
				ss.Unlock()
			},
			false,
		},
		"one current data resource object in root module": {
			func(ss *SyncState) {
				ss.SetResourceInstanceCurrent(
					mustAbsResourceAddr("data.test.foo").Instance(addrs.NoKey),
					&ResourceInstanceObjectSrc{
						AttrsJSON: []byte(`{}`),
						Status:    ObjectReady,
					},
					providerConfig,
				)
			},
			false, // data resources aren't managed resources, so they don't count
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			state := BuildState(test.Setup)
			got := state.HasManagedResourceInstanceObjects()
			if got != test.Want {
				t.Errorf("wrong result\nstate content: (using legacy state string format; might not be comprehensive)\n%s\n\ngot:  %t\nwant: %t", state, got, test.Want)
			}
		})
	}

}

func TestState_MoveAbsResource(t *testing.T) {
	// Set up a starter state for the embedded tests, which should start from a copy of this state.
	state := NewState()
	rootModule := state.RootModule()
	rootModule.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "foo",
		}.Instance(addrs.IntKey(0)),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)
	src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Absolute(addrs.RootModuleInstance)

	t.Run("basic move", func(t *testing.T) {
		s := state.DeepCopy()
		dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(addrs.RootModuleInstance)

		s.MoveAbsResource(src, dst)

		if s.Empty() {
			t.Fatal("unexpected empty state")
		}

		if len(s.RootModule().Resources) != 1 {
			t.Fatalf("wrong number of resources in state; expected 1, found %d", len(state.RootModule().Resources))
		}

		got := s.Resource(dst)
		if got.Addr.Resource != dst.Resource {
			t.Fatalf("dst resource not in state")
		}
	})

	t.Run("move to new module", func(t *testing.T) {
		s := state.DeepCopy()
		dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("one"))
		dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(dstModule)

		s.MoveAbsResource(src, dst)

		if s.Empty() {
			t.Fatal("unexpected empty state")
		}

		if s.Module(dstModule) == nil {
			t.Fatalf("child module %s not in state", dstModule.String())
		}

		if len(s.Module(dstModule).Resources) != 1 {
			t.Fatalf("wrong number of resources in state; expected 1, found %d", len(s.Module(dstModule).Resources))
		}

		got := s.Resource(dst)
		if got.Addr.Resource != dst.Resource {
			t.Fatalf("dst resource not in state")
		}
	})

	t.Run("from a child module to root", func(t *testing.T) {
		s := state.DeepCopy()
		srcModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
		cm := s.EnsureModule(srcModule)
		cm.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_thing",
				Name: "child",
			}.Instance(addrs.IntKey(0)), // Moving the AbsResouce moves all instances
			&ResourceInstanceObjectSrc{
				Status:        ObjectReady,
				SchemaVersion: 1,
				AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)
		cm.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_thing",
				Name: "child",
			}.Instance(addrs.IntKey(1)), // Moving the AbsResouce moves all instances
			&ResourceInstanceObjectSrc{
				Status:        ObjectReady,
				SchemaVersion: 1,
				AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)

		src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
		dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(addrs.RootModuleInstance)
		s.MoveAbsResource(src, dst)

		if s.Empty() {
			t.Fatal("unexpected empty state")
		}

		// The child module should have been removed after removing its only resource
		if s.Module(srcModule) != nil {
			t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
		}

		if len(s.RootModule().Resources) != 2 {
			t.Fatalf("wrong number of resources in state; expected 2, found %d", len(s.RootModule().Resources))
		}

		if len(s.Resource(dst).Instances) != 2 {
			t.Fatalf("wrong number of resource instances for dst, got %d expected 2", len(s.Resource(dst).Instances))
		}

		got := s.Resource(dst)
		if got.Addr.Resource != dst.Resource {
			t.Fatalf("dst resource not in state")
		}
	})

	t.Run("module to new module", func(t *testing.T) {
		s := NewState()
		srcModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("exists"))
		dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("new"))
		cm := s.EnsureModule(srcModule)
		cm.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_thing",
				Name: "child",
			}.Instance(addrs.NoKey),
			&ResourceInstanceObjectSrc{
				Status:        ObjectReady,
				SchemaVersion: 1,
				AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)

		src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
		dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(dstModule)
		s.MoveAbsResource(src, dst)

		if s.Empty() {
			t.Fatal("unexpected empty state")
		}

		// The child module should have been removed after removing its only resource
		if s.Module(srcModule) != nil {
			t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
		}

		gotMod := s.Module(dstModule)
		if len(gotMod.Resources) != 1 {
			t.Fatalf("wrong number of resources in state; expected 1, found %d", len(gotMod.Resources))
		}

		got := s.Resource(dst)
		if got.Addr.Resource != dst.Resource {
			t.Fatalf("dst resource not in state")
		}
	})

	t.Run("module to new module", func(t *testing.T) {
		s := NewState()
		srcModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("exists"))
		dstModule := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("new"))
		cm := s.EnsureModule(srcModule)
		cm.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_thing",
				Name: "child",
			}.Instance(addrs.NoKey),
			&ResourceInstanceObjectSrc{
				Status:        ObjectReady,
				SchemaVersion: 1,
				AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)

		src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(srcModule)
		dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "child"}.Absolute(dstModule)
		s.MoveAbsResource(src, dst)

		if s.Empty() {
			t.Fatal("unexpected empty state")
		}

		// The child module should have been removed after removing its only resource
		if s.Module(srcModule) != nil {
			t.Fatalf("child module %s was not removed from state after mv", srcModule.String())
		}

		gotMod := s.Module(dstModule)
		if len(gotMod.Resources) != 1 {
			t.Fatalf("wrong number of resources in state; expected 1, found %d", len(gotMod.Resources))
		}

		got := s.Resource(dst)
		if got.Addr.Resource != dst.Resource {
			t.Fatalf("dst resource not in state")
		}
	})
}

func TestState_MaybeMoveAbsResource(t *testing.T) {
	state := NewState()
	rootModule := state.RootModule()
	rootModule.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "foo",
		}.Instance(addrs.IntKey(0)),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)

	src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Absolute(addrs.RootModuleInstance)
	dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "bar"}.Absolute(addrs.RootModuleInstance)

	// First move, success
	t.Run("first move", func(t *testing.T) {
		moved := state.MaybeMoveAbsResource(src, dst)
		if !moved {
			t.Fatal("wrong result")
		}
	})

	// Trying to move a resource that doesn't exist in state to a resource which does exist should be a noop.
	t.Run("noop", func(t *testing.T) {
		moved := state.MaybeMoveAbsResource(src, dst)
		if moved {
			t.Fatal("wrong result")
		}
	})
}

func TestState_MoveAbsResourceInstance(t *testing.T) {
	state := NewState()
	rootModule := state.RootModule()
	rootModule.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "foo",
		}.Instance(addrs.NoKey),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)
	// src resource from the state above
	src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	t.Run("resource to resource instance", func(t *testing.T) {
		s := state.DeepCopy()
		// For a little extra fun, move a resource to a resource instance: test_thing.foo to test_thing.foo[1]
		dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance)

		s.MoveAbsResourceInstance(src, dst)

		if s.Empty() {
			t.Fatal("unexpected empty state")
		}

		if len(s.RootModule().Resources) != 1 {
			t.Fatalf("wrong number of resources in state; expected 1, found %d", len(state.RootModule().Resources))
		}

		got := s.ResourceInstance(dst)
		if got == nil {
			t.Fatalf("dst resource not in state")
		}
	})

	t.Run("move to new module", func(t *testing.T) {
		s := state.DeepCopy()
		// test_thing.foo to module.kinder.test_thing.foo["baz"]
		dstModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
		dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(dstModule)

		s.MoveAbsResourceInstance(src, dst)

		if s.Empty() {
			t.Fatal("unexpected empty state")
		}

		if s.Module(dstModule) == nil {
			t.Fatalf("child module %s not in state", dstModule.String())
		}

		if len(s.Module(dstModule).Resources) != 1 {
			t.Fatalf("wrong number of resources in state; expected 1, found %d", len(s.Module(dstModule).Resources))
		}

		got := s.ResourceInstance(dst)
		if got == nil {
			t.Fatalf("dst resource not in state")
		}
	})
}

func TestState_MaybeMoveAbsResourceInstance(t *testing.T) {
	state := NewState()
	rootModule := state.RootModule()
	rootModule.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "foo",
		}.Instance(addrs.NoKey),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)

	// For a little extra fun, let's go from a resource to a resource instance: test_thing.foo to test_thing.bar[1]
	src := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
	dst := addrs.Resource{Mode: addrs.ManagedResourceMode, Type: "test_thing", Name: "foo"}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance)

	// First move, success
	t.Run("first move", func(t *testing.T) {
		moved := state.MaybeMoveAbsResourceInstance(src, dst)
		if !moved {
			t.Fatal("wrong result")
		}
		got := state.ResourceInstance(dst)
		if got == nil {
			t.Fatal("destination resource instance not in state")
		}
	})

	// Moving a resource instance that doesn't exist in state to a resource which does exist should be a noop.
	t.Run("noop", func(t *testing.T) {
		moved := state.MaybeMoveAbsResourceInstance(src, dst)
		if moved {
			t.Fatal("wrong result")
		}
	})
}

func TestState_MoveModuleInstance(t *testing.T) {
	state := NewState()
	srcModule := addrs.RootModuleInstance.Child("kinder", addrs.NoKey)
	m := state.EnsureModule(srcModule)
	m.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "foo",
		}.Instance(addrs.NoKey),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)

	dstModule := addrs.RootModuleInstance.Child("child", addrs.IntKey(3))
	state.MoveModuleInstance(srcModule, dstModule)

	// srcModule should have been removed, dstModule should exist and have one resource
	if len(state.Modules) != 2 { // kinder[3] and root
		t.Fatalf("wrong number of modules in state. Expected 2, got %d", len(state.Modules))
	}

	got := state.Module(dstModule)
	if got == nil {
		t.Fatal("dstModule not found")
	}

	gone := state.Module(srcModule)
	if gone != nil {
		t.Fatal("srcModule not removed from state")
	}

	r := got.Resource(mustAbsResourceAddr("test_thing.foo").Resource)
	if r.Addr.Module.String() != dstModule.String() {
		fmt.Println(r.Addr.Module.String())
		t.Fatal("resource address was not updated")
	}

}

func TestState_MaybeMoveModuleInstance(t *testing.T) {
	state := NewState()
	src := addrs.RootModuleInstance.Child("child", addrs.StringKey("a"))
	cm := state.EnsureModule(src)
	cm.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "foo",
		}.Instance(addrs.NoKey),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)

	dst := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("b"))

	// First move, success
	t.Run("first move", func(t *testing.T) {
		moved := state.MaybeMoveModuleInstance(src, dst)
		if !moved {
			t.Fatal("wrong result")
		}
	})

	// Second move, should be a noop
	t.Run("noop", func(t *testing.T) {
		moved := state.MaybeMoveModuleInstance(src, dst)
		if moved {
			t.Fatal("wrong result")
		}
	})
}

func TestState_MoveModule(t *testing.T) {
	// For this test, add two module instances (kinder and kinder["a"]).
	// MoveModule(kinder) should move both instances.
	state := NewState() // starter state, should be copied by the subtests.
	srcModule := addrs.RootModule.Child("kinder")
	m := state.EnsureModule(srcModule.UnkeyedInstanceShim())
	m.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "foo",
		}.Instance(addrs.NoKey),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)

	moduleInstance := addrs.RootModuleInstance.Child("kinder", addrs.StringKey("a"))
	mi := state.EnsureModule(moduleInstance)
	mi.SetResourceInstanceCurrent(
		addrs.Resource{
			Mode: addrs.ManagedResourceMode,
			Type: "test_thing",
			Name: "foo",
		}.Instance(addrs.NoKey),
		&ResourceInstanceObjectSrc{
			Status:        ObjectReady,
			SchemaVersion: 1,
			AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
		},
		addrs.AbsProviderConfig{
			Provider: addrs.NewDefaultProvider("test"),
			Module:   addrs.RootModule,
		},
	)

	_, mc := srcModule.Call()
	src := mc.Absolute(addrs.RootModuleInstance.Child("kinder", addrs.NoKey))

	t.Run("basic", func(t *testing.T) {
		s := state.DeepCopy()
		_, dstMC := addrs.RootModule.Child("child").Call()
		dst := dstMC.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey))
		s.MoveModule(src, dst)

		// srcModule should have been removed, dstModule should exist and have one resource
		if len(s.Modules) != 3 { // child, child["a"] and root
			t.Fatalf("wrong number of modules in state. Expected 3, got %d", len(s.Modules))
		}

		got := s.Module(dst.Module)
		if got == nil {
			t.Fatal("dstModule not found")
		}

		got = s.Module(addrs.RootModuleInstance.Child("child", addrs.StringKey("a")))
		if got == nil {
			t.Fatal("dstModule instance \"a\" not found")
		}

		gone := s.Module(srcModule.UnkeyedInstanceShim())
		if gone != nil {
			t.Fatal("srcModule not removed from state")
		}
	})

	t.Run("nested modules", func(t *testing.T) {
		s := state.DeepCopy()

		// add a child module to module.kinder
		mi := mustParseModuleInstanceStr(`module.kinder.module.grand[1]`)
		m := s.EnsureModule(mi)
		m.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_thing",
				Name: "foo",
			}.Instance(addrs.NoKey),
			&ResourceInstanceObjectSrc{
				Status:        ObjectReady,
				SchemaVersion: 1,
				AttrsJSON:     []byte(`{"woozles":"confuzles"}`),
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)

		_, dstMC := addrs.RootModule.Child("child").Call()
		dst := dstMC.Absolute(addrs.RootModuleInstance.Child("child", addrs.NoKey))
		s.MoveModule(src, dst)

		moved := s.Module(addrs.RootModuleInstance.Child("child", addrs.StringKey("a")))
		if moved == nil {
			t.Fatal("dstModule not found")
		}

		// The nested module's relative address should also have been updated
		nested := s.Module(mustParseModuleInstanceStr(`module.child.module.grand[1]`))
		if nested == nil {
			t.Fatal("nested child module of src wasn't moved")
		}
	})
}

func mustParseModuleInstanceStr(str string) addrs.ModuleInstance {
	addr, diags := addrs.ParseModuleInstanceStr(str)
	if diags.HasErrors() {
		panic(diags.Err())
	}
	return addr
}

func mustAbsResourceAddr(s string) addrs.AbsResource {
	addr, diags := addrs.ParseAbsResourceStr(s)
	if diags.HasErrors() {
		panic(diags.Err())
	}
	return addr
}
