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

package stackruntime

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

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty-debug/ctydebug"
	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/msgpack"

	"github.com/hashicorp/terraform/internal/checks"
	"github.com/hashicorp/terraform/internal/depsfile"
	"github.com/hashicorp/terraform/internal/getproviders/providerreqs"
	"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"

	"github.com/hashicorp/terraform/internal/addrs"

	"github.com/hashicorp/terraform/internal/builtin/providers/terraform"
	terraformProvider "github.com/hashicorp/terraform/internal/builtin/providers/terraform"
	"github.com/hashicorp/terraform/internal/collections"
	"github.com/hashicorp/terraform/internal/configs"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/lang/marks"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/providers"
	default_testing_provider "github.com/hashicorp/terraform/internal/providers/testing"
	"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
	"github.com/hashicorp/terraform/internal/stacks/stackplan"
	"github.com/hashicorp/terraform/internal/stacks/stackruntime/internal/stackeval"
	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"
	"github.com/hashicorp/terraform/version"
)

// TestPlan_valid runs the same set of configurations as TestValidate_valid.
//
// Plan should execute the same set of validations as validate, so we expect
// all of the following to be valid for both plan and validate.
//
// We also want to make sure the static and dynamic evaluations are not
// returning duplicate / conflicting diagnostics. This test will tell us if
// either plan or validate is reporting diagnostics the others are missing.
func TestPlan_valid(t *testing.T) {
	for name, tc := range validConfigurations {
		t.Run(name, func(t *testing.T) {
			if tc.skip {
				// We've added this test before the implementation was ready.
				t.SkipNow()
			}
			ctx := context.Background()

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

			fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
			if err != nil {
				t.Fatal(err)
			}

			testContext := TestContext{
				config: loadMainBundleConfigForTest(t, name),
				providers: map[addrs.Provider]providers.Factory{
					// We support both hashicorp/testing and
					// terraform.io/builtin/testing as providers. This lets us
					// test the provider aliasing feature. Both providers
					// support the same set of resources and data sources.
					addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProvider(t), nil
					},
					addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProvider(t), nil
					},
					// We also support an "other" provider out of the box to
					// test the provider aliasing feature.
					addrs.NewDefaultProvider("other"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProvider(t), nil
					},
				},
				dependencyLocks: *lock,
				timestamp:       &fakePlanTimestamp,
			}

			cycle := TestCycle{
				planInputs:         tc.planInputVars,
				wantPlannedChanges: nil, // don't care about the planned changes in this test.
				wantPlannedDiags:   nil, // should return no diagnostics.
			}
			testContext.Plan(t, ctx, nil, cycle)
		})
	}
}

// TestPlan_invalid runs the same set of configurations as TestValidate_invalid.
//
// Plan should execute the same set of validations as validate, so we expect
// all of the following to be invalid for both plan and validate.
//
// We also want to make sure the static and dynamic evaluations are not
// returning duplicate / conflicting diagnostics. This test will tell us if
// either plan or validate is reporting diagnostics the others are missing.
//
// The dynamic validation that happens during the plan *might* introduce
// additional diagnostics that are not present in the static validation. These
// should be added manually into this function.
func TestPlan_invalid(t *testing.T) {
	for name, tc := range invalidConfigurations {
		t.Run(name, func(t *testing.T) {
			if tc.skip {
				// We've added this test before the implementation was ready.
				t.SkipNow()
			}
			ctx := context.Background()

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

			fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
			if err != nil {
				t.Fatal(err)
			}

			testContext := TestContext{
				config: loadMainBundleConfigForTest(t, name),
				providers: map[addrs.Provider]providers.Factory{
					// We support both hashicorp/testing and
					// terraform.io/builtin/testing as providers. This lets us
					// test the provider aliasing feature. Both providers
					// support the same set of resources and data sources.
					addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProvider(t), nil
					},
					addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProvider(t), nil
					},
				},
				dependencyLocks: *lock,
				timestamp:       &fakePlanTimestamp,
			}

			cycle := TestCycle{
				planInputs:         tc.planInputVars,
				wantPlannedChanges: nil, // don't care about the planned changes in this test.
				wantPlannedDiags:   tc.diags(),
			}
			testContext.Plan(t, ctx, nil, cycle)
		})
	}
}

