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

package stackmigrate

import (
	"encoding/json"
	"fmt"
	"path/filepath"
	"sort"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/google/go-cmp/cmp/cmpopts"
	"github.com/hashicorp/go-slug/sourceaddrs"
	"github.com/hashicorp/go-slug/sourcebundle"
	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty-debug/ctydebug"
	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/collections"
	"github.com/hashicorp/terraform/internal/depsfile"
	"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
	"github.com/hashicorp/terraform/internal/stacks/stackconfig"
	stacks_testing_provider "github.com/hashicorp/terraform/internal/stacks/stackruntime/testing"
	"github.com/hashicorp/terraform/internal/stacks/stackstate"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/tfdiags"
)

func TestMigrate(t *testing.T) {
	deposedKey := states.NewDeposedKey()

	tcs := map[string]struct {
		path          string
		state         func(ss *states.SyncState)
		resources     map[string]string
		modules       map[string]string
		expected      []stackstate.AppliedChange
		expectedDiags tfdiags.Diagnostics
	}{
		"module": {
			path: filepath.Join("with-single-input", "valid"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.AbsResourceInstance{
						Module: addrs.ModuleInstance{
							{
								Name: "child",
							},
						},
						Resource: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.ManagedResourceMode,
								Type: "testing_resource",
								Name: "data",
							},
							Key: addrs.NoKey,
						},
					},
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceDeposed(
					addrs.AbsResourceInstance{
						Module: addrs.ModuleInstance{
							{
								Name: "child",
							},
						},
						Resource: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.ManagedResourceMode,
								Type: "testing_resource",
								Name: "data",
							},
							Key: addrs.NoKey,
						},
					},
					deposedKey,
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			modules: map[string]string{
				"child": "self",
			},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.self"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
					OutputValues:          map[addrs.OutputValue]cty.Value{},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
						Component: mustAbsResourceInstanceObject("component.self.testing_resource.data").Component,
						Item: addrs.AbsResourceInstanceObject{
							ResourceInstance: mustAbsResourceInstanceObject("component.self.testing_resource.data").Item.ResourceInstance,
							DeposedKey:       deposedKey,
						},
					},
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
		},
		"root resources": {
			path: filepath.Join("with-single-input", "valid"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceDeposed(
					addrs.AbsResourceInstance{
						Module: addrs.RootModuleInstance,
						Resource: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.ManagedResourceMode,
								Type: "testing_resource",
								Name: "data",
							},
							Key: addrs.NoKey,
						},
					},
					deposedKey,
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.AbsResourceInstance{
						Module: addrs.RootModuleInstance,
						Resource: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.ManagedResourceMode,
								Type: "testing_resource",
								Name: "data",
							},
							Key: addrs.NoKey,
						},
					},
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				"testing_resource.data": "component.self",
			},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.self"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
					OutputValues:          map[addrs.OutputValue]cty.Value{},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
						Component: mustAbsResourceInstanceObject("component.self.testing_resource.data").Component,
						Item: addrs.AbsResourceInstanceObject{
							ResourceInstance: mustAbsResourceInstanceObject("component.self.testing_resource.data").Item.ResourceInstance,
							DeposedKey:       deposedKey,
						},
					},
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
		},
		"component_dependency": {
			path: filepath.Join("for-stacks-migrate", "with-dependency", "input-dependency"),
			state: func(ss *states.SyncState) {
				ss.SetOutputValue(addrs.AbsOutputValue{
					Module:      addrs.RootModuleInstance,
					OutputValue: addrs.OutputValue{Name: "output"},
				}, cty.StringVal("before"), false)
				ss.SetResourceInstanceCurrent(
					addrs.AbsResourceInstance{
						Module: addrs.RootModuleInstance,
						Resource: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.ManagedResourceMode,
								Type: "testing_resource",
								Name: "data",
							},
							Key: addrs.NoKey,
						},
					},
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.AbsResourceInstance{
						Module: addrs.RootModuleInstance,
						Resource: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.ManagedResourceMode,
								Type: "testing_resource",
								Name: "another",
							},
							Key: addrs.IntKey(0),
						},
					},
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.AbsResourceInstance{
						Module: addrs.RootModuleInstance,
						Resource: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.ManagedResourceMode,
								Type: "testing_resource",
								Name: "another",
							},
							Key: addrs.IntKey(1),
						},
					},
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				"testing_resource.data":       "component.parent",
				"testing_resource.another[0]": "component.child",
				"testing_resource.another[1]": "component.child",
			},
			modules: map[string]string{},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.child"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.child"),
					OutputValues: map[addrs.OutputValue]cty.Value{
						{Name: "id"}: cty.DynamicVal,
					},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
					Dependencies: collections.NewSet(mustAbsComponent("component.parent")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another[0]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another[1]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.parent"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
					OutputValues: map[addrs.OutputValue]cty.Value{
						{Name: "id"}: cty.DynamicVal,
					},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
					Dependents: collections.NewSet(mustAbsComponent("component.child")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
		},
		"nested module resources": {
			path: filepath.Join("for-stacks-migrate", "with-nested-module"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "another",
					}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "another",
					}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				for _, child := range []string{"child_mod", "child_mod2"} {
					ss.SetResourceInstanceCurrent(
						addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "child_data",
						}.Instance(addrs.NoKey).Absolute(addrs.ModuleInstance{
							{
								Name: child,
							},
						}),
						&states.ResourceInstanceObjectSrc{
							Status: states.ObjectReady,
							AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
								"id":    "foo",
								"value": "hello",
							}),
						},
						mustDefaultRootProvider("testing"),
					)
					ss.SetResourceInstanceCurrent(
						addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "another_child_data",
						}.Instance(addrs.IntKey(0)).Absolute(addrs.ModuleInstance{
							{
								Name: child,
							},
						}),
						&states.ResourceInstanceObjectSrc{
							Status: states.ObjectReady,
							AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
								"id":    "foo",
								"value": "hello",
							}),
						},
						mustDefaultRootProvider("testing"),
					)
					ss.SetResourceInstanceCurrent(
						addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "another_child_data",
						}.Instance(addrs.IntKey(1)).Absolute(addrs.ModuleInstance{
							{
								Name: child,
							},
						}),
						&states.ResourceInstanceObjectSrc{
							Status: states.ObjectReady,
							AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
								"id":    "foo",
								"value": "hello",
							}),
						},
						mustDefaultRootProvider("testing"),
					)
				}
			},

			resources: map[string]string{
				"testing_resource.data":       "component.parent",
				"testing_resource.another[0]": "component.parent",
				"testing_resource.another[1]": "component.parent",
			},
			modules: map[string]string{
				"child_mod":  "child",
				"child_mod2": "child2",
			},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.child"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.child"),
					OutputValues: map[addrs.OutputValue]cty.Value{
						{Name: "id"}: cty.DynamicVal,
					},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
					Dependencies: collections.NewSet(mustAbsComponent("component.parent")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another_child_data[0]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.another_child_data[1]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child.testing_resource.child_data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.child2"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.child2"),
					OutputValues: map[addrs.OutputValue]cty.Value{
						{Name: "id"}: cty.DynamicVal,
					},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
					Dependencies: collections.NewSet(mustAbsComponent("component.parent")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child2.testing_resource.another_child_data[0]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child2.testing_resource.another_child_data[1]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.child2.testing_resource.child_data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.parent"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
					OutputValues: map[addrs.OutputValue]cty.Value{
						{Name: "id"}: cty.DynamicVal,
					},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
					Dependents: collections.NewSet(mustAbsComponent("component.child"), mustAbsComponent("component.child2")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
		},
		"deeply nested module resources with implicit provider configs": {
			path: filepath.Join("for-stacks-migrate", "with-deeply-nested-module"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "another",
					}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "another",
					}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				"testing_resource.data":       "component.parent.module.child_mod.module.grand_child_mod.testing_resource.grand_child_data",
				"testing_resource.another[0]": "component.parent.module.child_mod.module.grand_child_mod.testing_resource.another_grand_child_data[0]",
				"testing_resource.another[1]": "component.parent.module.child_mod.module.grand_child_mod.testing_resource.another_grand_child_data[1]",
			},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.parent"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
					OutputValues:          map[addrs.OutputValue]cty.Value{},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "input"}: cty.DynamicVal,
					},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.module.child_mod.module.grand_child_mod.testing_resource.another_grand_child_data[0]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.module.child_mod.module.grand_child_mod.testing_resource.another_grand_child_data[1]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.module.child_mod.module.grand_child_mod.testing_resource.grand_child_data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
		},
		"missing config resource": {
			path: filepath.Join("for-stacks-migrate", "with-nested-module"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "another",
					}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "another",
					}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "for_child",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				"testing_resource.data":       "component.parent",
				"testing_resource.another[0]": "component.parent",
				"testing_resource.another[1]": "component.parent",
				"testing_resource.for_child":  "component.child",
			},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.child"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.child"),
					OutputValues: map[addrs.OutputValue]cty.Value{
						{Name: "id"}: cty.DynamicVal,
					},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
					Dependencies: collections.NewSet(mustAbsComponent("component.parent")),
				},
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.parent"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
					OutputValues: map[addrs.OutputValue]cty.Value{
						{Name: "id"}: cty.DynamicVal,
					},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
					Dependents: collections.NewSet(mustAbsComponent("component.child")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
			expectedDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Resource mapped to non-existent target",
				Detail:   "Could not migrate resource \"testing_resource.for_child\". Target resource \"testing_resource.for_child\" not found in component \"component.child\".",
			}),
		},

		"missing mapping for state resource": {
			path: filepath.Join("for-stacks-migrate", "with-nested-module"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "another",
					}.Instance(addrs.IntKey(0)).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "another",
					}.Instance(addrs.IntKey(1)).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "for_child",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				"testing_resource.data":       "component.parent",
				"testing_resource.another[0]": "component.parent",
				"testing_resource.another[1]": "component.parent",
			},
			modules: map[string]string{},
			expected: []stackstate.AppliedChange{
				// this component has a dependent "child", but that other component
				// is not present in the modules mapping, so it is not included here
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.parent"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.parent"),
					OutputValues: map[addrs.OutputValue]cty.Value{
						{Name: "id"}: cty.DynamicVal,
					},
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "id"}:    cty.DynamicVal,
						{Name: "input"}: cty.DynamicVal,
					},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[0]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.another[1]"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.parent.testing_resource.data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "hello",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
			expectedDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Resource not found",
				Detail:   "Resource \"testing_resource.for_child\" exists in state, but was not included in any provided mapping.",
			}),
		},
		"config depends on": {
			path: filepath.Join("for-stacks-migrate", "with-depends-on"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "second",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "third",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				"testing_resource.data":   "component.first",
				"testing_resource.second": "component.second",
				"testing_resource.third":  "component.second",
			},
			modules: map[string]string{},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.first"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.first"),
					OutputValues:          make(map[addrs.OutputValue]cty.Value),
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "input"}: cty.DynamicVal,
						{Name: "id"}:    cty.DynamicVal,
					},
					Dependents: collections.NewSet(mustAbsComponent("component.second")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.second"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.second"),
					OutputValues:          make(map[addrs.OutputValue]cty.Value),
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "input"}: cty.DynamicVal,
						{Name: "id"}:    cty.DynamicVal,
					},
					Dependencies: collections.NewSet(mustAbsComponent("component.first")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.second"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.third"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
			expectedDiags: tfdiags.Diagnostics{}.Append(),
		},
		"unsupported component ref": {
			path: filepath.Join("for-stacks-migrate", "with-depends-on"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "second",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "third",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				"testing_resource.data":   "component.first",
				"testing_resource.second": "component.second",
				"testing_resource.third":  "stack.embedded.component.self",
			},
			modules: map[string]string{},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.first"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.first"),
					OutputValues:          make(map[addrs.OutputValue]cty.Value),
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "input"}: cty.DynamicVal,
						{Name: "id"}:    cty.DynamicVal,
					},
					Dependents: collections.NewSet(mustAbsComponent("component.second")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.second"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.second"),
					OutputValues:          make(map[addrs.OutputValue]cty.Value),
					InputVariables: map[addrs.InputVariable]cty.Value{
						{Name: "input"}: cty.DynamicVal,
						{Name: "id"}:    cty.DynamicVal,
					},
					Dependencies: collections.NewSet(mustAbsComponent("component.first")),
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.second"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "foo",
							"value": "depends_test",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
			expectedDiags: tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "Invalid component instance",
				Detail:   "Only root component instances are allowed, got \"stack.embedded.component.self\"",
			}),
		},
		"child module as component source": {
			path: filepath.Join("for-stacks-migrate", "child-module-as-component-source"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "root_id",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "root_id",
							"value": "root_output",
						}),
					},
					mustDefaultRootProvider("testing"),
				)

				childProv := mustDefaultRootProvider("testing")
				childProv.Module = addrs.Module{"child_module"}
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "child_data",
					}.Instance(addrs.NoKey).Absolute(addrs.ModuleInstance{
						{
							Name: "child_module",
						},
					}),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "child_data",
							"value": "child_output",
						}),
					},
					childProv,
				)
			},
			resources: map[string]string{
				"testing_resource.root_id":    "component.self",
				"testing_resource.child_data": "component.self", // this should just be ignored
			},
			modules: map[string]string{
				"child_module": "triage",
			},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.self"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
					OutputValues:          map[addrs.OutputValue]cty.Value{},
					InputVariables:        map[addrs.InputVariable]cty.Value{},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.root_id"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "root_id",
							"value": "root_output",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.triage"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.triage"),
					OutputValues:          map[addrs.OutputValue]cty.Value{},
					InputVariables: map[addrs.InputVariable]cty.Value{
						addrs.InputVariable{Name: "input"}: cty.DynamicVal,
					},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.triage.testing_resource.child_data"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "child_data",
							"value": "child_output",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
		},
		"unclaimed resources fall into modules": {
			path: filepath.Join("for-stacks-migrate", "multiple-components"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "one",
					}.Instance(addrs.NoKey).Absolute(addrs.ModuleInstance{
						{
							Name: "self",
						},
					}),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "one",
							"value": "one",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "resource",
					}.Instance(addrs.NoKey).Absolute(addrs.ModuleInstance{
						{
							Name: "self",
						},
					}),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "two",
							"value": "two",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				// this specific resource goes to component.one
				"module.self.testing_resource.one": "component.one.testing_resource.resource",
			},
			modules: map[string]string{
				"self": "two", // all other resources go to component.two
			},
			expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.one"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.one"),
					OutputValues:          map[addrs.OutputValue]cty.Value{},
					InputVariables:        map[addrs.InputVariable]cty.Value{},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.one.testing_resource.resource"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "one",
							"value": "one",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.two"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.two"),
					OutputValues:          map[addrs.OutputValue]cty.Value{},
					InputVariables:        map[addrs.InputVariable]cty.Value{},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.two.testing_resource.resource"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "two",
							"value": "two",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
		},
		"single component": {
			path: filepath.Join("for-stacks-migrate", "single-component"),
			state: func(ss *states.SyncState) {
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "one",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "one",
							"value": "one",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "two",
					}.Instance(addrs.NoKey).Absolute(addrs.ModuleInstance{
						{
							Name: "two",
						},
					}),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "two",
							"value": "two",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
				ss.SetResourceInstanceCurrent(
					addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "three",
					}.Instance(addrs.NoKey).Absolute(addrs.ModuleInstance{
						{
							Name: "three",
						},
					}),
					&states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "three",
							"value": "three",
						}),
					},
					mustDefaultRootProvider("testing"),
				)
			},
			resources: map[string]string{
				"testing_resource.one": "component.single.testing_resource.one",
			},
			modules: map[string]string{
				"two":   "single",
				"three": "single",
			}, expected: []stackstate.AppliedChange{
				&stackstate.AppliedChangeComponentInstance{
					ComponentAddr:         mustAbsComponent("component.single"),
					ComponentInstanceAddr: mustAbsComponentInstance("component.single"),
					OutputValues:          map[addrs.OutputValue]cty.Value{},
					InputVariables:        map[addrs.InputVariable]cty.Value{},
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.one"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "one",
							"value": "one",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.three"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "three",
							"value": "three",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackstate.AppliedChangeResourceInstanceObject{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.single.testing_resource.two"),
					NewStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "two",
							"value": "two",
						}),
						Status:  states.ObjectReady,
						Private: nil,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
			},
		},
	}
	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			cfg := loadMainBundleConfigForTest(t, tc.path)

			lock := depsfile.NewLocks()
			lock.SetProvider(
				addrs.NewDefaultProvider("testing"),
				providerreqs.MustParseVersion("0.0.0"),
				providerreqs.MustParseVersionConstraints("=0.0.0"),
				providerreqs.PreferredHashes([]providerreqs.Hash{}),
			)

			state := states.BuildState(tc.state)

			migration := Migration{
				Providers: map[addrs.Provider]providers.Factory{
					addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProvider(t), nil
					},
				},
				PreviousState: state,
				Config:        cfg,
			}

			var applied []stackstate.AppliedChange
			var gotDiags tfdiags.Diagnostics

			migration.Migrate(tc.resources, tc.modules, func(change stackstate.AppliedChange) {
				applied = append(applied, change)
			}, func(diagnostic tfdiags.Diagnostic) {
				gotDiags = append(gotDiags, diagnostic)
			})

			sort.SliceStable(applied, func(i, j int) bool {
				key := func(change stackstate.AppliedChange) string {
					switch change := change.(type) {
					case *stackstate.AppliedChangeComponentInstance:
						return change.ComponentInstanceAddr.String()
					case *stackstate.AppliedChangeResourceInstanceObject:
						return change.ResourceInstanceObjectAddr.String()
					default:
						panic("unsupported change type")
					}
				}

				return key(applied[i]) < key(applied[j])
			})

			if diff := cmp.Diff(tc.expected, applied, cmp.Options{
				ctydebug.CmpOptions,
				collections.CmpOptions,
				cmpopts.IgnoreUnexported(addrs.InputVariable{}),
				cmpopts.IgnoreUnexported(states.ResourceInstanceObjectSrc{}),
			}); len(diff) > 0 {
				t.Errorf("unexpected applied changes:\n%s", diff)
			}

			tfdiags.AssertDiagnosticsMatch(t, gotDiags, tc.expectedDiags)
		})
	}
}