// TestPlan uses a generic framework for running plan integration tests
// against Stacks. Generally, new tests should be added into this function
// rather than copying the large amount of duplicate code from the other
// tests in this file.
//
// If you are editing other tests in this file, please consider moving them
// into this test function so they can reuse the shared setup and boilerplate
// code managing the boring parts of the test.
func TestPlan(t *testing.T) {
	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	tcs := map[string]struct {
		path  string
		state *stackstate.State
		store *stacks_testing_provider.ResourceStore
		cycle TestCycle
	}{
		"empty-destroy-with-data-source": {
			path: path.Join("with-data-source", "dependent"),
			cycle: TestCycle{
				planMode: plans.DestroyMode,
				planInputs: map[string]cty.Value{
					"id": cty.StringVal("foo"),
				},
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr:                mustAbsComponentInstance("component.data"),
						PlanApplyable:       true,
						PlanComplete:        true,
						Action:              plans.Delete,
						Mode:                plans.DestroyMode,
						RequiredComponents:  collections.NewSet(mustAbsComponent("component.self")),
						PlannedOutputValues: make(map[string]cty.Value),
						PlanTimestamp:       fakePlanTimestamp,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr:          mustAbsComponentInstance("component.self"),
						PlanComplete:  true,
						PlanApplyable: true,
						Action:        plans.Delete,
						Mode:          plans.DestroyMode,
						PlannedOutputValues: map[string]cty.Value{
							"id": cty.StringVal("foo"),
						},
						PlanTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:          mustStackInputVariable("id"),
						Action:        plans.Create,
						Before:        cty.NullVal(cty.DynamicPseudoType),
						After:         cty.StringVal("foo"),
						DeleteOnApply: true,
					},
				},
			},
		},
		"deferred-provider-with-write-only": {
			path: "with-write-only-attribute",
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"providers": cty.UnknownVal(cty.Set(cty.String)),
				},
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr:   mustAbsComponentInstance("component.main"),
						Action: plans.Create,
						PlannedInputValues: map[string]plans.DynamicValue{
							"datasource_id":    mustPlanDynamicValueDynamicType(cty.StringVal("datasource")),
							"resource_id":      mustPlanDynamicValueDynamicType(cty.StringVal("resource")),
							"write_only_input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")),
						},
						PlannedInputValueMarks: map[string][]cty.PathValueMarks{
							"datasource_id":    nil,
							"resource_id":      nil,
							"write_only_input": nil,
						},
						PlannedOutputValues: make(map[string]cty.Value),
						PlannedCheckResults: &states.CheckResults{},
						PlanTimestamp:       fakePlanTimestamp,
					},
					&stackplan.PlannedChangeDeferredResourceInstancePlanned{
						ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
							ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.main.data.testing_write_only_data_source.data"),
							ChangeSrc: &plans.ResourceInstanceChangeSrc{
								Addr:         mustAbsResourceInstance("data.testing_write_only_data_source.data"),
								PrevRunAddr:  mustAbsResourceInstance("data.testing_write_only_data_source.data"),
								ProviderAddr: mustDefaultRootProvider("testing"),
								ChangeSrc: plans.ChangeSrc{
									Action: plans.Read,
									Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
									After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
										"id":         cty.StringVal("datasource"),
										"value":      cty.UnknownVal(cty.String),
										"write_only": cty.NullVal(cty.String),
									})),
									AfterSensitivePaths: []cty.Path{
										cty.GetAttrPath("write_only"),
									},
								},
								ActionReason: plans.ResourceInstanceReadBecauseDependencyPending,
							},
							ProviderConfigAddr: mustDefaultRootProvider("testing"),
							Schema:             stacks_testing_provider.WriteOnlyDataSourceSchema,
						},
						DeferredReason: providers.DeferredReasonProviderConfigUnknown,
					},
					&stackplan.PlannedChangeDeferredResourceInstancePlanned{
						ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
							ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.main.testing_write_only_resource.data"),
							ChangeSrc: &plans.ResourceInstanceChangeSrc{
								Addr:         mustAbsResourceInstance("testing_write_only_resource.data"),
								PrevRunAddr:  mustAbsResourceInstance("testing_write_only_resource.data"),
								ProviderAddr: mustDefaultRootProvider("testing"),
								ChangeSrc: plans.ChangeSrc{
									Action: plans.Create,
									Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
									After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
										"id":         cty.StringVal("resource"),
										"value":      cty.UnknownVal(cty.String),
										"write_only": cty.NullVal(cty.String),
									})),
									AfterSensitivePaths: []cty.Path{
										cty.GetAttrPath("write_only"),
									},
								},
							},
							PriorStateSrc:      nil,
							ProviderConfigAddr: mustDefaultRootProvider("testing"),
							Schema:             stacks_testing_provider.WriteOnlyResourceSchema,
						},
						DeferredReason: providers.DeferredReasonProviderConfigUnknown,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   mustStackInputVariable("providers"),
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.UnknownVal(cty.Set(cty.String)),
					},
				},
			},
		},
		"deferred-provider-with-data-sources": {
			path: path.Join("with-data-source", "deferred-provider-for-each"),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("data_known", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("data_known"),
					"value": cty.StringVal("known"),
				})).
				Build(),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"providers": cty.UnknownVal(cty.Set(cty.String)),
				},
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr:          mustAbsComponentInstance("component.const"),
						PlanApplyable: true,
						PlanComplete:  true,
						Action:        plans.Create,
						PlannedInputValues: map[string]plans.DynamicValue{
							"id":       mustPlanDynamicValueDynamicType(cty.StringVal("data_known")),
							"resource": mustPlanDynamicValueDynamicType(cty.StringVal("resource_known")),
						},
						PlannedInputValueMarks: map[string][]cty.PathValueMarks{
							"id":       nil,
							"resource": nil,
						},
						PlannedOutputValues: make(map[string]cty.Value),
						PlannedCheckResults: &states.CheckResults{},
						PlanTimestamp:       fakePlanTimestamp,
					},
					&stackplan.PlannedChangeResourceInstancePlanned{
						ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.data.testing_data_source.data"),
						ChangeSrc:                  nil,
						PriorStateSrc: &states.ResourceInstanceObjectSrc{
							AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
								"id":    "data_known",
								"value": "known",
							}),
							Status:       states.ObjectReady,
							Dependencies: make([]addrs.ConfigResource, 0),
						},
						ProviderConfigAddr: mustDefaultRootProvider("testing"),
						Schema:             stacks_testing_provider.TestingDataSourceSchema,
					},
					&stackplan.PlannedChangeResourceInstancePlanned{
						ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.const.testing_resource.data"),
						ChangeSrc: &plans.ResourceInstanceChangeSrc{
							Addr:         mustAbsResourceInstance("testing_resource.data"),
							PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
							ProviderAddr: mustDefaultRootProvider("testing"),
							ChangeSrc: plans.ChangeSrc{
								Action: plans.Create,
								Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
								After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
									"id":    cty.StringVal("resource_known"),
									"value": cty.StringVal("known"),
								})),
							},
						},
						PriorStateSrc:      nil,
						ProviderConfigAddr: mustDefaultRootProvider("testing"),
						Schema:             stacks_testing_provider.TestingResourceSchema,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr:          mustAbsComponentInstance("component.main[*]"),
						PlanApplyable: false, // only deferred changes
						PlanComplete:  false, // deferred
						Action:        plans.Create,
						PlannedInputValues: map[string]plans.DynamicValue{
							"id":       mustPlanDynamicValueDynamicType(cty.StringVal("data_unknown")),
							"resource": mustPlanDynamicValueDynamicType(cty.StringVal("resource_unknown")),
						},
						PlannedInputValueMarks: map[string][]cty.PathValueMarks{
							"id":       nil,
							"resource": nil,
						},
						PlannedOutputValues: make(map[string]cty.Value),
						PlannedCheckResults: &states.CheckResults{},
						PlanTimestamp:       fakePlanTimestamp,
					},
					&stackplan.PlannedChangeDeferredResourceInstancePlanned{
						ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
							ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
								Component: stackaddrs.AbsComponentInstance{
									Item: stackaddrs.ComponentInstance{
										Component: stackaddrs.Component{
											Name: "main",
										},
										Key: addrs.WildcardKey,
									},
								},
								Item: addrs.AbsResourceInstanceObject{
									ResourceInstance: mustAbsResourceInstance("data.testing_data_source.data"),
								},
							},
							ChangeSrc: &plans.ResourceInstanceChangeSrc{
								Addr:         mustAbsResourceInstance("data.testing_data_source.data"),
								PrevRunAddr:  mustAbsResourceInstance("data.testing_data_source.data"),
								ProviderAddr: mustDefaultRootProvider("testing"),
								ChangeSrc: plans.ChangeSrc{
									Action: plans.Read,
									Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
									After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
										"id":    cty.StringVal("data_unknown"),
										"value": cty.UnknownVal(cty.String),
									})),
								},
								ActionReason: plans.ResourceInstanceReadBecauseDependencyPending,
							},
							PriorStateSrc:      nil,
							ProviderConfigAddr: mustDefaultRootProvider("testing"),
							Schema:             stacks_testing_provider.TestingDataSourceSchema,
						},
						DeferredReason: providers.DeferredReasonProviderConfigUnknown,
					},
					&stackplan.PlannedChangeDeferredResourceInstancePlanned{
						ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
							ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
								Component: stackaddrs.AbsComponentInstance{
									Item: stackaddrs.ComponentInstance{
										Component: stackaddrs.Component{
											Name: "main",
										},
										Key: addrs.WildcardKey,
									},
								},
								Item: addrs.AbsResourceInstanceObject{
									ResourceInstance: mustAbsResourceInstance("testing_resource.data"),
								},
							},
							ChangeSrc: &plans.ResourceInstanceChangeSrc{
								Addr:         mustAbsResourceInstance("testing_resource.data"),
								PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
								ProviderAddr: mustDefaultRootProvider("testing"),
								ChangeSrc: plans.ChangeSrc{
									Action: plans.Create,
									Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
									After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
										"id":    cty.StringVal("resource_unknown"),
										"value": cty.UnknownVal(cty.String),
									})),
								},
							},
							PriorStateSrc:      nil,
							ProviderConfigAddr: mustDefaultRootProvider("testing"),
							Schema:             stacks_testing_provider.TestingResourceSchema,
						},
						DeferredReason: providers.DeferredReasonProviderConfigUnknown,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   mustStackInputVariable("providers"),
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.UnknownVal(cty.Set(cty.String)),
					},
				},
			},
		},
		"removed embedded component duplicate": {
			path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"for_each_input": cty.MapVal(map[string]cty.Value{
						"foo": cty.StringVal("bar"),
					}),
					"simple_input": cty.MapVal(map[string]cty.Value{
						"foo": cty.StringVal("bar"),
					}),
					"for_each_removed": cty.SetVal([]cty.Value{
						cty.StringVal("foo"),
					}),
					"simple_removed": cty.SetVal([]cty.Value{
						cty.StringVal("foo"),
					}),
				},
				wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
					diags = diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Cannot remove component instance",
						Detail:   "The component instance stack.for_each.component.self[\"foo\"] is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/for-each-component/for-each-component.tfcomponent.hcl:15,1-17.",
						Subject: &hcl.Range{
							Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl",
							Start:    hcl.Pos{Line: 38, Column: 1, Byte: 505},
							End:      hcl.Pos{Line: 38, Column: 8, Byte: 512},
						},
					})
					diags = diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Cannot remove component instance",
						Detail:   "The component instance stack.simple[\"foo\"].component.self is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/valid/valid.tfcomponent.hcl:19,1-17.",
						Subject: &hcl.Range{
							Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl",
							Start:    hcl.Pos{Line: 60, Column: 1, Byte: 811},
							End:      hcl.Pos{Line: 60, Column: 8, Byte: 818},
						},
					})
					return diags
				}),
			},
		},
		"deferred-embedded-stack-update": {
			path: path.Join("with-single-input", "deferred-embedded-stack-for-each"),
			state: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.a[\"deferred\"].component.self")).
					AddInputVariable("id", cty.StringVal("deferred")).
					AddInputVariable("input", cty.StringVal("deferred"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("stack.a[\"deferred\"].component.self.testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "deferred",
							"value": "deferred",
						}),
					})).
				AddInput("stacks", cty.MapVal(map[string]cty.Value{
					"deferred": cty.StringVal("deferred"),
				})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("deferred", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("deferred"),
					"value": cty.StringVal("deferred"),
				})).
				Build(),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"stacks": cty.UnknownVal(cty.Map(cty.String)),
				},
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr: stackaddrs.Absolute(
							stackaddrs.RootStackInstance.Child("a", addrs.StringKey("deferred")),
							stackaddrs.ComponentInstance{
								Component: stackaddrs.Component{Name: "self"},
							},
						),
						PlanApplyable: false, // Everything is deferred, so nothing to apply.
						PlanComplete:  false,
						Action:        plans.Update,
						PlannedInputValues: map[string]plans.DynamicValue{
							"id":    mustPlanDynamicValueDynamicType(cty.StringVal("deferred")),
							"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
						},
						PlannedOutputValues: map[string]cty.Value{},
						PlannedCheckResults: &states.CheckResults{},
						PlanTimestamp:       fakePlanTimestamp,
						PlannedInputValueMarks: map[string][]cty.PathValueMarks{
							"id":    nil,
							"input": nil,
						},
					},
					&stackplan.PlannedChangeDeferredResourceInstancePlanned{
						DeferredReason: providers.DeferredReasonDeferredPrereq,
						ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
							ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
								Component: stackaddrs.Absolute(
									stackaddrs.RootStackInstance.Child("a", addrs.StringKey("deferred")),
									stackaddrs.ComponentInstance{
										Component: stackaddrs.Component{Name: "self"},
									},
								),
								Item: addrs.AbsResourceInstanceObject{
									ResourceInstance: addrs.Resource{
										Mode: addrs.ManagedResourceMode,
										Type: "testing_resource",
										Name: "data",
									}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								},
							},
							ProviderConfigAddr: addrs.AbsProviderConfig{
								Module:   addrs.RootModule,
								Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
							},
							ChangeSrc: &plans.ResourceInstanceChangeSrc{
								Addr: addrs.Resource{
									Mode: addrs.ManagedResourceMode,
									Type: "testing_resource",
									Name: "data",
								}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								PrevRunAddr: addrs.Resource{
									Mode: addrs.ManagedResourceMode,
									Type: "testing_resource",
									Name: "data",
								}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								ProviderAddr: addrs.AbsProviderConfig{
									Module:   addrs.RootModule,
									Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
								},
								ChangeSrc: plans.ChangeSrc{
									Action: plans.Update,
									Before: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
										"id":    cty.StringVal("deferred"),
										"value": cty.StringVal("deferred"),
									}), stacks_testing_provider.TestingResourceSchema.Body),
									After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
										"id":    cty.StringVal("deferred"),
										"value": cty.UnknownVal(cty.String),
									}), stacks_testing_provider.TestingResourceSchema.Body),
									AfterSensitivePaths: nil,
								},
							},
							PriorStateSrc: &states.ResourceInstanceObjectSrc{
								Status: states.ObjectReady,
								AttrsJSON: mustMarshalJSONAttrs(map[string]any{
									"id":    "deferred",
									"value": "deferred",
								}),
								Dependencies: make([]addrs.ConfigResource, 0),
							},
							Schema: stacks_testing_provider.TestingResourceSchema,
						},
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "stacks"},
						Action: plans.Update,
						Before: cty.MapVal(map[string]cty.Value{
							"deferred": cty.StringVal("deferred"),
						}),
						After: cty.UnknownVal(cty.Map(cty.String)),
					},
				},
			},
		},
		"deferred-embedded-stack-create": {
			path: path.Join("with-single-input", "deferred-embedded-stack-for-each"),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"stacks": cty.UnknownVal(cty.Map(cty.String)),
				},
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr: stackaddrs.Absolute(
							stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey),
							stackaddrs.ComponentInstance{
								Component: stackaddrs.Component{Name: "self"},
							},
						),
						PlanApplyable: false, // Everything is deferred, so nothing to apply.
						PlanComplete:  false,
						Action:        plans.Create,
						PlannedInputValues: map[string]plans.DynamicValue{
							"id":    mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
							"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
						},
						PlannedOutputValues: map[string]cty.Value{},
						PlannedCheckResults: &states.CheckResults{},
						PlanTimestamp:       fakePlanTimestamp,
						PlannedInputValueMarks: map[string][]cty.PathValueMarks{
							"id":    nil,
							"input": nil,
						},
					},
					&stackplan.PlannedChangeDeferredResourceInstancePlanned{
						DeferredReason: providers.DeferredReasonDeferredPrereq,
						ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
							ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
								Component: stackaddrs.Absolute(
									stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey),
									stackaddrs.ComponentInstance{
										Component: stackaddrs.Component{Name: "self"},
									},
								),
								Item: addrs.AbsResourceInstanceObject{
									ResourceInstance: addrs.Resource{
										Mode: addrs.ManagedResourceMode,
										Type: "testing_resource",
										Name: "data",
									}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								},
							},
							ProviderConfigAddr: addrs.AbsProviderConfig{
								Module:   addrs.RootModule,
								Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
							},
							ChangeSrc: &plans.ResourceInstanceChangeSrc{
								Addr: addrs.Resource{
									Mode: addrs.ManagedResourceMode,
									Type: "testing_resource",
									Name: "data",
								}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								PrevRunAddr: addrs.Resource{
									Mode: addrs.ManagedResourceMode,
									Type: "testing_resource",
									Name: "data",
								}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								ProviderAddr: addrs.AbsProviderConfig{
									Module:   addrs.RootModule,
									Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
								},
								ChangeSrc: plans.ChangeSrc{
									Action: plans.Create,
									Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
									After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
										"id":    cty.UnknownVal(cty.String),
										"value": cty.UnknownVal(cty.String),
									}), stacks_testing_provider.TestingResourceSchema.Body),
									AfterSensitivePaths: nil,
								},
							},
							Schema: stacks_testing_provider.TestingResourceSchema,
						},
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "stacks"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.UnknownVal(cty.Map(cty.String)),
					},
				},
			},
		},
		"deferred-embedded-stack-and-component-for-each": {
			path: path.Join("with-single-input", "deferred-embedded-stack-and-component-for-each"),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"stacks": cty.UnknownVal(cty.Map(cty.Set(cty.String))),
				},
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr: stackaddrs.Absolute(
							stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey),
							stackaddrs.ComponentInstance{
								Component: stackaddrs.Component{Name: "self"},
								Key:       addrs.WildcardKey,
							},
						),
						PlanApplyable: false, // Everything is deferred, so nothing to apply.
						PlanComplete:  false,
						Action:        plans.Create,
						PlannedInputValues: map[string]plans.DynamicValue{
							"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
							"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
						},
						PlannedOutputValues: map[string]cty.Value{},
						PlannedCheckResults: &states.CheckResults{},
						PlanTimestamp:       fakePlanTimestamp,
						PlannedInputValueMarks: map[string][]cty.PathValueMarks{
							"id":    nil,
							"input": nil,
						},
					},
					&stackplan.PlannedChangeDeferredResourceInstancePlanned{
						DeferredReason: providers.DeferredReasonDeferredPrereq,
						ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
							ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
								Component: stackaddrs.Absolute(
									stackaddrs.RootStackInstance.Child("a", addrs.WildcardKey),
									stackaddrs.ComponentInstance{
										Component: stackaddrs.Component{Name: "self"},
										Key:       addrs.WildcardKey,
									},
								),
								Item: addrs.AbsResourceInstanceObject{
									ResourceInstance: addrs.Resource{
										Mode: addrs.ManagedResourceMode,
										Type: "testing_resource",
										Name: "data",
									}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								},
							},
							ProviderConfigAddr: addrs.AbsProviderConfig{
								Module:   addrs.RootModule,
								Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
							},
							ChangeSrc: &plans.ResourceInstanceChangeSrc{
								Addr: addrs.Resource{
									Mode: addrs.ManagedResourceMode,
									Type: "testing_resource",
									Name: "data",
								}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								PrevRunAddr: addrs.Resource{
									Mode: addrs.ManagedResourceMode,
									Type: "testing_resource",
									Name: "data",
								}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
								ProviderAddr: addrs.AbsProviderConfig{
									Module:   addrs.RootModule,
									Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
								},
								ChangeSrc: plans.ChangeSrc{
									Action: plans.Create,
									Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
									After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
										"id":    cty.UnknownVal(cty.String),
										"value": cty.UnknownVal(cty.String),
									}), stacks_testing_provider.TestingResourceSchema.Body),
									AfterSensitivePaths: nil,
								},
							},
							Schema: stacks_testing_provider.TestingResourceSchema,
						},
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "stacks"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.UnknownVal(cty.Map(cty.Set(cty.String))),
					},
				},
			},
		},
		"removed block targets stack not in configuration or state": {
			path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"input": cty.MapValEmpty(cty.String),
					"removed": cty.MapVal(map[string]cty.Value{
						"component": cty.StringVal("component"),
					}),
				},
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "input"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.MapValEmpty(cty.String),
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "removed"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After: cty.MapVal(map[string]cty.Value{
							"component": cty.StringVal("component"),
						}),
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "removed-direct"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.SetValEmpty(cty.String),
					},
				},
			},
		},
		"embedded stack in state but not in configuration": {
			path: filepath.Join("with-single-input", "valid"),
			state: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.child.component.self"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("stack.child.component.self.testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "leftover",
							"value": "leftover",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("leftover", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("leftover"),
					"value": cty.StringVal("leftover"),
				})).
				Build(),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"input": cty.StringVal("input"),
				},
				wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
					return diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Unclaimed component instance",
						Detail:   "The component instance stack.child.component.self is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.",
					})
				}),
			},
		},
		"removed and stack block target the same stack": {
			path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"input": cty.MapVal(map[string]cty.Value{
						"component": cty.StringVal("component"),
					}),
					"removed": cty.MapVal(map[string]cty.Value{
						"component": cty.StringVal("component"),
					}),
				},
				wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
					return diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Cannot remove stack instance",
						Detail:   "The stack instance stack.simple[\"component\"] is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl:25,1-15.",
						Subject: &hcl.Range{
							Filename: "git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl",
							Start:    hcl.Pos{Line: 36, Column: 1, Byte: 441},
							End:      hcl.Pos{Line: 36, Column: 8, Byte: 448},
						},
					})
				}),
			},
		},
		"removed targets stack block in embedded stack that exists": {
			path: filepath.Join("with-single-input", "removed-stack-from-embedded-stack"),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"input": cty.MapVal(map[string]cty.Value{
						"component": cty.MapVal(map[string]cty.Value{
							"component": cty.StringVal("component"),
						}),
					}),
					"removed": cty.MapVal(map[string]cty.Value{
						"component": cty.MapVal(map[string]cty.Value{
							"id":    cty.StringVal("component"),
							"input": cty.StringVal("component"),
						}),
					}),
				},
				wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
					return diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Cannot remove stack instance",
						Detail:   "The stack instance stack.embedded[\"component\"].stack.simple[\"component\"] is targeted by an embedded stack block and cannot be removed. The relevant embedded stack is defined at git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl:25,1-15.",
						Subject: &hcl.Range{
							Filename: "git::https://example.com/test.git//with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfcomponent.hcl",
							Start:    hcl.Pos{Line: 28, Column: 1, Byte: 360},
							End:      hcl.Pos{Line: 28, Column: 8, Byte: 367},
						},
					})
				}),
			},
		},
		"removed block targets component inside removed stack": {
			path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
			state: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")).
					AddInputVariable("id", cty.StringVal("component")).
					AddInputVariable("input", cty.StringVal("component"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "component",
							"value": "component",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("component", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("component"),
					"value": cty.StringVal("component"),
				})).
				Build(),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"removed": cty.MapVal(map[string]cty.Value{
						"component": cty.StringVal("component"),
					}),
					"removed-direct": cty.SetVal([]cty.Value{
						cty.StringVal("component"),
					}),
				},
				wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
					return diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Cannot remove component instance",
						Detail:   "The component instance stack.simple[\"component\"].component.self is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/valid/valid.tfcomponent.hcl:19,1-17.",
						Subject: &hcl.Range{
							Filename: "git::https://example.com/test.git//with-single-input/removed-stack-instance-dynamic/removed-stack-instance-dynamic.tfcomponent.hcl",
							Start:    hcl.Pos{Line: 51, Column: 1, Byte: 708},
							End:      hcl.Pos{Line: 51, Column: 8, Byte: 715},
						},
					})
				}),
			},
		},
		"removed block targets orphaned component": {
			path: filepath.Join("with-single-input", "removed-component-from-stack-dynamic"),
			state: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")).
					AddInputVariable("id", cty.StringVal("component")).
					AddInputVariable("input", cty.StringVal("component"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "component",
							"value": "component",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("component", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("component"),
					"value": cty.StringVal("component"),
				})).
				Build(),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"simple_input": cty.MapValEmpty(cty.String),
					"simple_removed": cty.SetVal([]cty.Value{
						cty.StringVal("component"),
					}),
				},
				wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
					return diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid removed block",
						Detail:   "The component instance stack.simple[\"component\"].component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.",
						Subject: &hcl.Range{
							Filename: "git::https://example.com/test.git//with-single-input/removed-component-from-stack-dynamic/removed-component-from-stack-dynamic.tfcomponent.hcl",
							Start:    hcl.Pos{Line: 60, Column: 1, Byte: 811},
							End:      hcl.Pos{Line: 60, Column: 8, Byte: 818},
						},
					})
				}),
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: false,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "for_each_input"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.MapValEmpty(cty.String),
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "for_each_removed"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.SetValEmpty(cty.String),
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "simple_input"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.MapValEmpty(cty.String),
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "simple_removed"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After: cty.SetVal([]cty.Value{
							cty.StringVal("component"),
						}),
					},
				},
			},
		},
		"removed block targets orphaned stack": {
			path: filepath.Join("with-single-input", "removed-stack-from-embedded-stack"),
			state: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.embedded[\"component\"].stack.simple[\"component\"].component.self")).
					AddInputVariable("id", cty.StringVal("component")).
					AddInputVariable("input", cty.StringVal("component"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("stack.embedded[\"component\"].stack.simple[\"component\"].component.self.testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "component",
							"value": "component",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("component", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("component"),
					"value": cty.StringVal("component"),
				})).
				Build(),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"input": cty.MapValEmpty(cty.Map(cty.String)),
					"removed": cty.MapVal(map[string]cty.Value{
						"component": cty.MapVal(map[string]cty.Value{
							"id":    cty.StringVal("component"),
							"input": cty.StringVal("component"),
						}),
					}),
				},
				wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
					return diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid removed block",
						Detail:   "The component instance stack.embedded[\"component\"].stack.simple[\"component\"].component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.",
						Subject: &hcl.Range{
							Filename: "git::https://example.com/test.git//with-single-input/removed-stack-from-embedded-stack/removed-stack-from-embedded-stack.tfcomponent.hcl",
							Start:    hcl.Pos{Line: 28, Column: 1, Byte: 360},
							End:      hcl.Pos{Line: 28, Column: 8, Byte: 367},
						},
					})
				}),
			},
		},
		"removed block targets orphaned component without config definition": {
			path: filepath.Join("with-single-input", "orphaned-component"),
			state: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.embedded.component.self")).
					AddInputVariable("id", cty.StringVal("component")).
					AddInputVariable("input", cty.StringVal("component"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("stack.embedded.component.self.testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "component",
							"value": "component",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("component", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("component"),
					"value": cty.StringVal("component"),
				})).
				Build(),
			cycle: TestCycle{
				wantPlannedDiags: initDiags(func(diags tfdiags.Diagnostics) tfdiags.Diagnostics {
					return diags.Append(&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "Invalid removed block",
						Detail:   "The component instance stack.embedded.component.self could not be removed. The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.",
						Subject: &hcl.Range{
							Filename: "git::https://example.com/test.git//with-single-input/orphaned-component/orphaned-component.tfcomponent.hcl",
							Start:    hcl.Pos{Line: 10, Column: 1, Byte: 131},
							End:      hcl.Pos{Line: 10, Column: 8, Byte: 138},
						},
					})
				}),
			},
		},
		"unknown embedded stack with internal component targeted by concrete removed block": {
			path: filepath.Join("with-single-input", "removed-stack-instance-dynamic"),
			state: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.simple[\"component\"].component.self")).
					AddInputVariable("id", cty.StringVal("component")).
					AddInputVariable("input", cty.StringVal("component"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "component",
							"value": "component",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("component", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("component"),
					"value": cty.StringVal("component"),
				})).
				Build(),
			cycle: TestCycle{
				planInputs: map[string]cty.Value{
					"removed": cty.UnknownVal(cty.Map(cty.String)),
				},
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr:   mustAbsComponentInstance("stack.simple[\"component\"].component.self"),
						Action: plans.Delete,
						Mode:   plans.DestroyMode,
						PlannedInputValues: map[string]plans.DynamicValue{
							"id":    mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
							"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
						},
						PlannedInputValueMarks: map[string][]cty.PathValueMarks{
							"id":    nil,
							"input": nil,
						},
						PlannedOutputValues: make(map[string]cty.Value),
						PlannedCheckResults: &states.CheckResults{},
						PlanTimestamp:       fakePlanTimestamp,
					},
					&stackplan.PlannedChangeDeferredResourceInstancePlanned{
						ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
							ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.simple[\"component\"].component.self.testing_resource.data"),
							ChangeSrc: &plans.ResourceInstanceChangeSrc{
								Addr:         mustAbsResourceInstance("testing_resource.data"),
								PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
								ProviderAddr: mustDefaultRootProvider("testing"),
								ChangeSrc: plans.ChangeSrc{
									Action: plans.Delete,
									Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
										"id":    cty.StringVal("component"),
										"value": cty.StringVal("component"),
									})),
									After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
										"id":    cty.String,
										"value": cty.String,
									}))),
								},
							},
							PriorStateSrc: &states.ResourceInstanceObjectSrc{
								Status: states.ObjectReady,
								AttrsJSON: mustMarshalJSONAttrs(map[string]any{
									"id":    "component",
									"value": "component",
								}),
								Dependencies: make([]addrs.ConfigResource, 0),
							},
							ProviderConfigAddr: mustDefaultRootProvider("testing"),
							Schema:             stacks_testing_provider.TestingResourceSchema,
						},
						DeferredReason: providers.DeferredReasonDeferredPrereq,
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "input"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.MapValEmpty(cty.String),
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "removed"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.UnknownVal(cty.Map(cty.String)),
					},
					&stackplan.PlannedChangeRootInputValue{
						Addr:   stackaddrs.InputVariable{Name: "removed-direct"},
						Action: plans.Create,
						Before: cty.NullVal(cty.DynamicPseudoType),
						After:  cty.SetValEmpty(cty.String),
					},
				},
			},
		},
		"remove partial stack": {
			path: filepath.Join("with-single-input", "multiple-components", "removed"),
			state: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("stack.multiple.component.one")).
					AddInputVariable("id", cty.StringVal("one")).
					AddInputVariable("input", cty.StringVal("one"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("stack.multiple.component.one.testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "one",
							"value": "one",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("one", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("one"),
					"value": cty.StringVal("one"),
				})).
				Build(),
			cycle: TestCycle{
				wantPlannedChanges: []stackplan.PlannedChange{
					&stackplan.PlannedChangeApplyable{
						Applyable: true,
					},
					&stackplan.PlannedChangeHeader{
						TerraformVersion: version.SemVer,
					},
					&stackplan.PlannedChangePlannedTimestamp{
						PlannedTimestamp: fakePlanTimestamp,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr:          mustAbsComponentInstance("stack.multiple.component.one"),
						PlanApplyable: true,
						PlanComplete:  true,
						Action:        plans.Delete,
						Mode:          plans.DestroyMode,
						PlannedInputValues: map[string]plans.DynamicValue{
							"id":    mustPlanDynamicValueDynamicType(cty.StringVal("one")),
							"input": mustPlanDynamicValueDynamicType(cty.StringVal("one")),
						},
						PlannedInputValueMarks: map[string][]cty.PathValueMarks{
							"id":    nil,
							"input": nil,
						},
						PlannedOutputValues: make(map[string]cty.Value),
						PlannedCheckResults: &states.CheckResults{},
						PlanTimestamp:       fakePlanTimestamp,
					},
					&stackplan.PlannedChangeResourceInstancePlanned{
						ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.multiple.component.one.testing_resource.data"),
						ChangeSrc: &plans.ResourceInstanceChangeSrc{
							Addr:         mustAbsResourceInstance("testing_resource.data"),
							PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
							ProviderAddr: mustDefaultRootProvider("testing"),
							ChangeSrc: plans.ChangeSrc{
								Action: plans.Delete,
								Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
									"id":    cty.StringVal("one"),
									"value": cty.StringVal("one"),
								})),
								After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
									"id":    cty.String,
									"value": cty.String,
								}))),
							},
						},
						PriorStateSrc: &states.ResourceInstanceObjectSrc{
							Status: states.ObjectReady,
							AttrsJSON: mustMarshalJSONAttrs(map[string]any{
								"id":    "one",
								"value": "one",
							}),
							Dependencies: make([]addrs.ConfigResource, 0),
						},
						ProviderConfigAddr: mustDefaultRootProvider("testing"),
						Schema:             stacks_testing_provider.TestingResourceSchema,
					},
					&stackplan.PlannedChangeComponentInstance{
						Addr:                mustAbsComponentInstance("stack.multiple.component.two"),
						PlanApplyable:       true,
						PlanComplete:        true,
						Action:              plans.Delete,
						Mode:                plans.DestroyMode,
						PlannedOutputValues: make(map[string]cty.Value),
						PlanTimestamp:       fakePlanTimestamp,
					},
				},
			},
		},
	}
	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			ctx := context.Background()

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

			store := tc.store
			if store == nil {
				store = stacks_testing_provider.NewResourceStore()
			}

			testContext := TestContext{
				timestamp: &fakePlanTimestamp,
				config:    loadMainBundleConfigForTest(t, tc.path),
				providers: map[addrs.Provider]providers.Factory{
					addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProviderWithData(t, store), nil
					},
				},
				dependencyLocks: *lock,
			}

			testContext.Plan(t, ctx, tc.state, tc.cycle)
		})
	}
}

func TestPlanWithMissingInputVariable(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "plan-undeclared-variable-in-component")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
				return terraformProvider.NewProvider(), nil
			},
		},

		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	_, gotDiags := collectPlanOutput(changesCh, diagsCh)

	// We'll normalize the diagnostics to be of consistent underlying type
	// using ForRPC, so that we can easily diff them; we don't actually care
	// about which underlying implementation is in use.
	gotDiags = gotDiags.ForRPC()
	var wantDiags tfdiags.Diagnostics
	wantDiags = wantDiags.Append(&hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  "Reference to undeclared input variable",
		Detail:   `There is no variable "input" block declared in this stack.`,
		Subject: &hcl.Range{
			Filename: mainBundleSourceAddrStr("plan-undeclared-variable-in-component/undeclared-variable.tfcomponent.hcl"),
			Start:    hcl.Pos{Line: 17, Column: 13, Byte: 250},
			End:      hcl.Pos{Line: 17, Column: 22, Byte: 259},
		},
	})
	wantDiags = wantDiags.ForRPC()

	if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
		t.Errorf("wrong diagnostics\n%s", diff)
	}
}

func TestPlanWithNoValueForRequiredVariable(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "plan-no-value-for-required-variable")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
				return terraformProvider.NewProvider(), nil
			},
		},

		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	_, gotDiags := collectPlanOutput(changesCh, diagsCh)

	// We'll normalize the diagnostics to be of consistent underlying type
	// using ForRPC, so that we can easily diff them; we don't actually care
	// about which underlying implementation is in use.
	gotDiags = gotDiags.ForRPC()
	var wantDiags tfdiags.Diagnostics
	wantDiags = wantDiags.Append(&hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  "No value for required variable",
		Detail:   `The root input variable "var.beep" is not set, and has no default value.`,
		Subject: &hcl.Range{
			Filename: mainBundleSourceAddrStr("plan-no-value-for-required-variable/unset-variable.tfcomponent.hcl"),
			Start:    hcl.Pos{Line: 1, Column: 1, Byte: 0},
			End:      hcl.Pos{Line: 1, Column: 16, Byte: 15},
		},
	})
	wantDiags = wantDiags.ForRPC()

	if diff := cmp.Diff(wantDiags, gotDiags); diff != "" {
		t.Errorf("wrong diagnostics\n%s", diff)
	}
}

func TestPlanWithVariableDefaults(t *testing.T) {
	// Test that defaults are applied correctly for both unspecified input
	// variables and those with an explicit null value.
	testCases := map[string]struct {
		inputs map[stackaddrs.InputVariable]ExternalInputValue
	}{
		"unspecified": {
			inputs: make(map[stackaddrs.InputVariable]ExternalInputValue),
		},
		"explicit null": {
			inputs: map[stackaddrs.InputVariable]ExternalInputValue{
				{Name: "beep"}: {
					Value:    cty.NullVal(cty.DynamicPseudoType),
					DefRange: tfdiags.SourceRange{Filename: "fake.tfcomponent.hcl"},
				},
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ctx := context.Background()
			cfg := loadMainBundleConfigForTest(t, "plan-variable-defaults")

			fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
			if err != nil {
				t.Fatal(err)
			}

			changesCh := make(chan stackplan.PlannedChange, 8)
			diagsCh := make(chan tfdiags.Diagnostic, 2)
			req := PlanRequest{
				Config:             cfg,
				InputValues:        tc.inputs,
				ForcePlanTimestamp: &fakePlanTimestamp,
			}
			resp := PlanResponse{
				PlannedChanges: changesCh,
				Diagnostics:    diagsCh,
			}

			go Plan(ctx, &req, &resp)
			gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

			if len(diags) != 0 {
				t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
			}

			wantChanges := []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangeOutputValue{
					Addr:   stackaddrs.OutputValue{Name: "beep"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.StringVal("BEEP"),
				},
				&stackplan.PlannedChangeOutputValue{
					Addr:   stackaddrs.OutputValue{Name: "defaulted"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.StringVal("BOOP"),
				},
				&stackplan.PlannedChangeOutputValue{
					Addr:   stackaddrs.OutputValue{Name: "specified"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.StringVal("BEEP"),
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr: stackaddrs.InputVariable{
						Name: "beep",
					},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.StringVal("BEEP"),
				},
			}
			sort.SliceStable(gotChanges, func(i, j int) bool {
				return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
			})

			if diff := cmp.Diff(wantChanges, gotChanges, ctydebug.CmpOptions); diff != "" {
				t.Errorf("wrong changes\n%s", diff)
			}
		})
	}
}

func TestPlanWithComplexVariableDefaults(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("complex-inputs"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange)
	diagsCh := make(chan tfdiags.Diagnostic)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks: *lock,
		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "optional"}: {
				Value:    cty.EmptyObjectVal, // This should be populated by defaults.
				DefRange: tfdiags.SourceRange{},
			},
		},
		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	go Plan(ctx, &req, &resp)
	changes, diags := collectPlanOutput(changesCh, diagsCh)
	if len(diags) != 0 {
		t.Fatalf("unexpected diagnostics: %s", diags)
	}

	sort.SliceStable(changes, func(i, j int) bool {
		return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j])
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr:               mustAbsComponentInstance("component.self"),
			PlanComplete:       true,
			PlanApplyable:      true,
			Action:             plans.Create,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
			PlannedInputValues: map[string]plans.DynamicValue{
				"input": mustPlanDynamicValueDynamicType(cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("cec9bc39"),
						"value": cty.StringVal("hello, mercury!"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("78d8b3d7"),
						"value": cty.StringVal("hello, venus!"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"id":    cty.NullVal(cty.String),
						"value": cty.StringVal("hello, earth!"),
					}),
				})),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"input": nil,
			},
			PlannedOutputValues: make(map[string]cty.Value),
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[0]"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:        mustAbsResourceInstance("testing_resource.data[0]"),
				PrevRunAddr: mustAbsResourceInstance("testing_resource.data[0]"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("cec9bc39"),
						"value": cty.StringVal("hello, mercury!"),
					})),
				},
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[1]"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:        mustAbsResourceInstance("testing_resource.data[1]"),
				PrevRunAddr: mustAbsResourceInstance("testing_resource.data[1]"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("78d8b3d7"),
						"value": cty.StringVal("hello, venus!"),
					})),
				},
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data[2]"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:        mustAbsResourceInstance("testing_resource.data[2]"),
				PrevRunAddr: mustAbsResourceInstance("testing_resource.data[2]"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("hello, earth!"),
					})),
				},
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr:               mustAbsComponentInstance("stack.child.component.parent"),
			PlanComplete:       true,
			PlanApplyable:      true,
			Action:             plans.Create,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
			PlannedInputValues: map[string]plans.DynamicValue{
				"input": mustPlanDynamicValueDynamicType(cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("cec9bc39"),
						"value": cty.StringVal("hello, mercury!"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("78d8b3d7"),
						"value": cty.StringVal("hello, venus!"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"id":    cty.NullVal(cty.String),
						"value": cty.StringVal("hello, earth!"),
					}),
				})),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"input": nil,
			},
			PlannedOutputValues: make(map[string]cty.Value),
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[0]"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:        mustAbsResourceInstance("testing_resource.data[0]"),
				PrevRunAddr: mustAbsResourceInstance("testing_resource.data[0]"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("cec9bc39"),
						"value": cty.StringVal("hello, mercury!"),
					})),
				},
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[1]"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:        mustAbsResourceInstance("testing_resource.data[1]"),
				PrevRunAddr: mustAbsResourceInstance("testing_resource.data[1]"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("78d8b3d7"),
						"value": cty.StringVal("hello, venus!"),
					})),
				},
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.child.component.parent.testing_resource.data[2]"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:        mustAbsResourceInstance("testing_resource.data[2]"),
				PrevRunAddr: mustAbsResourceInstance("testing_resource.data[2]"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("hello, earth!"),
					})),
				},
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "default"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":    cty.StringVal("cec9bc39"),
				"value": cty.StringVal("hello, mercury!"),
			}),
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "optional"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":    cty.NullVal(cty.String),
				"value": cty.StringVal("hello, earth!"),
			}),
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "optional_default"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":    cty.StringVal("78d8b3d7"),
				"value": cty.StringVal("hello, venus!"),
			}),
		},
	}

	if diff := cmp.Diff(wantChanges, changes, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithSingleResource(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "with-single-resource")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
				return terraformProvider.NewProvider(), nil
			},
		},

		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 0 {
		t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
	}

	// The order of emission for our planned changes is unspecified since it
	// depends on how the various goroutines get scheduled, and so we'll
	// arbitrarily sort gotChanges lexically by the name of the change type
	// so that we have some dependable order to diff against below.
	sort.Slice(gotChanges, func(i, j int) bool {
		ic := gotChanges[i]
		jc := gotChanges[j]
		return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc)
	})

	// extract the schema from the builtin provider so we can generate correctly
	// shaped plan objects.
	schema := terraform.NewProvider().GetProviderSchema().ResourceTypes["terraform_data"]
	wantMap := schema.Body.EmptyValue().AsValueMap()
	wantMap["id"] = cty.UnknownVal(cty.String).RefineNotNull()
	wantMap["input"] = cty.StringVal("hello")
	wantMap["output"] = cty.UnknownVal(cty.String)

	wantVal := cty.ObjectVal(wantMap)
	wantValEncoded, err := msgpack.Marshal(wantVal, schema.Body.ImpliedType())
	if err != nil {
		t.Fatal(err)
	}

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:              plans.Create,
			PlanApplyable:       true,
			PlanComplete:        true,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues:  make(map[string]plans.DynamicValue),
			PlannedOutputValues: map[string]cty.Value{
				"input":  cty.StringVal("hello"),
				"output": cty.UnknownVal(cty.String),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangeOutputValue{
			Addr:   stackaddrs.OutputValue{Name: "obj"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After: cty.ObjectVal(map[string]cty.Value{
				"input":  cty.StringVal("hello"),
				"output": cty.UnknownVal(cty.String),
			}),
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
				Component: stackaddrs.Absolute(
					stackaddrs.RootStackInstance,
					stackaddrs.ComponentInstance{
						Component: stackaddrs.Component{Name: "self"},
					},
				),
				Item: addrs.AbsResourceInstanceObject{
					ResourceInstance: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "terraform_data",
						Name: "main",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"),
			},
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "terraform_data",
					Name: "main",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				PrevRunAddr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "terraform_data",
					Name: "main",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.NewBuiltInProvider("terraform"),
				},
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After:  plans.DynamicValue(wantValEncoded),
				},
			},

			// The following is schema for the real terraform_data resource
			// type from the real terraform.io/builtin/terraform provider
			// maintained elsewhere in this codebase. If that schema changes
			// in future then this should change to match it.
			Schema: schema,
		},
	}

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithEphemeralInputVariables(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "variable-ephemeral")

	t.Run("with variables set", func(t *testing.T) {
		changesCh := make(chan stackplan.PlannedChange, 8)
		diagsCh := make(chan tfdiags.Diagnostic, 2)
		fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
		if err != nil {
			t.Fatal(err)
		}

		req := PlanRequest{
			Config: cfg,
			InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{
				{Name: "eph"}:    {Value: cty.StringVal("eph value")},
				{Name: "noneph"}: {Value: cty.StringVal("noneph value")},
			},
			ForcePlanTimestamp: &fakePlanTimestamp,
		}
		resp := PlanResponse{
			PlannedChanges: changesCh,
			Diagnostics:    diagsCh,
		}

		go Plan(ctx, &req, &resp)
		gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
		}

		wantChanges := []stackplan.PlannedChange{
			&stackplan.PlannedChangeApplyable{
				Applyable: true,
			},
			&stackplan.PlannedChangeHeader{
				TerraformVersion: version.SemVer,
			},
			&stackplan.PlannedChangePlannedTimestamp{
				PlannedTimestamp: fakePlanTimestamp,
			},
			&stackplan.PlannedChangeRootInputValue{
				Addr: stackaddrs.InputVariable{
					Name: "eph",
				},
				Action:          plans.Create,
				Before:          cty.NullVal(cty.DynamicPseudoType),
				After:           cty.NullVal(cty.String), // ephemeral
				RequiredOnApply: true,
			},
			&stackplan.PlannedChangeRootInputValue{
				Addr: stackaddrs.InputVariable{
					Name: "noneph",
				},
				Action: plans.Create,
				Before: cty.NullVal(cty.DynamicPseudoType),
				After:  cty.StringVal("noneph value"),
			},
		}
		sort.SliceStable(gotChanges, func(i, j int) bool {
			return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
		})

		if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
			t.Errorf("wrong changes\n%s", diff)
		}
	})

	t.Run("without variables set", func(t *testing.T) {
		changesCh := make(chan stackplan.PlannedChange, 8)
		diagsCh := make(chan tfdiags.Diagnostic, 2)
		fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
		if err != nil {
			t.Fatal(err)
		}
		req := PlanRequest{
			InputValues: map[stackaddrs.InputVariable]stackeval.ExternalInputValue{
				// Intentionally not set for this subtest.
			},
			Config:             cfg,
			ForcePlanTimestamp: &fakePlanTimestamp,
		}
		resp := PlanResponse{
			PlannedChanges: changesCh,
			Diagnostics:    diagsCh,
		}

		go Plan(ctx, &req, &resp)
		gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
		}

		wantChanges := []stackplan.PlannedChange{
			&stackplan.PlannedChangeApplyable{
				Applyable: true,
			},
			&stackplan.PlannedChangeHeader{
				TerraformVersion: version.SemVer,
			},
			&stackplan.PlannedChangePlannedTimestamp{
				PlannedTimestamp: fakePlanTimestamp,
			},
			&stackplan.PlannedChangeRootInputValue{
				Addr: stackaddrs.InputVariable{
					Name: "eph",
				},
				Action:          plans.Create,
				Before:          cty.NullVal(cty.DynamicPseudoType),
				After:           cty.NullVal(cty.String), // ephemeral
				RequiredOnApply: false,
			},
			&stackplan.PlannedChangeRootInputValue{
				Addr: stackaddrs.InputVariable{
					Name: "noneph",
				},
				Action: plans.Create,
				Before: cty.NullVal(cty.DynamicPseudoType),
				After:  cty.NullVal(cty.String),
			},
		}
		sort.SliceStable(gotChanges, func(i, j int) bool {
			return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
		})

		if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
			t.Errorf("wrong changes\n%s", diff)
		}
	})
}