func mustMarshalJSONAttrs(attrs map[string]interface{}) []byte {
	jsonAttrs, err := json.Marshal(attrs)
	if err != nil {
		panic(err)
	}
	return jsonAttrs
}

func mustDefaultRootProvider(provider string) addrs.AbsProviderConfig {
	return addrs.AbsProviderConfig{
		Module:   addrs.RootModule,
		Provider: addrs.NewDefaultProvider(provider),
	}
}

func mustAbsResourceInstanceObject(addr string) stackaddrs.AbsResourceInstanceObject {
	ret, diags := stackaddrs.ParseAbsResourceInstanceObjectStr(addr)
	if len(diags) > 0 {
		panic(fmt.Sprintf("failed to parse resource instance object address %q: %s", addr, diags))
	}
	return ret
}

func mustAbsComponentInstance(addr string) stackaddrs.AbsComponentInstance {
	ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr)
	if len(diags) > 0 {
		panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags))
	}
	return ret
}

func mustAbsComponent(addr string) stackaddrs.AbsComponent {
	ret, diags := stackaddrs.ParsePartialComponentInstanceStr(addr)
	if len(diags) > 0 {
		panic(fmt.Sprintf("failed to parse component instance address %q: %s", addr, diags))
	}
	return stackaddrs.AbsComponent{
		Stack: ret.Stack,
		Item:  ret.Item.Component,
	}
}