func TestPlanVariableOutputRoundtripNested(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "variable-output-roundtrip-nested")

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
	if err != nil {
		t.Fatal(err)
	}
	req := PlanRequest{
		Config:             cfg,
		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 0 {
		t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
	}

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangeOutputValue{
			Addr:   stackaddrs.OutputValue{Name: "msg"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.StringVal("default"),
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr: stackaddrs.InputVariable{
				Name: "msg",
			},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.StringVal("default"),
		},
	}
	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanSensitiveOutput(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "sensitive-output")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config:             cfg,
		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 0 {
		t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
	}

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:              plans.Create,
			PlanApplyable:       true,
			PlanComplete:        true,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues:  make(map[string]plans.DynamicValue),
			PlannedOutputValues: map[string]cty.Value{
				"out": cty.StringVal("secret").Mark(marks.Sensitive),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangeOutputValue{
			Addr:   stackaddrs.OutputValue{Name: "result"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.StringVal("secret").Mark(marks.Sensitive),
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
	}
	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanSensitiveOutputNested(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "sensitive-output-nested")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config:             cfg,
		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 0 {
		t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
	}

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangeOutputValue{
			Addr:   stackaddrs.OutputValue{Name: "result"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.StringVal("secret").Mark(marks.Sensitive),
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance.Child("child", addrs.NoKey),
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:              plans.Create,
			PlanApplyable:       true,
			PlanComplete:        true,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues:  make(map[string]plans.DynamicValue),
			PlannedOutputValues: map[string]cty.Value{
				"out": cty.StringVal("secret").Mark(marks.Sensitive),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
	}
	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanSensitiveOutputAsInput(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "sensitive-output-as-input")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config:             cfg,
		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 0 {
		t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
	}

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:        plans.Create,
			PlanApplyable: true,
			PlanComplete:  true,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
				mustAbsComponent("stack.sensitive.component.self"),
			),
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues: map[string]plans.DynamicValue{
				"secret": mustPlanDynamicValueDynamicType(cty.StringVal("secret")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"secret": {
					{
						Marks: cty.NewValueMarks(marks.Sensitive),
					},
				},
			},
			PlannedOutputValues: map[string]cty.Value{
				"result": cty.StringVal("SECRET").Mark(marks.Sensitive),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangeOutputValue{
			Addr:   stackaddrs.OutputValue{Name: "result"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType), // MessagePack nil
			After:  cty.StringVal("SECRET").Mark(marks.Sensitive),
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey),
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:              plans.Create,
			PlanApplyable:       true,
			PlanComplete:        true,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues:  make(map[string]plans.DynamicValue),
			PlannedOutputValues: map[string]cty.Value{
				"out": cty.StringVal("secret").Mark(marks.Sensitive),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
	}
	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithProviderConfig(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "with-provider-config")
	providerAddr := addrs.MustParseProviderSourceString("example.com/test/test")
	providerSchema := &providers.GetProviderSchemaResponse{
		Provider: providers.Schema{
			Body: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"name": {
						Type:     cty.String,
						Required: true,
					},
				},
			},
		},
	}
	inputVarAddr := stackaddrs.InputVariable{Name: "name"}
	fakeSrcRng := tfdiags.SourceRange{
		Filename: "fake-source",
	}
	lock := depsfile.NewLocks()
	lock.SetProvider(
		providerAddr,
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)

	t.Run("valid", func(t *testing.T) {
		changesCh := make(chan stackplan.PlannedChange, 8)
		diagsCh := make(chan tfdiags.Diagnostic, 2)

		provider := &default_testing_provider.MockProvider{
			GetProviderSchemaResponse:      providerSchema,
			ValidateProviderConfigResponse: &providers.ValidateProviderConfigResponse{},
			ConfigureProviderResponse:      &providers.ConfigureProviderResponse{},
		}

		req := PlanRequest{
			Config: cfg,
			InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
				inputVarAddr: {
					Value:    cty.StringVal("Jackson"),
					DefRange: fakeSrcRng,
				},
			},
			ProviderFactories: map[addrs.Provider]providers.Factory{
				providerAddr: func() (providers.Interface, error) {
					return provider, nil
				},
			},
			DependencyLocks: *lock,
		}
		resp := PlanResponse{
			PlannedChanges: changesCh,
			Diagnostics:    diagsCh,
		}
		go Plan(ctx, &req, &resp)
		_, diags := collectPlanOutput(changesCh, diagsCh)
		if len(diags) != 0 {
			t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
		}

		if !provider.ValidateProviderConfigCalled {
			t.Error("ValidateProviderConfig wasn't called")
		} else {
			req := provider.ValidateProviderConfigRequest
			if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) {
				t.Errorf("wrong name in ValidateProviderConfig\ngot:  %#v\nwant: %#v", got, want)
			}
		}
		if !provider.ConfigureProviderCalled {
			t.Error("ConfigureProvider wasn't called")
		} else {
			req := provider.ConfigureProviderRequest
			if got, want := req.Config.GetAttr("name"), cty.StringVal("Jackson"); !got.RawEquals(want) {
				t.Errorf("wrong name in ConfigureProvider\ngot:  %#v\nwant: %#v", got, want)
			}
		}
		if !provider.CloseCalled {
			t.Error("provider wasn't closed")
		}
	})
}

func TestPlanWithRemovedResource(t *testing.T) {
	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
	if err != nil {
		t.Fatal(err)
	}

	attrs := map[string]interface{}{
		"id": "FE1D5830765C",
		"input": map[string]interface{}{
			"value": "hello",
			"type":  "string",
		},
		"output": map[string]interface{}{
			"value": nil,
			"type":  "string",
		},
		"triggers_replace": nil,
	}
	attrsJSON, err := json.Marshal(attrs)
	if err != nil {
		t.Fatal(err)
	}

	// We want to see that it's adding the extra context for when a provider is
	// missing for a resource that's in state and not in config.
	expectedDiagnostic := "has resources in state that"

	tcs := make(map[string]*string)
	tcs["missing-providers"] = &expectedDiagnostic
	tcs["valid-providers"] = nil

	for name, diag := range tcs {
		t.Run(name, func(t *testing.T) {
			ctx := context.Background()
			cfg := loadMainBundleConfigForTest(t, path.Join("empty-component", name))

			req := PlanRequest{
				Config: cfg,
				ProviderFactories: map[addrs.Provider]providers.Factory{
					addrs.NewBuiltInProvider("terraform"): func() (providers.Interface, error) {
						return terraformProvider.NewProvider(), nil
					},
				},

				ForcePlanTimestamp: &fakePlanTimestamp,

				// PrevState specifies a state with a resource that is not present in
				// the current configuration. This is a common situation when a resource
				// is removed from the configuration but still exists in the state.
				PrevState: stackstate.NewStateBuilder().
					AddResourceInstance(stackstate.NewResourceInstanceBuilder().
						SetAddr(stackaddrs.AbsResourceInstanceObject{
							Component: stackaddrs.AbsComponentInstance{
								Stack: stackaddrs.RootStackInstance,
								Item: stackaddrs.ComponentInstance{
									Component: stackaddrs.Component{
										Name: "self",
									},
									Key: addrs.NoKey,
								},
							},
							Item: addrs.AbsResourceInstanceObject{
								ResourceInstance: addrs.AbsResourceInstance{
									Module: addrs.RootModuleInstance,
									Resource: addrs.ResourceInstance{
										Resource: addrs.Resource{
											Mode: addrs.ManagedResourceMode,
											Type: "terraform_data",
											Name: "main",
										},
										Key: addrs.NoKey,
									},
								},
								DeposedKey: addrs.NotDeposed,
							},
						}).
						SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
							SchemaVersion: 0,
							AttrsJSON:     attrsJSON,
							Status:        states.ObjectReady,
						}).
						SetProviderAddr(addrs.AbsProviderConfig{
							Module:   addrs.RootModule,
							Provider: addrs.MustParseProviderSourceString("terraform.io/builtin/terraform"),
						})).
					Build(),
			}

			changesCh := make(chan stackplan.PlannedChange)
			diagsCh := make(chan tfdiags.Diagnostic)
			resp := PlanResponse{
				PlannedChanges: changesCh,
				Diagnostics:    diagsCh,
			}

			go Plan(ctx, &req, &resp)
			_, diags := collectPlanOutput(changesCh, diagsCh)

			if diag != nil {
				if len(diags) == 0 {
					t.Fatalf("expected diagnostics, got none")
				}
				if !strings.Contains(diags[0].Description().Detail, *diag) {
					t.Fatalf("expected diagnostic %q, got %q", *diag, diags[0].Description().Detail)
				}
			} else if len(diags) > 0 {
				t.Fatalf("unexpected diagnostics: %s", diags.ErrWithWarnings().Error())
			}
		})
	}
}

func TestPlanWithSensitivePropagation(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks:    *lock,
		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 0 {
		t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
	}

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			PlanApplyable: true,
			PlanComplete:  true,
			Action:        plans.Create,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
				stackaddrs.AbsComponent{
					Stack: stackaddrs.RootStackInstance,
					Item:  stackaddrs.Component{Name: "sensitive"},
				},
			),
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id": nil,
				"input": {
					{
						Marks: cty.NewValueMarks(marks.Sensitive),
					},
				},
			},
			PlannedOutputValues: make(map[string]cty.Value),
			PlanTimestamp:       fakePlanTimestamp,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
				Component: stackaddrs.Absolute(
					stackaddrs.RootStackInstance,
					stackaddrs.ComponentInstance{
						Component: stackaddrs.Component{Name: "self"},
					},
				),
				Item: addrs.AbsResourceInstanceObject{
					ResourceInstance: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "testing_resource",
					Name: "data",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				PrevRunAddr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "testing_resource",
					Name: "data",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.NewDefaultProvider("testing"),
				},
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("secret"),
					}), stacks_testing_provider.TestingResourceSchema.Body),
					AfterSensitivePaths: []cty.Path{
						cty.GetAttrPath("value"),
					},
				},
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "sensitive"},
				},
			),
			PlanApplyable:       true,
			PlanComplete:        true,
			Action:              plans.Create,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues:  make(map[string]plans.DynamicValue),
			PlannedOutputValues: map[string]cty.Value{
				"out": cty.StringVal("secret").Mark(marks.Sensitive),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "id"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.NullVal(cty.String),
		},
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithSensitivePropagationNested(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "sensitive-input-nested"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks: *lock,

		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 0 {
		t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
	}

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:        plans.Create,
			PlanApplyable: true,
			PlanComplete:  true,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
				mustAbsComponent("stack.sensitive.component.self"),
			),
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("secret")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id": nil,
				"input": {
					{
						Marks: cty.NewValueMarks(marks.Sensitive),
					},
				},
			},
			PlannedOutputValues: make(map[string]cty.Value),
			PlanTimestamp:       fakePlanTimestamp,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
				Component: stackaddrs.Absolute(
					stackaddrs.RootStackInstance,
					stackaddrs.ComponentInstance{
						Component: stackaddrs.Component{Name: "self"},
					},
				),
				Item: addrs.AbsResourceInstanceObject{
					ResourceInstance: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "testing_resource",
					Name: "data",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				PrevRunAddr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "testing_resource",
					Name: "data",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.NewDefaultProvider("testing"),
				},
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("secret"),
					}), stacks_testing_provider.TestingResourceSchema.Body),
					AfterSensitivePaths: []cty.Path{
						cty.GetAttrPath("value"),
					},
				},
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance.Child("sensitive", addrs.NoKey),
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:              plans.Create,
			PlanApplyable:       true,
			PlanComplete:        true,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues:  make(map[string]plans.DynamicValue),
			PlannedOutputValues: map[string]cty.Value{
				"out": cty.StringVal("secret").Mark(marks.Sensitive),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "id"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.NullVal(cty.String),
		},
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithForEach(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "input-from-component-list"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks: *lock,

		ForcePlanTimestamp: &fakePlanTimestamp,

		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "components"}: {
				Value:    cty.ListVal([]cty.Value{cty.StringVal("one"), cty.StringVal("two"), cty.StringVal("three")}),
				DefRange: tfdiags.SourceRange{},
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	_, diags := collectPlanOutput(changesCh, diagsCh)

	reportDiagnosticsForTest(t, diags)
	if len(diags) != 0 {
		t.FailNow() // We reported the diags above/
	}
}

func TestPlanWithCheckableObjects(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "checkable-objects")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks: *lock,

		ForcePlanTimestamp: &fakePlanTimestamp,

		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "foo"}: {
				Value: cty.StringVal("bar"),
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	var wantDiags tfdiags.Diagnostics
	wantDiags = wantDiags.Append(&hcl.Diagnostic{
		Severity: hcl.DiagWarning,

		Summary: "Check block assertion failed",
		Detail:  `value must be 'baz'`,
		Subject: &hcl.Range{
			Filename: mainBundleSourceAddrStr("checkable-objects/checkable-objects.tf"),
			Start:    hcl.Pos{Line: 41, Column: 21, Byte: 716},
			End:      hcl.Pos{Line: 41, Column: 57, Byte: 752},
		},
	})

	go Plan(ctx, &req, &resp)
	gotChanges, gotDiags := collectPlanOutput(changesCh, diagsCh)

	if diff := cmp.Diff(wantDiags.ForRPC(), gotDiags.ForRPC()); diff != "" {
		t.Errorf("wrong diagnostics\n%s", diff)
	}

	// The order of emission for our planned changes is unspecified since it
	// depends on how the various goroutines get scheduled, and so we'll
	// arbitrarily sort gotChanges lexically by the name of the change type
	// so that we have some dependable order to diff against below.
	sort.Slice(gotChanges, func(i, j int) bool {
		ic := gotChanges[i]
		jc := gotChanges[j]
		return fmt.Sprintf("%T", ic) < fmt.Sprintf("%T", jc)
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "single"},
				},
			),
			Action:        plans.Create,
			PlanApplyable: true,
			PlanComplete:  true,
			PlannedInputValues: map[string]plans.DynamicValue{
				"foo": mustPlanDynamicValueDynamicType(cty.StringVal("bar")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{"foo": nil},
			PlannedOutputValues: map[string]cty.Value{
				"foo": cty.StringVal("bar"),
			},
			PlannedCheckResults: &states.CheckResults{
				ConfigResults: addrs.MakeMap(
					addrs.MakeMapElem[addrs.ConfigCheckable](
						addrs.Check{
							Name: "value_is_baz",
						}.InModule(addrs.RootModule),
						&states.CheckResultAggregate{
							Status: checks.StatusFail,
							ObjectResults: addrs.MakeMap(
								addrs.MakeMapElem[addrs.Checkable](
									addrs.Check{
										Name: "value_is_baz",
									}.Absolute(addrs.RootModuleInstance),
									&states.CheckResultObject{
										Status:          checks.StatusFail,
										FailureMessages: []string{"value must be 'baz'"},
									},
								),
							),
						},
					),
					addrs.MakeMapElem[addrs.ConfigCheckable](
						addrs.InputVariable{
							Name: "foo",
						}.InModule(addrs.RootModule),
						&states.CheckResultAggregate{
							Status: checks.StatusPass,
							ObjectResults: addrs.MakeMap(
								addrs.MakeMapElem[addrs.Checkable](
									addrs.InputVariable{
										Name: "foo",
									}.Absolute(addrs.RootModuleInstance),
									&states.CheckResultObject{
										Status: checks.StatusPass,
									},
								),
							),
						},
					),
					addrs.MakeMapElem[addrs.ConfigCheckable](
						addrs.OutputValue{
							Name: "foo",
						}.InModule(addrs.RootModule),
						&states.CheckResultAggregate{
							Status: checks.StatusPass,
							ObjectResults: addrs.MakeMap(
								addrs.MakeMapElem[addrs.Checkable](
									addrs.OutputValue{
										Name: "foo",
									}.Absolute(addrs.RootModuleInstance),
									&states.CheckResultObject{
										Status: checks.StatusPass,
									},
								),
							),
						},
					),
					addrs.MakeMapElem[addrs.ConfigCheckable](
						addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "main",
						}.InModule(addrs.RootModule),
						&states.CheckResultAggregate{
							Status: checks.StatusPass,
							ObjectResults: addrs.MakeMap(
								addrs.MakeMapElem[addrs.Checkable](
									addrs.Resource{
										Mode: addrs.ManagedResourceMode,
										Type: "testing_resource",
										Name: "main",
									}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
									&states.CheckResultObject{
										Status: checks.StatusPass,
									},
								),
							),
						},
					),
				),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
				Component: stackaddrs.Absolute(
					stackaddrs.RootStackInstance,
					stackaddrs.ComponentInstance{
						Component: stackaddrs.Component{Name: "single"},
					},
				),
				Item: addrs.AbsResourceInstanceObject{
					ResourceInstance: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "main",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.NewDefaultProvider("testing"),
			},
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "testing_resource",
					Name: "main",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				PrevRunAddr: addrs.Resource{
					Mode: addrs.ManagedResourceMode,
					Type: "testing_resource",
					Name: "main",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.NewDefaultProvider("testing"),
				},
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("test"),
						"value": cty.StringVal("bar"),
					}), stacks_testing_provider.TestingResourceSchema.Body),
				},
			},

			Schema: stacks_testing_provider.TestingResourceSchema,
		},
	}

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithDeferredResource(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "deferrable-component")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange)
	diagsCh := make(chan tfdiags.Diagnostic)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks:    *lock,
		ForcePlanTimestamp: &fakePlanTimestamp,
		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "id"}: {
				Value: cty.StringVal("62594ae3"),
			},
			{Name: "defer"}: {
				Value: cty.BoolVal(true),
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	reportDiagnosticsForTest(t, diags)
	if len(diags) != 0 {
		t.FailNow() // We reported the diags above
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			PlanComplete:  false,
			PlanApplyable: false, // We don't have any resources to apply since they're deferred.
			Action:        plans.Create,
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.StringVal("62594ae3")),
				"defer": mustPlanDynamicValueDynamicType(cty.BoolVal(true)),
			},
			PlannedOutputValues: map[string]cty.Value{},
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"defer": nil,
			},
		},
		&stackplan.PlannedChangeDeferredResourceInstancePlanned{
			ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
				ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
					Component: stackaddrs.Absolute(
						stackaddrs.RootStackInstance,
						stackaddrs.ComponentInstance{
							Component: stackaddrs.Component{Name: "self"},
						},
					),
					Item: addrs.AbsResourceInstanceObject{
						ResourceInstance: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_deferred_resource",
							Name: "data",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					},
				},
				ProviderConfigAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
				ChangeSrc: &plans.ResourceInstanceChangeSrc{
					Addr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_deferred_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					PrevRunAddr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_deferred_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					ProviderAddr: addrs.AbsProviderConfig{
						Module:   addrs.RootModule,
						Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
					},
					ChangeSrc: plans.ChangeSrc{
						Action: plans.Create,
						Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
						After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
							"id":       cty.StringVal("62594ae3"),
							"value":    cty.NullVal(cty.String),
							"deferred": cty.BoolVal(true),
						}), stacks_testing_provider.DeferredResourceSchema.Body),
						AfterSensitivePaths: nil,
					},
				},
				Schema: stacks_testing_provider.DeferredResourceSchema,
			},
			DeferredReason: providers.DeferredReasonResourceConfigUnknown,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "defer"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.BoolVal(true),
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "id"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.StringVal("62594ae3"),
		},
	}

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithDeferredComponentForEach(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input-and-output", "deferred-component-for-each"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks:    *lock,
		ForcePlanTimestamp: &fakePlanTimestamp,
		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "components"}: {
				Value:    cty.UnknownVal(cty.Set(cty.String)),
				DefRange: tfdiags.SourceRange{},
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	reportDiagnosticsForTest(t, diags)
	if len(diags) != 0 {
		t.FailNow() // We reported the diags above/
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "child"},
				},
			),
			PlanApplyable: true,
			PlanComplete:  false,
			Action:        plans.Create,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
				stackaddrs.AbsComponent{
					Stack: stackaddrs.RootStackInstance,
					Item: stackaddrs.Component{
						Name: "self",
					},
				},
			),
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
			PlannedOutputValues: map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
			},
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
		},
		&stackplan.PlannedChangeDeferredResourceInstancePlanned{
			ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
				ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
					Component: stackaddrs.AbsComponentInstance{
						Stack: stackaddrs.RootStackInstance,
						Item: stackaddrs.ComponentInstance{
							Component: stackaddrs.Component{
								Name: "child",
							},
						},
					},
					Item: addrs.AbsResourceInstanceObject{
						ResourceInstance: addrs.AbsResourceInstance{
							Module: addrs.RootModuleInstance,
							Resource: addrs.ResourceInstance{
								Resource: addrs.Resource{
									Mode: addrs.ManagedResourceMode,
									Type: "testing_resource",
									Name: "data",
								},
								Key: addrs.NoKey,
							},
						},
					},
				},
				ChangeSrc: &plans.ResourceInstanceChangeSrc{
					Addr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					PrevRunAddr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					ProviderAddr: addrs.AbsProviderConfig{
						Module:   addrs.RootModule,
						Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
					},
					ChangeSrc: plans.ChangeSrc{
						Action: plans.Create,
						Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
						After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
							"id":    cty.UnknownVal(cty.String),
							"value": cty.UnknownVal(cty.String),
						}), stacks_testing_provider.TestingResourceSchema.Body),
						AfterSensitivePaths: nil,
					},
				},
				ProviderConfigAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
				Schema: stacks_testing_provider.TestingResourceSchema,
			},
			DeferredReason: providers.DeferredReasonDeferredPrereq,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
					Key:       addrs.WildcardKey,
				},
			),
			PlanApplyable: true, // TODO: Questionable? We only have outputs.
			PlanComplete:  false,
			Action:        plans.Create,
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
			},
			PlannedOutputValues: map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
			},
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
		},
		&stackplan.PlannedChangeDeferredResourceInstancePlanned{
			DeferredReason: providers.DeferredReasonDeferredPrereq,
			ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
				ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
					Component: stackaddrs.Absolute(
						stackaddrs.RootStackInstance,
						stackaddrs.ComponentInstance{
							Component: stackaddrs.Component{Name: "self"},
							Key:       addrs.WildcardKey,
						},
					),
					Item: addrs.AbsResourceInstanceObject{
						ResourceInstance: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "data",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					},
				},
				ProviderConfigAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
				ChangeSrc: &plans.ResourceInstanceChangeSrc{
					Addr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					PrevRunAddr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					ProviderAddr: addrs.AbsProviderConfig{
						Module:   addrs.RootModule,
						Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
					},
					ChangeSrc: plans.ChangeSrc{
						Action: plans.Create,
						Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
						After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
							"id":    cty.UnknownVal(cty.String),
							"value": cty.UnknownVal(cty.String),
						}), stacks_testing_provider.TestingResourceSchema.Body),
						AfterSensitivePaths: nil,
					},
				},
				Schema: stacks_testing_provider.TestingResourceSchema,
			},
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "components"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.UnknownVal(cty.Set(cty.String)),
		},
	}

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithDeferredComponentReferences(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input-and-output", "deferred-component-references"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks:    *lock,
		ForcePlanTimestamp: &fakePlanTimestamp,
		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "known_components"}: {
				Value:    cty.ListVal([]cty.Value{cty.StringVal("known")}),
				DefRange: tfdiags.SourceRange{},
			},
			{Name: "unknown_components"}: {
				Value:    cty.UnknownVal(cty.Set(cty.String)),
				DefRange: tfdiags.SourceRange{},
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	reportDiagnosticsForTest(t, diags)
	if len(diags) != 0 {
		t.FailNow() // We reported the diags above.
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "children"},
					Key:       addrs.WildcardKey,
				},
			),
			PlanApplyable: true, // TODO: Questionable? We only have outputs.
			PlanComplete:  false,
			Action:        plans.Create,
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.UnknownVal(cty.String)),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
			PlannedOutputValues: map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
			},
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
				stackaddrs.AbsComponent{
					Stack: stackaddrs.RootStackInstance,
					Item: stackaddrs.Component{
						Name: "self",
					},
				},
			),
		},
		&stackplan.PlannedChangeDeferredResourceInstancePlanned{
			DeferredReason: providers.DeferredReasonDeferredPrereq,
			ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
				ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
					Component: stackaddrs.Absolute(
						stackaddrs.RootStackInstance,
						stackaddrs.ComponentInstance{
							Component: stackaddrs.Component{Name: "children"},
							Key:       addrs.WildcardKey,
						},
					),
					Item: addrs.AbsResourceInstanceObject{
						ResourceInstance: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "data",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					},
				},
				ProviderConfigAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
				ChangeSrc: &plans.ResourceInstanceChangeSrc{
					Addr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					PrevRunAddr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					ProviderAddr: addrs.AbsProviderConfig{
						Module:   addrs.RootModule,
						Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
					},
					ChangeSrc: plans.ChangeSrc{
						Action: plans.Create,
						Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
						After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
							"id":    cty.UnknownVal(cty.String),
							"value": cty.UnknownVal(cty.String),
						}), stacks_testing_provider.TestingResourceSchema.Body),
						AfterSensitivePaths: nil,
					},
				},
				Schema: stacks_testing_provider.TestingResourceSchema,
			},
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
					Key:       addrs.StringKey("known"),
				}),
			PlanApplyable: true,
			PlanComplete:  true,
			Action:        plans.Create,
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("known")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
			PlannedOutputValues: map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
			},
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
				Component: stackaddrs.AbsComponentInstance{
					Stack: stackaddrs.RootStackInstance,
					Item: stackaddrs.ComponentInstance{
						Component: stackaddrs.Component{
							Name: "self",
						},
						Key: addrs.StringKey("known"),
					},
				},
				Item: addrs.AbsResourceInstanceObject{
					ResourceInstance: addrs.AbsResourceInstance{
						Module: addrs.RootModuleInstance,
						Resource: addrs.ResourceInstance{
							Resource: addrs.Resource{
								Mode: addrs.ManagedResourceMode,
								Type: "testing_resource",
								Name: "data",
							},
							Key: addrs.NoKey,
						},
					},
				},
			},
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr: addrs.AbsResourceInstance{
					Module: addrs.RootModuleInstance,
					Resource: addrs.ResourceInstance{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "data",
						},
						Key: addrs.NoKey,
					},
				},
				PrevRunAddr: addrs.AbsResourceInstance{
					Module: addrs.RootModuleInstance,
					Resource: addrs.ResourceInstance{
						Resource: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "data",
						},
						Key: addrs.NoKey,
					},
				},
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("known"),
					}), stacks_testing_provider.TestingResourceSchema.Body),
				},
				ProviderAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
			},
			ProviderConfigAddr: addrs.AbsProviderConfig{
				Module:   addrs.RootModule,
				Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
			},
			Schema: stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "known_components"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.SetVal([]cty.Value{cty.StringVal("known")}),
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "unknown_components"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.UnknownVal(cty.Set(cty.String)),
		},
	}

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithDeferredComponentForEachOfInvalidType(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "deferred-component-for-each-from-component-of-invalid-type")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks: *lock,

		ForcePlanTimestamp: &fakePlanTimestamp,

		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "components"}: {
				Value:    cty.UnknownVal(cty.Set(cty.String)),
				DefRange: tfdiags.SourceRange{},
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	go Plan(ctx, &req, &resp)
	_, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 1 {
		t.Fatalf("expected 1 diagnostic, got %d: %s", len(diags), diags)
	}

	if diags[0].Severity() != tfdiags.Error {
		t.Errorf("expected error diagnostic, got %q", diags[0].Severity())
	}

	expectedSummary := "Invalid for_each value"
	if diags[0].Description().Summary != expectedSummary {
		t.Errorf("expected diagnostic with summary %q, got %q", expectedSummary, diags[0].Description().Summary)
	}

	expectedDetail := "The for_each expression must produce either a map of any type or a set of strings. The keys of the map or the set elements will serve as unique identifiers for multiple instances of this component."
	if diags[0].Description().Detail != expectedDetail {
		t.Errorf("expected diagnostic with detail %q, got %q", expectedDetail, diags[0].Description().Detail)
	}
}

func TestPlanWithDeferredProviderForEach(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "deferred-provider-for-each"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange)
	diagsCh := make(chan tfdiags.Diagnostic)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks:    *lock,
		ForcePlanTimestamp: &fakePlanTimestamp,
		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "providers"}: {
				Value:    cty.UnknownVal(cty.Set(cty.String)),
				DefRange: tfdiags.SourceRange{},
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	reportDiagnosticsForTest(t, diags)
	if len(diags) != 0 {
		t.FailNow() // We reported the diags above
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "known"},
				}),
			PlanComplete:  false,
			PlanApplyable: false,
			Action:        plans.Create,
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("primary")),
			},
			PlannedOutputValues: map[string]cty.Value{},
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
		},
		&stackplan.PlannedChangeDeferredResourceInstancePlanned{
			ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
				ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
					Component: stackaddrs.Absolute(
						stackaddrs.RootStackInstance,
						stackaddrs.ComponentInstance{
							Component: stackaddrs.Component{Name: "known"},
						},
					),
					Item: addrs.AbsResourceInstanceObject{
						ResourceInstance: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "data",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					},
				},
				ProviderConfigAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
				ChangeSrc: &plans.ResourceInstanceChangeSrc{
					Addr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					PrevRunAddr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					ProviderAddr: addrs.AbsProviderConfig{
						Module:   addrs.RootModule,
						Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
					},
					ChangeSrc: plans.ChangeSrc{
						Action: plans.Create,
						Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
						After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
							"id":    cty.UnknownVal(cty.String),
							"value": cty.StringVal("primary"),
						}), stacks_testing_provider.TestingResourceSchema.Body),
					},
				},
				Schema: stacks_testing_provider.TestingResourceSchema,
			},
			DeferredReason: providers.DeferredReasonProviderConfigUnknown,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "unknown"},
					Key:       addrs.WildcardKey,
				}),
			PlanComplete:  false,
			PlanApplyable: false,
			Action:        plans.Create,
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("secondary")),
			},
			PlannedOutputValues: map[string]cty.Value{},
			PlannedCheckResults: &states.CheckResults{},
			PlanTimestamp:       fakePlanTimestamp,
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
		},
		&stackplan.PlannedChangeDeferredResourceInstancePlanned{
			ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
				ResourceInstanceObjectAddr: stackaddrs.AbsResourceInstanceObject{
					Component: stackaddrs.Absolute(
						stackaddrs.RootStackInstance,
						stackaddrs.ComponentInstance{
							Component: stackaddrs.Component{Name: "unknown"},
							Key:       addrs.WildcardKey,
						},
					),
					Item: addrs.AbsResourceInstanceObject{
						ResourceInstance: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "testing_resource",
							Name: "data",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					},
				},
				ProviderConfigAddr: addrs.AbsProviderConfig{
					Module:   addrs.RootModule,
					Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
				},
				ChangeSrc: &plans.ResourceInstanceChangeSrc{
					Addr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					PrevRunAddr: addrs.Resource{
						Mode: addrs.ManagedResourceMode,
						Type: "testing_resource",
						Name: "data",
					}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
					ProviderAddr: addrs.AbsProviderConfig{
						Module:   addrs.RootModule,
						Provider: addrs.MustParseProviderSourceString("hashicorp/testing"),
					},
					ChangeSrc: plans.ChangeSrc{
						Action: plans.Create,
						Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
						After: mustPlanDynamicValueSchema(cty.ObjectVal(map[string]cty.Value{
							"id":    cty.UnknownVal(cty.String),
							"value": cty.StringVal("secondary"),
						}), stacks_testing_provider.TestingResourceSchema.Body),
					},
				},
				Schema: stacks_testing_provider.TestingResourceSchema,
			},
			DeferredReason: providers.DeferredReasonProviderConfigUnknown,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr:   stackaddrs.InputVariable{Name: "providers"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.UnknownVal(cty.Set(cty.String)),
		},
	}

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanInvalidProvidersFailGracefully(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("invalid-providers"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange)
	diagsCh := make(chan tfdiags.Diagnostic)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks:    *lock,
		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	go Plan(ctx, &req, &resp)
	changes, diags := collectPlanOutput(changesCh, diagsCh)

	sort.SliceStable(diags, diagnosticSortFunc(diags))
	expectDiagnosticsForTest(t, diags,
		expectDiagnostic(tfdiags.Error, "Provider configuration is invalid", "Cannot plan changes for this resource because its associated provider configuration is invalid."),
		expectDiagnostic(tfdiags.Error, "invalid configuration", "configure_error attribute was set"))

	sort.SliceStable(changes, func(i, j int) bool {
		return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j])
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:              plans.Create,
			PlanTimestamp:       fakePlanTimestamp,
			PlannedInputValues:  make(map[string]plans.DynamicValue),
			PlannedOutputValues: make(map[string]cty.Value),
			PlannedCheckResults: &states.CheckResults{},
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
	}

	if diff := cmp.Diff(wantChanges, changes, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanWithStateManipulation(t *testing.T) {
	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)

	tcs := map[string]struct {
		state            *stackstate.State
		store            *stacks_testing_provider.ResourceStore
		inputs           map[string]cty.Value
		changes          []stackplan.PlannedChange
		counts           collections.Map[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]
		expectedWarnings []string
	}{
		"moved": {
			state: stackstate.NewStateBuilder().
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "moved",
							"value": "moved",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("moved", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("moved"),
					"value": cty.StringVal("moved"),
				})).
				Build(),
			changes: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:                mustAbsComponentInstance("component.self"),
					PlanApplyable:       true,
					PlanComplete:        true,
					Action:              plans.Update,
					PlannedInputValues:  make(map[string]plans.DynamicValue),
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					RequiredComponents:  collections.NewSet[stackaddrs.AbsComponent](),
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.after"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:         mustAbsResourceInstance("testing_resource.after"),
						PrevRunAddr:  mustAbsResourceInstance("testing_resource.before"),
						ProviderAddr: mustDefaultRootProvider("testing"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.NoOp,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("moved"),
								"value": cty.StringVal("moved"),
							})),
							After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("moved"),
								"value": cty.StringVal("moved"),
							})),
						},
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "moved",
							"value": "moved",
						}),
						Dependencies: make([]addrs.ConfigResource, 0),
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
			},
			counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
				collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
					K: mustAbsComponentInstance("component.self"),
					V: &hooks.ComponentInstanceChange{
						Addr: mustAbsComponentInstance("component.self"),
						Move: 1,
					},
				}),
		},
		"cross-type-moved": {
			state: stackstate.NewStateBuilder().
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.before")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "moved",
							"value": "moved",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("moved", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("moved"),
					"value": cty.StringVal("moved"),
				})).
				Build(),
			changes: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:                mustAbsComponentInstance("component.self"),
					PlanApplyable:       true,
					PlanComplete:        true,
					Action:              plans.Update,
					PlannedInputValues:  make(map[string]plans.DynamicValue),
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					RequiredComponents:  collections.NewSet[stackaddrs.AbsComponent](),
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_deferred_resource.after"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:         mustAbsResourceInstance("testing_deferred_resource.after"),
						PrevRunAddr:  mustAbsResourceInstance("testing_resource.before"),
						ProviderAddr: mustDefaultRootProvider("testing"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.NoOp,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":       cty.StringVal("moved"),
								"value":    cty.StringVal("moved"),
								"deferred": cty.False,
							})),
							After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":       cty.StringVal("moved"),
								"value":    cty.StringVal("moved"),
								"deferred": cty.False,
							})),
						},
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":       "moved",
							"value":    "moved",
							"deferred": false,
						}),
						Dependencies: make([]addrs.ConfigResource, 0),
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.DeferredResourceSchema,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
			},
			counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
				collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
					K: mustAbsComponentInstance("component.self"),
					V: &hooks.ComponentInstanceChange{
						Addr: mustAbsComponentInstance("component.self"),
						Move: 1,
					},
				}),
		},
		"import": {
			state: stackstate.NewStateBuilder().Build(), // We start with an empty state for this.
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("imported", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("imported"),
					"value": cty.StringVal("imported"),
				})).
				Build(),
			inputs: map[string]cty.Value{
				"id": cty.StringVal("imported"),
			},
			changes: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self"),
					PlanApplyable: true,
					PlanComplete:  true,
					// The component is still CREATE even though all the
					// instances are NoOps, because the component itself didn't
					// exist before even though all the resources might have.
					Action: plans.Create,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id": mustPlanDynamicValueDynamicType(cty.StringVal("imported")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"id": nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					RequiredComponents:  collections.NewSet[stackaddrs.AbsComponent](),
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.data"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:         mustAbsResourceInstance("testing_resource.data"),
						PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
						ProviderAddr: mustDefaultRootProvider("testing"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.NoOp,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("imported"),
								"value": cty.StringVal("imported"),
							})),
							After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("imported"),
								"value": cty.StringVal("imported"),
							})),
							Importing: &plans.ImportingSrc{
								ID: "imported",
							},
						},
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "imported",
							"value": "imported",
						}),
						Dependencies: make([]addrs.ConfigResource, 0),
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr: stackaddrs.InputVariable{
						Name: "id",
					},
					Action:          plans.Create,
					Before:          cty.NullVal(cty.DynamicPseudoType),
					After:           cty.StringVal("imported"),
					RequiredOnApply: false,
				},
			},
			counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
				collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
					K: mustAbsComponentInstance("component.self"),
					V: &hooks.ComponentInstanceChange{
						Addr:   mustAbsComponentInstance("component.self"),
						Import: 1,
					},
				}),
		},
		"removed": {
			state: stackstate.NewStateBuilder().
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self.testing_resource.resource")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "removed",
							"value": "removed",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("removed", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("removed"),
					"value": cty.StringVal("removed"),
				})).
				Build(),
			changes: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:                mustAbsComponentInstance("component.self"),
					PlanApplyable:       true,
					PlanComplete:        true,
					Action:              plans.Update,
					PlannedInputValues:  make(map[string]plans.DynamicValue),
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					RequiredComponents:  collections.NewSet[stackaddrs.AbsComponent](),
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource.resource"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:         mustAbsResourceInstance("testing_resource.resource"),
						PrevRunAddr:  mustAbsResourceInstance("testing_resource.resource"),
						ProviderAddr: mustDefaultRootProvider("testing"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Forget,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("removed"),
								"value": cty.StringVal("removed"),
							})),
							After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
								"id":    cty.String,
								"value": cty.String,
							}))),
						},
						ActionReason: plans.ResourceInstanceDeleteBecauseNoResourceConfig,
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "removed",
							"value": "removed",
						}),
						Dependencies: make([]addrs.ConfigResource, 0),
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
			},
			counts: collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange](
				collections.MapElem[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]{
					K: mustAbsComponentInstance("component.self"),
					V: &hooks.ComponentInstanceChange{
						Addr:   mustAbsComponentInstance("component.self"),
						Forget: 1,
					},
				}),
			expectedWarnings: []string{"Some objects will no longer be managed by Terraform"},
		},
	}

	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			ctx := context.Background()
			cfg := loadMainBundleConfigForTest(t, path.Join("state-manipulation", name))

			gotCounts := collections.NewMap[stackaddrs.AbsComponentInstance, *hooks.ComponentInstanceChange]()
			ctx = ContextWithHooks(ctx, &stackeval.Hooks{
				ReportComponentInstancePlanned: func(ctx context.Context, span any, change *hooks.ComponentInstanceChange) any {
					gotCounts.Put(change.Addr, change)
					return span
				},
			})

			inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs))
			for name, input := range tc.inputs {
				inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{
					Value: input,
				}
			}

			changesCh := make(chan stackplan.PlannedChange)
			diagsCh := make(chan tfdiags.Diagnostic)
			req := PlanRequest{
				Config: cfg,
				ProviderFactories: map[addrs.Provider]providers.Factory{
					addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProviderWithData(t, tc.store), nil
					},
				},
				DependencyLocks:    *lock,
				InputValues:        inputs,
				ForcePlanTimestamp: &fakePlanTimestamp,
				PrevState:          tc.state,
			}
			resp := PlanResponse{
				PlannedChanges: changesCh,
				Diagnostics:    diagsCh,
			}
			go Plan(ctx, &req, &resp)
			changes, diags := collectPlanOutput(changesCh, diagsCh)

			reportDiagnosticsForTest(t, diags)
			if len(diags) > len(tc.expectedWarnings) {
				t.Fatalf("had unexpected warnings")
			}
			for i, diag := range diags {
				if diag.Description().Summary != tc.expectedWarnings[i] {
					t.Fatalf("expected diagnostic with summary %q, got %q", tc.expectedWarnings[i], diag.Description().Summary)
				}
			}

			sort.SliceStable(changes, func(i, j int) bool {
				return plannedChangeSortKey(changes[i]) < plannedChangeSortKey(changes[j])
			})

			if diff := cmp.Diff(tc.changes, changes, changesCmpOpts); diff != "" {
				t.Errorf("wrong changes\n%s", diff)
			}

			wantCounts := tc.counts
			for key, elem := range wantCounts.All() {
				// First, make sure everything we wanted is present.
				if !gotCounts.HasKey(key) {
					t.Errorf("wrong counts: wanted %s but didn't get it", key)
				}

				// And that the values actually match.
				got, want := gotCounts.Get(key), elem
				if diff := cmp.Diff(want, got); diff != "" {
					t.Errorf("wrong counts for %s: %s", want.Addr, diff)
				}

			}

			for key := range gotCounts.All() {
				// Then, make sure we didn't get anything we didn't want.
				if !wantCounts.HasKey(key) {
					t.Errorf("wrong counts: got %s but didn't want it", key)
				}
			}
		})
	}
}