// TODO: Perhaps export this from helper_test instead
func loadMainBundleConfigForTest(t *testing.T, dirName string) *stackconfig.Config {
	t.Helper()
	fullSourceAddr := mainBundleSourceAddrStr(dirName)
	return loadConfigForTest(t, "../stackruntime/testdata/mainbundle", fullSourceAddr)
}

func mainBundleSourceAddrStr(dirName string) string {
	return "git::https://example.com/test.git//" + dirName
}

// loadConfigForTest is a test helper that tries to open bundleRoot as a
// source bundle, and then if successful tries to load the given source address
// from it as a stack configuration. If any part of the operation fails then
// it halts execution of the test and doesn't return.
func loadConfigForTest(t *testing.T, bundleRoot string, configSourceAddr string) *stackconfig.Config {
	t.Helper()
	sources, err := sourcebundle.OpenDir(bundleRoot)
	if err != nil {
		t.Fatalf("cannot load source bundle: %s", err)
	}

	// We force using remote source addresses here because that avoids
	// us having to deal with the extra version constraints argument
	// that registry sources require. Exactly what source address type
	// we use isn't relevant for tests in this package, since it's
	// the sourcebundle package's responsibility to make sure its
	// abstraction works for all of the source types.
	sourceAddr, err := sourceaddrs.ParseRemoteSource(configSourceAddr)
	if err != nil {
		t.Fatalf("invalid config source address: %s", err)
	}

	cfg, diags := stackconfig.LoadConfigDir(sourceAddr, sources)
	reportDiagnosticsForTest(t, diags)
	return cfg
}

// reportDiagnosticsForTest creates a test log entry for every diagnostic in
// the given diags, and halts the test if any of them are error diagnostics.
func reportDiagnosticsForTest(t *testing.T, diags tfdiags.Diagnostics) {
	t.Helper()
	for _, diag := range diags {
		var b strings.Builder
		desc := diag.Description()
		locs := diag.Source()

		switch sev := diag.Severity(); sev {
		case tfdiags.Error:
			b.WriteString("Error: ")
		case tfdiags.Warning:
			b.WriteString("Warning: ")
		default:
			t.Errorf("unsupported diagnostic type %s", sev)
		}
		b.WriteString(desc.Summary)
		if desc.Address != "" {
			b.WriteString("\nwith ")
			b.WriteString(desc.Summary)
		}
		if locs.Subject != nil {
			b.WriteString("\nat ")
			b.WriteString(locs.Subject.StartString())
		}
		if desc.Detail != "" {
			b.WriteString("\n\n")
			b.WriteString(desc.Detail)
		}
		t.Log(b.String())
	}
	if diags.HasErrors() {
		t.FailNow()
	}
}