func TestPlan_plantimestamp_force_timestamp(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "with-plantimestamp")

	forcedPlanTimestamp := "1991-08-25T20:57:08Z"
	fakePlanTimestamp, err := time.Parse(time.RFC3339, forcedPlanTimestamp)
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			// We support both hashicorp/testing and
			// terraform.io/builtin/testing as providers. This lets us
			// test the provider aliasing feature. Both providers
			// support the same set of resources and data sources.
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
			addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
			return map[stackaddrs.InputVariable]ExternalInputValue{}
		}(),
		ForcePlanTimestamp: &fakePlanTimestamp,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	// The following will fail the test if there are any error
	// diagnostics.
	reportDiagnosticsForTest(t, diags)

	// We also want to fail if there are just warnings, since the
	// configurations here are supposed to be totally problem-free.
	if len(diags) != 0 {
		// reportDiagnosticsForTest already showed the diagnostics in
		// the log
		t.FailNow()
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "second-self"},
				},
			),
			Action:              plans.Create,
			PlanApplyable:       true,
			PlanComplete:        true,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"value": nil,
			},
			PlannedInputValues: map[string]plans.DynamicValue{
				"value": mustPlanDynamicValueDynamicType(cty.StringVal(forcedPlanTimestamp)),
			},
			PlannedOutputValues: map[string]cty.Value{
				"input": cty.StringVal(forcedPlanTimestamp),
				"out":   cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:              plans.Create,
			PlanApplyable:       true,
			PlanComplete:        true,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"value": nil,
			},
			PlannedInputValues: map[string]plans.DynamicValue{
				"value": mustPlanDynamicValueDynamicType(cty.StringVal(forcedPlanTimestamp)),
			},
			PlannedOutputValues: map[string]cty.Value{
				"input": cty.StringVal(forcedPlanTimestamp),
				"out":   cty.StringVal(fmt.Sprintf("module-output-%s", forcedPlanTimestamp)),
			},
			PlanTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangeOutputValue{
			Addr:   stackaddrs.OutputValue{Name: "plantimestamp"},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.StringVal(forcedPlanTimestamp),
		},
		&stackplan.PlannedChangePlannedTimestamp{PlannedTimestamp: fakePlanTimestamp},
	}

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlan_plantimestamp_later_than_when_writing_this_test(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "with-plantimestamp")

	dayOfWritingThisTest := "2024-06-21T06:37:08Z"
	dayOfWritingThisTestTime, err := time.Parse(time.RFC3339, dayOfWritingThisTest)
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			// We support both hashicorp/testing and
			// terraform.io/builtin/testing as providers. This lets us
			// test the provider aliasing feature. Both providers
			// support the same set of resources and data sources.
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
			addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
			return map[stackaddrs.InputVariable]ExternalInputValue{}
		}(),
		ForcePlanTimestamp: nil, // This is what we want to test
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	changes, diags := collectPlanOutput(changesCh, diagsCh)
	output := expectOutput(t, "plantimestamp", changes)

	plantimestampValue := output.After
	plantimestamp, err := time.Parse(time.RFC3339, plantimestampValue.AsString())
	if err != nil {
		t.Fatal(err)
	}

	if plantimestamp.Before(dayOfWritingThisTestTime) {
		t.Errorf("expected plantimestamp to be later than %q, got %q", dayOfWritingThisTest, plantimestampValue.AsString())
	}

	// The following will fail the test if there are any error
	// diagnostics.
	reportDiagnosticsForTest(t, diags)

	// We also want to fail if there are just warnings, since the
	// configurations here are supposed to be totally problem-free.
	if len(diags) != 0 {
		// reportDiagnosticsForTest already showed the diagnostics in
		// the log
		t.FailNow()
	}
}

func TestPlan_DependsOnUpdatesRequirements(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, path.Join("with-single-input", "depends-on"))

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

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

	changesCh := make(chan stackplan.PlannedChange)
	diagsCh := make(chan tfdiags.Diagnostic)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks:    *lock,
		ForcePlanTimestamp: &fakePlanTimestamp,
		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "input"}: {
				Value: cty.StringVal("hello, world!"),
			},
		},
	}

	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	reportDiagnosticsForTest(t, diags)
	if len(diags) != 0 {
		t.FailNow()
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr:               mustAbsComponentInstance("component.first"),
			PlanApplyable:      true,
			PlanComplete:       true,
			Action:             plans.Create,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](),
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
			PlanTimestamp:       fakePlanTimestamp,
			PlannedOutputValues: make(map[string]cty.Value),
			PlannedCheckResults: &states.CheckResults{},
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.first.testing_resource.data"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:         mustAbsResourceInstance("testing_resource.data"),
				PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
				ProviderAddr: mustDefaultRootProvider("testing"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
						"id":    cty.String,
						"value": cty.String,
					}))),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("hello, world!"),
					})),
				},
			},
			ProviderConfigAddr: mustDefaultRootProvider("testing"),
			Schema:             stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr:          mustAbsComponentInstance("component.second"),
			PlanApplyable: true,
			PlanComplete:  true,
			Action:        plans.Create,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
				mustAbsComponent("component.first"),
				mustAbsComponent("stack.second.component.self"),
			),
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
			PlanTimestamp:       fakePlanTimestamp,
			PlannedOutputValues: make(map[string]cty.Value),
			PlannedCheckResults: &states.CheckResults{},
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.second.testing_resource.data"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:         mustAbsResourceInstance("testing_resource.data"),
				PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
				ProviderAddr: mustDefaultRootProvider("testing"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
						"id":    cty.String,
						"value": cty.String,
					}))),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("hello, world!"),
					})),
				},
			},
			ProviderConfigAddr: mustDefaultRootProvider("testing"),
			Schema:             stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr:          mustAbsComponentInstance("stack.first.component.self"),
			PlanApplyable: true,
			PlanComplete:  true,
			Action:        plans.Create,
			RequiredComponents: collections.NewSet[stackaddrs.AbsComponent](
				mustAbsComponent("component.first"),
				mustAbsComponent("component.empty"),
			),
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
			PlanTimestamp:       fakePlanTimestamp,
			PlannedOutputValues: make(map[string]cty.Value),
			PlannedCheckResults: &states.CheckResults{},
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.first.component.self.testing_resource.data"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:         mustAbsResourceInstance("testing_resource.data"),
				PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
				ProviderAddr: mustDefaultRootProvider("testing"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
						"id":    cty.String,
						"value": cty.String,
					}))),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("hello, world!"),
					})),
				},
			},
			ProviderConfigAddr: mustDefaultRootProvider("testing"),
			Schema:             stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr:          mustAbsComponentInstance("stack.second.component.self"),
			PlanApplyable: true,
			PlanComplete:  true,
			Action:        plans.Create,
			PlannedInputValues: map[string]plans.DynamicValue{
				"id":    mustPlanDynamicValueDynamicType(cty.NullVal(cty.String)),
				"input": mustPlanDynamicValueDynamicType(cty.StringVal("hello, world!")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{
				"id":    nil,
				"input": nil,
			},
			PlanTimestamp:       fakePlanTimestamp,
			PlannedOutputValues: make(map[string]cty.Value),
			PlannedCheckResults: &states.CheckResults{},
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("stack.second.component.self.testing_resource.data"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:         mustAbsResourceInstance("testing_resource.data"),
				PrevRunAddr:  mustAbsResourceInstance("testing_resource.data"),
				ProviderAddr: mustDefaultRootProvider("testing"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
						"id":    cty.String,
						"value": cty.String,
					}))),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.UnknownVal(cty.String),
						"value": cty.StringVal("hello, world!"),
					})),
				},
			},
			ProviderConfigAddr: mustDefaultRootProvider("testing"),
			Schema:             stacks_testing_provider.TestingResourceSchema,
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr: stackaddrs.InputVariable{
				Name: "empty",
			},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.SetValEmpty(cty.String),
		},
		&stackplan.PlannedChangeRootInputValue{
			Addr: stackaddrs.InputVariable{
				Name: "input",
			},
			Action: plans.Create,
			Before: cty.NullVal(cty.DynamicPseudoType),
			After:  cty.StringVal("hello, world!"),
		},
	}

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlan_RemovedBlocks(t *testing.T) {
	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

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

	tcs := map[string]struct {
		source          string
		initialState    *stackstate.State
		store           *stacks_testing_provider.ResourceStore
		inputs          map[string]cty.Value
		wantPlanChanges []stackplan.PlannedChange
		wantPlanDiags   []expectedDiagnostic
	}{
		"unknown removed block with nothing to remove": {
			source: filepath.Join("with-single-input", "removed-component-instance"),
			initialState: stackstate.NewStateBuilder().
				// we have a single component instance in state
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
					AddInputVariable("id", cty.StringVal("a")).
					AddInputVariable("input", cty.StringVal("a"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "a",
							"value": "a",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("a", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("a"),
					"value": cty.StringVal("a"),
				})).
				Build(),
			inputs: map[string]cty.Value{
				"input": cty.SetVal([]cty.Value{
					cty.StringVal("a"),
				}),
				"removed": cty.UnknownVal(cty.Set(cty.String)),
			},
			wantPlanChanges: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"a\"]"),
					PlanComplete:  true,
					PlanApplyable: false, // all changes are no-ops
					Action:        plans.Update,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("a")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:        mustAbsResourceInstance("testing_resource.data"),
						PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.NoOp,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("a"),
								"value": cty.StringVal("a"),
							})),
							After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("a"),
								"value": cty.StringVal("a"),
							})),
						},
						ProviderAddr: mustDefaultRootProvider("testing"),
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "a",
							"value": "a",
						}),
						Status:       states.ObjectReady,
						Dependencies: make([]addrs.ConfigResource, 0),
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "input"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After: cty.SetVal([]cty.Value{
						cty.StringVal("a"),
					}),
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "removed"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.UnknownVal(cty.Set(cty.String)),
				},
			},
		},
		"unknown removed block with elements in state": {
			source: filepath.Join("with-single-input", "removed-component-instance"),
			initialState: stackstate.NewStateBuilder().
				// we have a single component instance in state
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
					AddInputVariable("id", cty.StringVal("a")).
					AddInputVariable("input", cty.StringVal("a"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "a",
							"value": "a",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("a", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("a"),
					"value": cty.StringVal("a"),
				})).
				Build(),
			inputs: map[string]cty.Value{
				"input":   cty.SetValEmpty(cty.String),
				"removed": cty.UnknownVal(cty.Set(cty.String)),
			},
			wantPlanChanges: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"a\"]"),
					PlanComplete:  false, // has deferred changes
					PlanApplyable: false, // only deferred changes
					Action:        plans.Delete,
					Mode:          plans.DestroyMode,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("a")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeDeferredResourceInstancePlanned{
					ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
						ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
						ChangeSrc: &plans.ResourceInstanceChangeSrc{
							Addr:        mustAbsResourceInstance("testing_resource.data"),
							PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
							ChangeSrc: plans.ChangeSrc{
								Action: plans.Delete,
								Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
									"id":    cty.StringVal("a"),
									"value": cty.StringVal("a"),
								})),
								After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
									"id":    cty.String,
									"value": cty.String,
								}))),
							},
							ProviderAddr: mustDefaultRootProvider("testing"),
						},
						PriorStateSrc: &states.ResourceInstanceObjectSrc{
							AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
								"id":    "a",
								"value": "a",
							}),
							Status:       states.ObjectReady,
							Dependencies: make([]addrs.ConfigResource, 0),
						},
						ProviderConfigAddr: mustDefaultRootProvider("testing"),
						Schema:             stacks_testing_provider.TestingResourceSchema,
					},
					DeferredReason: providers.DeferredReasonDeferredPrereq,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "input"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.SetValEmpty(cty.String),
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "removed"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.UnknownVal(cty.Set(cty.String)),
				},
			},
		},
		"unknown component block with element to remove": {
			source: filepath.Join("with-single-input", "removed-component-instance"),
			initialState: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
					AddInputVariable("id", cty.StringVal("a")).
					AddInputVariable("input", cty.StringVal("a"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "a",
							"value": "a",
						}),
					})).
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"b\"]")).
					AddInputVariable("id", cty.StringVal("b")).
					AddInputVariable("input", cty.StringVal("b"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"b\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "b",
							"value": "b",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("a", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("a"),
					"value": cty.StringVal("a"),
				})).
				AddResource("b", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("b"),
					"value": cty.StringVal("b"),
				})).
				Build(),
			inputs: map[string]cty.Value{
				"input":   cty.UnknownVal(cty.Set(cty.String)),
				"removed": cty.SetVal([]cty.Value{cty.StringVal("b")}),
			},
			wantPlanChanges: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"a\"]"),
					PlanComplete:  false, // has deferred changes
					PlanApplyable: false, // only deferred changes
					Action:        plans.Update,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("a")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeDeferredResourceInstancePlanned{
					ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
						ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
						ChangeSrc: &plans.ResourceInstanceChangeSrc{
							Addr:        mustAbsResourceInstance("testing_resource.data"),
							PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
							ChangeSrc: plans.ChangeSrc{
								Action: plans.NoOp,
								Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
									"id":    cty.StringVal("a"),
									"value": cty.StringVal("a"),
								})),
								After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
									"id":    cty.StringVal("a"),
									"value": cty.StringVal("a"),
								})),
							},
							ProviderAddr: mustDefaultRootProvider("testing"),
						},
						PriorStateSrc: &states.ResourceInstanceObjectSrc{
							AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
								"id":    "a",
								"value": "a",
							}),
							Status:       states.ObjectReady,
							Dependencies: make([]addrs.ConfigResource, 0),
						},
						ProviderConfigAddr: mustDefaultRootProvider("testing"),
						Schema:             stacks_testing_provider.TestingResourceSchema,
					},
					DeferredReason: providers.DeferredReasonDeferredPrereq,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"b\"]"),
					PlanComplete:  true,
					PlanApplyable: true,
					Action:        plans.Delete,
					Mode:          plans.DestroyMode,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("b")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("b")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"b\"].testing_resource.data"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:        mustAbsResourceInstance("testing_resource.data"),
						PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Delete,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("b"),
								"value": cty.StringVal("b"),
							})),
							After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
								"id":    cty.String,
								"value": cty.String,
							}))),
						},
						ProviderAddr: mustDefaultRootProvider("testing"),
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
							"id":    "b",
							"value": "b",
						}),
						Status:       states.ObjectReady,
						Dependencies: make([]addrs.ConfigResource, 0),
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "input"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.UnknownVal(cty.Set(cty.String)),
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "removed"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.SetVal([]cty.Value{cty.StringVal("b")}),
				},
			},
		},
		"unknown component and removed block with element in state": {
			source: filepath.Join("with-single-input", "removed-component-instance"),
			initialState: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
					AddInputVariable("id", cty.StringVal("a")).
					AddInputVariable("input", cty.StringVal("a"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "a",
							"value": "a",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("a", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("a"),
					"value": cty.StringVal("a"),
				})).
				Build(),
			inputs: map[string]cty.Value{
				"input":   cty.UnknownVal(cty.Set(cty.String)),
				"removed": cty.UnknownVal(cty.Set(cty.String)),
			},
			wantPlanChanges: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"a\"]"),
					PlanComplete:  false, // has deferred changes
					PlanApplyable: false, // only deferred changes
					Action:        plans.Update,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("a")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeDeferredResourceInstancePlanned{
					ResourceInstancePlanned: stackplan.PlannedChangeResourceInstancePlanned{
						ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
						ChangeSrc: &plans.ResourceInstanceChangeSrc{
							Addr:        mustAbsResourceInstance("testing_resource.data"),
							PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
							ChangeSrc: plans.ChangeSrc{
								Action: plans.NoOp,
								Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
									"id":    cty.StringVal("a"),
									"value": cty.StringVal("a"),
								})),
								After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
									"id":    cty.StringVal("a"),
									"value": cty.StringVal("a"),
								})),
							},
							ProviderAddr: mustDefaultRootProvider("testing"),
						},
						PriorStateSrc: &states.ResourceInstanceObjectSrc{
							AttrsJSON: mustMarshalJSONAttrs(map[string]interface{}{
								"id":    "a",
								"value": "a",
							}),
							Status:       states.ObjectReady,
							Dependencies: make([]addrs.ConfigResource, 0),
						},
						ProviderConfigAddr: mustDefaultRootProvider("testing"),
						Schema:             stacks_testing_provider.TestingResourceSchema,
					},
					DeferredReason: providers.DeferredReasonDeferredPrereq,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "input"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.UnknownVal(cty.Set(cty.String)),
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "removed"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After:  cty.UnknownVal(cty.Set(cty.String)),
				},
			},
		},
		"absent component": {
			source: filepath.Join("with-single-input", "removed-component"),
			wantPlanChanges: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
			},
		},
		"absent component instance": {
			source: filepath.Join("with-single-input", "removed-component-instance"),
			initialState: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")).
					AddInputVariable("id", cty.StringVal("a")).
					AddInputVariable("input", cty.StringVal("a"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "a",
							"value": "a",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("a", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("a"),
					"value": cty.StringVal("a"),
				})).
				Build(),
			inputs: map[string]cty.Value{
				"input": cty.SetVal([]cty.Value{
					cty.StringVal("a"),
				}),
				"removed": cty.SetVal([]cty.Value{
					cty.StringVal("b"), // Doesn't exist!
				}),
			},
			wantPlanChanges: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: true,
				},
				// we're expecting the new component to be created
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"a\"]"),
					PlanComplete:  true,
					PlanApplyable: false, // no changes
					Action:        plans.Update,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("a")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:        mustAbsResourceInstance("testing_resource.data"),
						PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.NoOp,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("a"),
								"value": cty.StringVal("a"),
							})),
							After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("a"),
								"value": cty.StringVal("a"),
							})),
						},
						ProviderAddr: mustDefaultRootProvider("testing"),
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "a",
							"value": "a",
						}),
						Dependencies: make([]addrs.ConfigResource, 0),
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeComponentInstanceRemoved{
					Addr: mustAbsComponentInstance("component.self[\"removed\"]"),
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "input"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After: cty.SetVal([]cty.Value{
						cty.StringVal("a"),
					}),
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "removed"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After: cty.SetVal([]cty.Value{
						cty.StringVal("b"),
					}),
				},
			},
		},
		"orphaned component": {
			source: filepath.Join("with-single-input", "removed-component-instance"),
			initialState: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"removed\"]")).
					AddInputVariable("id", cty.StringVal("removed")).
					AddInputVariable("input", cty.StringVal("removed"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "removed",
							"value": "removed",
						}),
					})).
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"orphaned\"]")).
					AddInputVariable("id", cty.StringVal("orphaned")).
					AddInputVariable("input", cty.StringVal("orphaned"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"orphaned\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "orphaned",
							"value": "orphaned",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("removed", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("removed"),
					"value": cty.StringVal("removed"),
				})).
				AddResource("orphaned", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("orphaned"),
					"value": cty.StringVal("orphaned"),
				})).
				Build(),
			inputs: map[string]cty.Value{
				"input": cty.SetVal([]cty.Value{
					cty.StringVal("added"),
				}),
				"removed": cty.SetVal([]cty.Value{
					cty.StringVal("removed"),
				}),
			},
			wantPlanChanges: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: false, // No! We have an unclaimed instance!
				},
				// we're expecting the new component to be created
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"added\"]"),
					PlanComplete:  true,
					PlanApplyable: true,
					Action:        plans.Create,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("added")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("added")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"added\"].testing_resource.data"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:        mustAbsResourceInstance("testing_resource.data"),
						PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Create,
							Before: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
								"id":    cty.String,
								"value": cty.String,
							}))),
							After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("added"),
								"value": cty.StringVal("added"),
							})),
						},
						ProviderAddr: mustDefaultRootProvider("testing"),
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"removed\"]"),
					PlanComplete:  true,
					PlanApplyable: true,
					Mode:          plans.DestroyMode,
					Action:        plans.Delete,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("removed")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("removed")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"removed\"].testing_resource.data"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:        mustAbsResourceInstance("testing_resource.data"),
						PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.Delete,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("removed"),
								"value": cty.StringVal("removed"),
							})),
							After: mustPlanDynamicValue(cty.NullVal(cty.Object(map[string]cty.Type{
								"id":    cty.String,
								"value": cty.String,
							}))),
						},
						ProviderAddr: mustDefaultRootProvider("testing"),
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "removed",
							"value": "removed",
						}),
						Dependencies: make([]addrs.ConfigResource, 0),
						Status:       states.ObjectReady,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "input"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After: cty.SetVal([]cty.Value{
						cty.StringVal("added"),
					}),
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "removed"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After: cty.SetVal([]cty.Value{
						cty.StringVal("removed"),
					}),
				},
			},
			wantPlanDiags: []expectedDiagnostic{
				{
					severity: tfdiags.Error,
					summary:  "Unclaimed component instance",
					detail:   "The component instance component.self[\"orphaned\"] is not claimed by any component or removed block in the configuration. Make sure it is instantiated by a component block, or targeted for removal by a removed block.",
				},
			},
		},
		"duplicate component": {
			source: filepath.Join("with-single-input", "removed-component-instance"),
			initialState: stackstate.NewStateBuilder().
				AddComponentInstance(stackstate.NewComponentInstanceBuilder(mustAbsComponentInstance("component.self[\"a\"]")).
					AddInputVariable("id", cty.StringVal("a")).
					AddInputVariable("input", cty.StringVal("a"))).
				AddResourceInstance(stackstate.NewResourceInstanceBuilder().
					SetAddr(mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data")).
					SetProviderAddr(mustDefaultRootProvider("testing")).
					SetResourceInstanceObjectSrc(states.ResourceInstanceObjectSrc{
						Status: states.ObjectReady,
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "a",
							"value": "a",
						}),
					})).
				Build(),
			store: stacks_testing_provider.NewResourceStoreBuilder().
				AddResource("a", cty.ObjectVal(map[string]cty.Value{
					"id":    cty.StringVal("a"),
					"value": cty.StringVal("a"),
				})).
				Build(),
			inputs: map[string]cty.Value{
				"input": cty.SetVal([]cty.Value{
					cty.StringVal("a"),
				}),
				"removed": cty.SetVal([]cty.Value{
					cty.StringVal("a"),
				}),
			},
			wantPlanChanges: []stackplan.PlannedChange{
				&stackplan.PlannedChangeApplyable{
					Applyable: false, // No! The removed block is a duplicate of the component!
				},
				&stackplan.PlannedChangeComponentInstance{
					Addr:          mustAbsComponentInstance("component.self[\"a\"]"),
					PlanComplete:  true,
					PlanApplyable: false, // no changes
					Action:        plans.Update,
					PlannedInputValues: map[string]plans.DynamicValue{
						"id":    mustPlanDynamicValueDynamicType(cty.StringVal("a")),
						"input": mustPlanDynamicValueDynamicType(cty.StringVal("a")),
					},
					PlannedInputValueMarks: map[string][]cty.PathValueMarks{
						"input": nil,
						"id":    nil,
					},
					PlannedOutputValues: make(map[string]cty.Value),
					PlannedCheckResults: &states.CheckResults{},
					PlanTimestamp:       fakePlanTimestamp,
				},
				&stackplan.PlannedChangeResourceInstancePlanned{
					ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self[\"a\"].testing_resource.data"),
					ChangeSrc: &plans.ResourceInstanceChangeSrc{
						Addr:        mustAbsResourceInstance("testing_resource.data"),
						PrevRunAddr: mustAbsResourceInstance("testing_resource.data"),
						ChangeSrc: plans.ChangeSrc{
							Action: plans.NoOp,
							Before: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("a"),
								"value": cty.StringVal("a"),
							})),
							After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
								"id":    cty.StringVal("a"),
								"value": cty.StringVal("a"),
							})),
						},
						ProviderAddr: mustDefaultRootProvider("testing"),
					},
					PriorStateSrc: &states.ResourceInstanceObjectSrc{
						AttrsJSON: mustMarshalJSONAttrs(map[string]any{
							"id":    "a",
							"value": "a",
						}),
						Dependencies: make([]addrs.ConfigResource, 0),
						Status:       states.ObjectReady,
					},
					ProviderConfigAddr: mustDefaultRootProvider("testing"),
					Schema:             stacks_testing_provider.TestingResourceSchema,
				},
				&stackplan.PlannedChangeHeader{
					TerraformVersion: version.SemVer,
				},
				&stackplan.PlannedChangePlannedTimestamp{
					PlannedTimestamp: fakePlanTimestamp,
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "input"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After: cty.SetVal([]cty.Value{
						cty.StringVal("a"),
					}),
				},
				&stackplan.PlannedChangeRootInputValue{
					Addr:   stackaddrs.InputVariable{Name: "removed"},
					Action: plans.Create,
					Before: cty.NullVal(cty.DynamicPseudoType),
					After: cty.SetVal([]cty.Value{
						cty.StringVal("a"),
					}),
				},
			},
			wantPlanDiags: []expectedDiagnostic{
				{
					severity: tfdiags.Error,
					summary:  "Cannot remove component instance",
					detail:   "The component instance component.self[\"a\"] is targeted by a component block and cannot be removed. The relevant component is defined at git::https://example.com/test.git//with-single-input/removed-component-instance/removed-component-instance.tfcomponent.hcl:18,1-17.",
				},
			},
		},
	}

	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			ctx := context.Background()
			cfg := loadMainBundleConfigForTest(t, tc.source)

			inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.inputs))
			for name, input := range tc.inputs {
				inputs[stackaddrs.InputVariable{Name: name}] = ExternalInputValue{
					Value: input,
				}
			}

			providers := map[addrs.Provider]providers.Factory{
				addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
					return stacks_testing_provider.NewProviderWithData(t, tc.store), nil
				},
			}

			planChangesCh := make(chan stackplan.PlannedChange)
			planDiagsCh := make(chan tfdiags.Diagnostic)
			planReq := PlanRequest{
				Config:             cfg,
				ProviderFactories:  providers,
				InputValues:        inputs,
				ForcePlanTimestamp: &fakePlanTimestamp,
				PrevState:          tc.initialState,
				DependencyLocks:    *lock,
			}
			planResp := PlanResponse{
				PlannedChanges: planChangesCh,
				Diagnostics:    planDiagsCh,
			}
			go Plan(ctx, &planReq, &planResp)
			gotPlanChanges, gotPlanDiags := collectPlanOutput(planChangesCh, planDiagsCh)

			sort.SliceStable(gotPlanChanges, func(i, j int) bool {
				return plannedChangeSortKey(gotPlanChanges[i]) < plannedChangeSortKey(gotPlanChanges[j])
			})
			sort.SliceStable(gotPlanDiags, diagnosticSortFunc(gotPlanDiags))

			expectDiagnosticsForTest(t, gotPlanDiags, tc.wantPlanDiags...)
			if diff := cmp.Diff(tc.wantPlanChanges, gotPlanChanges, changesCmpOpts); diff != "" {
				t.Errorf("wrong changes\n%s", diff)
			}
		})
	}
}

func TestPlanWithResourceIdentities(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "resource-identity")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

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

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config:             cfg,
		ForcePlanTimestamp: &fakePlanTimestamp,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks: *lock,
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	if len(diags) != 0 {
		t.Errorf("unexpected diagnostics\n%s", diags.ErrWithWarnings().Error())
	}

	wantChanges := []stackplan.PlannedChange{
		&stackplan.PlannedChangeApplyable{
			Applyable: true,
		},
		&stackplan.PlannedChangeComponentInstance{
			Addr: stackaddrs.Absolute(
				stackaddrs.RootStackInstance,
				stackaddrs.ComponentInstance{
					Component: stackaddrs.Component{Name: "self"},
				},
			),
			Action:              plans.Create,
			PlanApplyable:       true,
			PlanComplete:        true,
			PlannedCheckResults: &states.CheckResults{},
			PlannedInputValues: map[string]plans.DynamicValue{
				"name": mustPlanDynamicValueDynamicType(cty.StringVal("example")),
			},
			PlannedInputValueMarks: map[string][]cty.PathValueMarks{"name": nil},
			PlannedOutputValues:    map[string]cty.Value{},
			PlanTimestamp:          fakePlanTimestamp,
		},
		&stackplan.PlannedChangeResourceInstancePlanned{
			ResourceInstanceObjectAddr: mustAbsResourceInstanceObject("component.self.testing_resource_with_identity.hello"),
			ChangeSrc: &plans.ResourceInstanceChangeSrc{
				Addr:         mustAbsResourceInstance("testing_resource_with_identity.hello"),
				PrevRunAddr:  mustAbsResourceInstance("testing_resource_with_identity.hello"),
				ProviderAddr: mustDefaultRootProvider("testing"),
				ChangeSrc: plans.ChangeSrc{
					Action: plans.Create,
					Before: mustPlanDynamicValue(cty.NullVal(cty.DynamicPseudoType)),
					After: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id":    cty.StringVal("example"),
						"value": cty.NullVal(cty.String),
					})),
					AfterIdentity: mustPlanDynamicValue(cty.ObjectVal(map[string]cty.Value{
						"id": cty.StringVal("id:example"),
					})),
				},
			},
			ProviderConfigAddr: mustDefaultRootProvider("testing"),
			Schema:             stacks_testing_provider.TestingResourceWithIdentitySchema,
		},
		&stackplan.PlannedChangeHeader{
			TerraformVersion: version.SemVer,
		},
		&stackplan.PlannedChangePlannedTimestamp{
			PlannedTimestamp: fakePlanTimestamp,
		},
	}
	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	if diff := cmp.Diff(wantChanges, gotChanges, changesCmpOpts); diff != "" {
		t.Errorf("wrong changes\n%s", diff)
	}
}

func TestPlanInvalidLocalValue(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "invalid-local")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

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

	changesCh := make(chan stackplan.PlannedChange, 8)
	diagsCh := make(chan tfdiags.Diagnostic, 2)
	req := PlanRequest{
		Config:             cfg,
		ForcePlanTimestamp: &fakePlanTimestamp,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks: *lock,
		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "in"}: {
				Value: cty.ObjectVal(map[string]cty.Value{"name": cty.StringVal("foo")}),
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}

	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	tfdiags.AssertDiagnosticsMatch(t, diags, tfdiags.Diagnostics{}.Append(&hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  "Invalid operand",
		Detail:   "Unsuitable value for left operand: a number is required.",
		Subject: &hcl.Range{
			Filename: "git::https://example.com/test.git//invalid-local/invalid-local.tfcomponent.hcl",
			Start:    hcl.Pos{Line: 19, Column: 49, Byte: 377},
			End:      hcl.Pos{Line: 19, Column: 50, Byte: 378},
		},
		Context: &hcl.Range{
			Filename: "git::https://example.com/test.git//invalid-local/invalid-local.tfcomponent.hcl",
			Start:    hcl.Pos{Line: 19, Column: 49, Byte: 377},
			End:      hcl.Pos{Line: 19, Column: 54, Byte: 382},
		},
	}))

	// We don't really care about the precise content of the plan changes here,
	// we just want to ensure that the produced plan is not applyable
	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	pca, ok := gotChanges[0].(*stackplan.PlannedChangeApplyable)
	if !ok {
		t.Fatalf("expected first change to be PlannedChangeApplyable, got %T", gotChanges[0])
	}
	if pca.Applyable {
		t.Fatalf("expected plan to be not applyable due to invalid local value, but it is applyable")
	}
}

// collectPlanOutput consumes the two output channels emitting results from
// a call to [Plan], and collects all of the data written to them before
// returning once changesCh has been closed by the sender to indicate that
// the planning process is complete.
func collectPlanOutput(changesCh <-chan stackplan.PlannedChange, diagsCh <-chan tfdiags.Diagnostic) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
	var changes []stackplan.PlannedChange
	var diags tfdiags.Diagnostics

	for {
		select {
		case change, ok := <-changesCh:
			if !ok {
				// The plan operation is complete but we might still have
				// some buffered diagnostics to consume.
				if diagsCh != nil {
					for diag := range diagsCh {
						diags = append(diags, diag)
					}
				}
				return changes, diags
			}
			changes = append(changes, change)
		case diag, ok := <-diagsCh:
			if !ok {
				// no more diagnostics to read
				diagsCh = nil
				continue
			}
			diags = append(diags, diag)
		}
	}
}

func expectOutput(t *testing.T, name string, changes []stackplan.PlannedChange) *stackplan.PlannedChangeOutputValue {
	t.Helper()
	for _, change := range changes {
		if v, ok := change.(*stackplan.PlannedChangeOutputValue); ok && v.Addr.Name == name {
			return v
		}
	}

	t.Fatalf("expected output value %q", name)
	return nil
}

func TestPlanWithActionInvocationHooks(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "planning-action-lifecycle")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
	if err != nil {
		t.Fatal(err)
	}

	testCtx := TestContext{
		config: cfg,
		providers: map[addrs.Provider]providers.Factory{
			addrs.NewBuiltInProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		timestamp: &fakePlanTimestamp,
	}

	// Create dynamic values for resource change
	resourceBeforeVal := cty.NullVal(cty.Object(map[string]cty.Type{
		"id":    cty.String,
		"value": cty.String,
	}))
	resourceAfterVal := cty.ObjectVal(map[string]cty.Value{
		"id":    cty.UnknownVal(cty.String),
		"value": cty.StringVal("example"),
	})
	resourceBeforeDynVal, err := plans.NewDynamicValue(resourceBeforeVal, resourceBeforeVal.Type())
	if err != nil {
		t.Fatal(err)
	}
	resourceAfterDynVal, err := plans.NewDynamicValue(resourceAfterVal, resourceAfterVal.Type())
	if err != nil {
		t.Fatal(err)
	}

	// Common addresses used throughout the test
	webComponentInstance := stackaddrs.AbsComponentInstance{
		Stack: stackaddrs.RootStackInstance,
		Item: stackaddrs.ComponentInstance{
			Component: stackaddrs.Component{Name: "web"},
		},
	}
	webComponent := stackaddrs.AbsComponent{
		Stack: stackaddrs.RootStackInstance,
		Item:  stackaddrs.Component{Name: "web"},
	}
	testResourceInstance := addrs.RootModuleInstance.ResourceInstance(addrs.ManagedResourceMode, "testing_resource", "main", addrs.NoKey)
	testResourceObject := stackaddrs.AbsResourceInstanceObject{
		Component: webComponentInstance,
		Item: addrs.AbsResourceInstanceObject{
			ResourceInstance: testResourceInstance,
		},
	}
	testActionInstance := addrs.RootModuleInstance.ActionInstance("testing_action", "notify", addrs.NoKey)
	testActionInvocationAddr := stackaddrs.AbsActionInvocationInstance{
		Component: webComponentInstance,
		Item:      testActionInstance,
	}
	testProviderConfig := addrs.AbsProviderConfig{
		Module:   addrs.RootModule,
		Provider: addrs.NewBuiltInProvider("testing"),
	}

	expectedHooks := ExpectedHooks{
		ReportActionInvocationPlanned: []*hooks.ActionInvocation{
			{
				Addr:         testActionInvocationAddr,
				ProviderAddr: addrs.NewBuiltInProvider("testing"),
				Trigger: &plans.ResourceActionTrigger{
					TriggeringResourceAddr:  testResourceInstance,
					ActionTriggerEvent:      configs.AfterCreate,
					ActionTriggerBlockIndex: 0,
					ActionsListIndex:        0,
				},
			},
		},
		ComponentExpanded: []*hooks.ComponentInstances{
			{
				ComponentAddr: webComponent,
				InstanceAddrs: []stackaddrs.AbsComponentInstance{webComponentInstance},
			},
		},
		PendingComponentInstancePlan: collections.NewSet(webComponentInstance),
		BeginComponentInstancePlan:   collections.NewSet(webComponentInstance),
		EndComponentInstancePlan:     collections.NewSet(webComponentInstance),
		ReportResourceInstanceStatus: []*hooks.ResourceInstanceStatusHookData{
			{
				Addr:         testResourceObject,
				ProviderAddr: addrs.NewBuiltInProvider("testing"),
				Status:       hooks.ResourceInstancePlanning,
			},
			{
				Addr:         testResourceObject,
				ProviderAddr: addrs.NewBuiltInProvider("testing"),
				Status:       hooks.ResourceInstancePlanned,
			},
		},
		ReportResourceInstancePlanned: []*hooks.ResourceInstanceChange{
			{
				Addr: testResourceObject,
				Change: &plans.ResourceInstanceChangeSrc{
					Addr:         testResourceInstance,
					PrevRunAddr:  testResourceInstance,
					ProviderAddr: testProviderConfig,
					ChangeSrc: plans.ChangeSrc{
						Action: plans.Create,
						Before: resourceBeforeDynVal,
						After:  resourceAfterDynVal,
					},
				},
			},
		},
		ReportComponentInstancePlanned: []*hooks.ComponentInstanceChange{
			{
				Addr:             webComponentInstance,
				Add:              1,
				ActionInvocation: 1,
			},
		},
	}

	cycle := TestCycle{
		planMode:         plans.NormalMode,
		wantPlannedHooks: &expectedHooks,
	}

	testCtx.Plan(t, ctx, stackstate.NewState(), cycle)
}

func TestPlanWithDeferredActionInvocation(t *testing.T) {
	ctx := context.Background()
	cfg := loadMainBundleConfigForTest(t, "deferred-action")

	fakePlanTimestamp, err := time.Parse(time.RFC3339, "1994-09-05T08:50:00Z")
	if err != nil {
		t.Fatal(err)
	}

	changesCh := make(chan stackplan.PlannedChange)
	diagsCh := make(chan tfdiags.Diagnostic)
	lock := depsfile.NewLocks()
	lock.SetProvider(
		addrs.NewDefaultProvider("testing"),
		providerreqs.MustParseVersion("0.0.0"),
		providerreqs.MustParseVersionConstraints("=0.0.0"),
		providerreqs.PreferredHashes([]providerreqs.Hash{}),
	)
	req := PlanRequest{
		Config: cfg,
		ProviderFactories: map[addrs.Provider]providers.Factory{
			addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
				return stacks_testing_provider.NewProvider(t), nil
			},
		},
		DependencyLocks:    *lock,
		ForcePlanTimestamp: &fakePlanTimestamp,
		InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
			{Name: "id"}: {
				Value: cty.StringVal("test-id-123"),
			},
			{Name: "defer"}: {
				Value: cty.BoolVal(true),
			},
		},
	}
	resp := PlanResponse{
		PlannedChanges: changesCh,
		Diagnostics:    diagsCh,
	}
	go Plan(ctx, &req, &resp)
	gotChanges, diags := collectPlanOutput(changesCh, diagsCh)

	reportDiagnosticsForTest(t, diags)
	if len(diags) != 0 {
		t.FailNow() // We reported the diags above
	}

	sort.SliceStable(gotChanges, func(i, j int) bool {
		return plannedChangeSortKey(gotChanges[i]) < plannedChangeSortKey(gotChanges[j])
	})

	// Find the deferred action invocation in the changes
	var foundDeferredAction bool
	for _, change := range gotChanges {
		if _, ok := change.(*stackplan.PlannedChangeDeferredActionInvocation); ok {
			foundDeferredAction = true
			break
		}
	}

	if !foundDeferredAction {
		t.Error("Expected to find a deferred action invocation in the plan changes, but none was found")
		t.Logf("Got %d changes:", len(gotChanges))
		for i, change := range gotChanges {
			t.Logf("  [%d] %T", i, change)
		}
	}
}

// TestPlan_variableValidationAdvanced tests advanced variable validation scenarios
func TestPlan_variableValidationAdvanced(t *testing.T) {
	ctx := context.Background()

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

	fakePlanTimestamp, _ := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")

	testCases := map[string]struct {
		configPath        string
		planInputVars     map[string]cty.Value
		wantErrorMessages []string // Just check for error message presence, not exact diagnostic structure
	}{
		// Type validation tests
		"types-number-pass": {
			configPath: path.Join("with-single-input", "validation-types"),
			planInputVars: map[string]cty.Value{
				"input":        cty.StringVal("test"),
				"number_input": cty.NumberIntVal(50),
				"list_input":   cty.ListVal([]cty.Value{cty.StringVal("item1")}),
				"map_input":    cty.MapVal(map[string]cty.Value{"required_key": cty.StringVal("value")}),
			},
			wantErrorMessages: nil,
		},
		"types-number-out-of-range": {
			configPath: path.Join("with-single-input", "validation-types"),
			planInputVars: map[string]cty.Value{
				"input":        cty.StringVal("test"),
				"number_input": cty.NumberIntVal(150),
				"list_input":   cty.ListVal([]cty.Value{cty.StringVal("item1")}),
				"map_input":    cty.MapVal(map[string]cty.Value{"required_key": cty.StringVal("value")}),
			},
			wantErrorMessages: []string{"Number must be between 0 and 100."},
		},
		"types-list-too-many": {
			configPath: path.Join("with-single-input", "validation-types"),
			planInputVars: map[string]cty.Value{
				"input":        cty.StringVal("test"),
				"number_input": cty.NumberIntVal(50),
				"list_input":   cty.ListVal([]cty.Value{cty.StringVal("1"), cty.StringVal("2"), cty.StringVal("3"), cty.StringVal("4"), cty.StringVal("5"), cty.StringVal("6")}),
				"map_input":    cty.MapVal(map[string]cty.Value{"required_key": cty.StringVal("value")}),
			},
			wantErrorMessages: []string{"List must contain 1-5 items."},
		},
		"types-list-empty-string": {
			configPath: path.Join("with-single-input", "validation-types"),
			planInputVars: map[string]cty.Value{
				"input":        cty.StringVal("test"),
				"number_input": cty.NumberIntVal(50),
				"list_input":   cty.ListVal([]cty.Value{cty.StringVal("item"), cty.StringVal("")}),
				"map_input":    cty.MapVal(map[string]cty.Value{"required_key": cty.StringVal("value")}),
			},
			wantErrorMessages: []string{"List items cannot be empty strings."},
		},
		"types-map-missing-key": {
			configPath: path.Join("with-single-input", "validation-types"),
			planInputVars: map[string]cty.Value{
				"input":        cty.StringVal("test"),
				"number_input": cty.NumberIntVal(50),
				"list_input":   cty.ListVal([]cty.Value{cty.StringVal("item1")}),
				"map_input":    cty.MapVal(map[string]cty.Value{"other_key": cty.StringVal("value")}),
			},
			wantErrorMessages: []string{"Map must contain 'required_key'."},
		},

		// Sensitive variable validation tests
		"sensitive-password-pass": {
			configPath: path.Join("with-single-input", "validation-sensitive"),
			planInputVars: map[string]cty.Value{
				"input":    cty.StringVal("test"),
				"password": cty.StringVal("SecurePass123"),
				"api_key":  cty.StringVal("abcdef0123456789abcdef0123456789"),
			},
			wantErrorMessages: nil,
		},
		"sensitive-password-too-short": {
			configPath: path.Join("with-single-input", "validation-sensitive"),
			planInputVars: map[string]cty.Value{
				"input":    cty.StringVal("test"),
				"password": cty.StringVal("Short1"),
				"api_key":  cty.StringVal("abcdef0123456789abcdef0123456789"),
			},
			wantErrorMessages: []string{"Password must be at least 8 characters long."},
		},
		"sensitive-password-no-uppercase": {
			configPath: path.Join("with-single-input", "validation-sensitive"),
			planInputVars: map[string]cty.Value{
				"input":    cty.StringVal("test"),
				"password": cty.StringVal("securepass123"),
				"api_key":  cty.StringVal("abcdef0123456789abcdef0123456789"),
			},
			wantErrorMessages: []string{"Password must contain at least one uppercase letter."},
		},
		"sensitive-password-no-number": {
			configPath: path.Join("with-single-input", "validation-sensitive"),
			planInputVars: map[string]cty.Value{
				"input":    cty.StringVal("test"),
				"password": cty.StringVal("SecurePass"),
				"api_key":  cty.StringVal("abcdef0123456789abcdef0123456789"),
			},
			wantErrorMessages: []string{"Password must contain at least one number."},
		},
		"sensitive-api-key-wrong-length": {
			configPath: path.Join("with-single-input", "validation-sensitive"),
			planInputVars: map[string]cty.Value{
				"input":    cty.StringVal("test"),
				"password": cty.StringVal("SecurePass123"),
				"api_key":  cty.StringVal("abc123"),
			},
			wantErrorMessages: []string{"API key must be exactly 32 characters."},
		},
		"sensitive-api-key-invalid-chars": {
			configPath: path.Join("with-single-input", "validation-sensitive"),
			planInputVars: map[string]cty.Value{
				"input":    cty.StringVal("test"),
				"password": cty.StringVal("SecurePass123"),
				"api_key":  cty.StringVal("ABCDEF0123456789ABCDEF0123456789"),
			},
			wantErrorMessages: []string{"API key must only contain lowercase hex characters."},
		},

		// Complex validation tests
		"complex-email-pass": {
			configPath: path.Join("with-single-input", "validation-complex"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"email":       cty.StringVal("user@example.com"),
				"ip_address":  cty.StringVal("192.168.1.1"),
				"environment": cty.StringVal("dev"),
				"tags":        cty.MapVal(map[string]cty.Value{"owner": cty.StringVal("team"), "env": cty.StringVal("dev")}),
			},
			wantErrorMessages: nil,
		},
		"complex-email-invalid": {
			configPath: path.Join("with-single-input", "validation-complex"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"email":       cty.StringVal("not-an-email"),
				"ip_address":  cty.StringVal("192.168.1.1"),
				"environment": cty.StringVal("dev"),
				"tags":        cty.MapVal(map[string]cty.Value{"owner": cty.StringVal("team")}),
			},
			wantErrorMessages: []string{"Must be a valid email address."},
		},
		"complex-ip-invalid": {
			configPath: path.Join("with-single-input", "validation-complex"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"email":       cty.StringVal("user@example.com"),
				"ip_address":  cty.StringVal("999.999.999.999"),
				"environment": cty.StringVal("dev"),
				"tags":        cty.MapVal(map[string]cty.Value{"owner": cty.StringVal("team")}),
			},
			wantErrorMessages: []string{"Must be a valid IPv4 address."},
		},
		"complex-environment-invalid": {
			configPath: path.Join("with-single-input", "validation-complex"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"email":       cty.StringVal("user@example.com"),
				"ip_address":  cty.StringVal("192.168.1.1"),
				"environment": cty.StringVal("test"),
				"tags":        cty.MapVal(map[string]cty.Value{"owner": cty.StringVal("team")}),
			},
			wantErrorMessages: []string{"Environment must be dev, staging, or prod."},
		},
		"complex-tags-invalid-key": {
			configPath: path.Join("with-single-input", "validation-complex"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"email":       cty.StringVal("user@example.com"),
				"ip_address":  cty.StringVal("192.168.1.1"),
				"environment": cty.StringVal("dev"),
				"tags":        cty.MapVal(map[string]cty.Value{"Owner": cty.StringVal("team"), "owner": cty.StringVal("team")}),
			},
			wantErrorMessages: []string{"Tag keys must start with lowercase letter and contain only lowercase letters, numbers, and hyphens."},
		},
		"complex-tags-missing-owner": {
			configPath: path.Join("with-single-input", "validation-complex"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"email":       cty.StringVal("user@example.com"),
				"ip_address":  cty.StringVal("192.168.1.1"),
				"environment": cty.StringVal("dev"),
				"tags":        cty.MapVal(map[string]cty.Value{"env": cty.StringVal("dev")}),
			},
			wantErrorMessages: []string{"Tags must include 'owner' key."},
		},
		"complex-tags-empty-value": {
			configPath: path.Join("with-single-input", "validation-complex"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"email":       cty.StringVal("user@example.com"),
				"ip_address":  cty.StringVal("192.168.1.1"),
				"environment": cty.StringVal("dev"),
				"tags":        cty.MapVal(map[string]cty.Value{"owner": cty.StringVal(""), "env": cty.StringVal("dev")}),
			},
			wantErrorMessages: []string{"Tag values must be 1-256 characters."},
		},
		"complex-tags-value-too-long": {
			configPath: path.Join("with-single-input", "validation-complex"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"email":       cty.StringVal("user@example.com"),
				"ip_address":  cty.StringVal("192.168.1.1"),
				"environment": cty.StringVal("dev"),
				"tags":        cty.MapVal(map[string]cty.Value{"owner": cty.StringVal("team"), "description": cty.StringVal(strings.Repeat("x", 257))}),
			},
			wantErrorMessages: []string{"Tag values must be 1-256 characters."},
		},

		// Invalid error message tests - these verify that invalid error messages
		// are caught even when validation passes or fails
		"invalid-error-message-sensitive-in-error": {
			configPath: path.Join("with-single-input", "validation-invalid-error-message"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"password":    cty.StringVal("short"),
				"token":       cty.StringVal("abcdef0123456789abcdef0123456789"),
				"count_value": cty.SetVal([]cty.Value{cty.StringVal("a")}),
				"api_key":     cty.StringVal("abcdef0123456789"),
			},
			wantErrorMessages: []string{
				"error expression used to explain this condition refers to sensitive values",
			},
		},
		"invalid-error-message-ephemeral-in-error": {
			configPath: path.Join("with-single-input", "validation-invalid-error-message"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"password":    cty.StringVal("SecurePass123"),
				"token":       cty.StringVal("short_token"),
				"count_value": cty.SetVal([]cty.Value{cty.StringVal("a")}),
				"api_key":     cty.StringVal("abcdef0123456789"),
			},
			wantErrorMessages: []string{
				"error expression used to explain this condition refers to ephemeral values",
			},
		},
		"invalid-error-message-not-string": {
			configPath: path.Join("with-single-input", "validation-invalid-error-message"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"password":    cty.StringVal("SecurePass123"),
				"token":       cty.StringVal("abcdef0123456789abcdef0123456789"),
				"count_value": cty.SetValEmpty(cty.String), // empty set fails condition; a set cannot be converted to a string
				"api_key":     cty.StringVal("abcdef0123456789"),
			},
			// A set value cannot be converted to a string, so we get an "Invalid error message" diagnostic
			// rather than the condition failure message.
			wantErrorMessages: []string{
				"Unsuitable value for error message",
			},
		},
		"invalid-error-message-sensitive-even-when-passing": {
			configPath: path.Join("with-single-input", "validation-invalid-error-message"),
			planInputVars: map[string]cty.Value{
				"input":       cty.StringVal("test"),
				"password":    cty.StringVal("SecurePass123"),
				"token":       cty.StringVal("abcdef0123456789abcdef0123456789"),
				"count_value": cty.SetVal([]cty.Value{cty.StringVal("a")}),
				"api_key":     cty.StringVal("abcdef0123456789abcdef0123456789abcdef0123456789"),
			},
			// This tests that we evaluate error_message even when validation passes
			wantErrorMessages: []string{
				"error expression used to explain this condition refers to sensitive values",
			},
		},

		// Provider function tests
		"provider-functions-pass": {
			configPath: path.Join("with-single-input", "validation-provider-functions"),
			planInputVars: map[string]cty.Value{
				"input":      cty.StringVal("test"),
				"echo_value": cty.StringVal("test_value"),
				"combined":   cty.StringVal("long_enough"),
			},
			wantErrorMessages: nil,
		},
		"provider-functions-fail": {
			configPath: path.Join("with-single-input", "validation-provider-functions"),
			planInputVars: map[string]cty.Value{
				"input":      cty.StringVal("test"),
				"echo_value": cty.StringVal("test"),
				"combined":   cty.StringVal("short"),
			},
			wantErrorMessages: []string{
				"Combined value must be longer than 5 characters after echo",
			},
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			cfg := loadMainBundleConfigForTest(t, tc.configPath)

			req := PlanRequest{
				Config: cfg,
				InputValues: func() map[stackaddrs.InputVariable]ExternalInputValue {
					inputs := make(map[stackaddrs.InputVariable]ExternalInputValue, len(tc.planInputVars))
					for k, v := range tc.planInputVars {
						inputs[stackaddrs.InputVariable{Name: k}] = ExternalInputValue{Value: v}
					}
					return inputs
				}(),
				ProviderFactories: map[addrs.Provider]providers.Factory{
					addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
						return stacks_testing_provider.NewProvider(t), nil
					},
				},
				DependencyLocks:    *lock,
				ForcePlanTimestamp: &fakePlanTimestamp,
			}

			changesCh := make(chan stackplan.PlannedChange)
			diagsCh := make(chan tfdiags.Diagnostic)
			resp := PlanResponse{
				PlannedChanges: changesCh,
				Diagnostics:    diagsCh,
			}

			go Plan(ctx, &req, &resp)
			_, diags := collectPlanOutput(changesCh, diagsCh)

			// Check that we get the expected error messages
			if tc.wantErrorMessages == nil {
				if len(diags) > 0 {
					t.Errorf("expected no diagnostics, got: %s", diags.ErrWithWarnings())
				}
			} else {
				if len(diags) == 0 {
					t.Fatalf("expected diagnostics with messages %v, got none", tc.wantErrorMessages)
				}
				// Check that all expected error messages are present
				for _, wantMsg := range tc.wantErrorMessages {
					found := false
					for _, diag := range diags {
						if strings.Contains(diag.Description().Detail, wantMsg) {
							found = true
							break
						}
					}
					if !found {
						t.Errorf("expected error message %q not found in diagnostics: %s", wantMsg, diags.ErrWithWarnings())
					}
				}
			}
		})
	}
}
