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

package jsonformat

import (
	"encoding/json"
	"fmt"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/mitchellh/colorstring"
	"github.com/zclconf/go-cty/cty"
	ctyjson "github.com/zclconf/go-cty/cty/json"

	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/command/jsonformat/differ"
	"github.com/hashicorp/terraform/internal/command/jsonformat/structured"
	"github.com/hashicorp/terraform/internal/command/jsonformat/structured/attribute_path"
	"github.com/hashicorp/terraform/internal/command/jsonplan"
	"github.com/hashicorp/terraform/internal/command/jsonprovider"
	"github.com/hashicorp/terraform/internal/configs/configschema"
	"github.com/hashicorp/terraform/internal/plans"
	"github.com/hashicorp/terraform/internal/providers"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/terminal"
	"github.com/hashicorp/terraform/internal/terraform"
)

func TestRenderHuman_InvokeActionPlan(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
	streams, done := terminal.StreamsForTesting(t)

	plan := Plan{
		ActionInvocations: []jsonplan.ActionInvocation{
			{
				Address:             "action.test_action.action",
				Type:                "test_action",
				Name:                "action",
				ConfigValues:        json.RawMessage("{\"attr\":\"one\"}"),
				ConfigSensitive:     nil,
				ProviderName:        "test",
				InvokeActionTrigger: new(jsonplan.InvokeActionTrigger),
			},
		},
		ProviderSchemas: map[string]*jsonprovider.Provider{
			"test": {
				ActionSchemas: map[string]*jsonprovider.ActionSchema{
					"test_action": {
						ConfigSchema: &jsonprovider.Block{
							Attributes: map[string]*jsonprovider.Attribute{
								"attr": {
									AttributeType: []byte("\"string\""),
								},
							},
						},
					},
				},
			},
		},
	}

	renderer := Renderer{Colorize: color, Streams: streams}
	plan.renderHuman(renderer, plans.RefreshOnlyMode)

	want := `
Terraform will perform the following actions:

Plan: 0 to add, 0 to change, 0 to destroy. Actions: 1 to invoke.

Terraform will invoke the following action(s):

  # action.test_action.action will be invoked
    action "test_action" "action" {
        config {
            attr = "one"
        }
    }

`

	got := done(t).Stdout()
	if diff := cmp.Diff(want, got); len(diff) > 0 {
		t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
	}
}

func TestRenderHuman_InvokeActionPlanWithRefresh(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
	streams, done := terminal.StreamsForTesting(t)

	plan := Plan{
		ActionInvocations: []jsonplan.ActionInvocation{
			{
				Address:             "action.test_action.action",
				Type:                "test_action",
				Name:                "action",
				ConfigValues:        json.RawMessage("{\"attr\":\"one\"}"),
				ConfigSensitive:     nil,
				ProviderName:        "test",
				InvokeActionTrigger: new(jsonplan.InvokeActionTrigger),
			},
		},
		ResourceDrift: []jsonplan.ResourceChange{
			{
				Address:      "aws_instance.foo",
				Mode:         "managed",
				Type:         "aws_instance",
				Name:         "foo",
				IndexUnknown: true,
				ProviderName: "aws",
				Change: jsonplan.Change{
					Actions: []string{"update"},
					Before: marshalJson(t, map[string]interface{}{
						"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
						"value": "Hello, World!",
					}),
					After: marshalJson(t, map[string]interface{}{
						"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
						"value": "Hello, World!",
					}),
				},
			},
		},
		ProviderSchemas: map[string]*jsonprovider.Provider{
			"test": {
				ActionSchemas: map[string]*jsonprovider.ActionSchema{
					"test_action": {
						ConfigSchema: &jsonprovider.Block{
							Attributes: map[string]*jsonprovider.Attribute{
								"attr": {
									AttributeType: []byte("\"string\""),
								},
							},
						},
					},
				},
			},
			"aws": {
				ResourceSchemas: map[string]*jsonprovider.Schema{
					"aws_instance": {
						Block: &jsonprovider.Block{
							Attributes: map[string]*jsonprovider.Attribute{
								"id": {
									AttributeType: marshalJson(t, "string"),
								},
								"ami": {
									AttributeType: marshalJson(t, "string"),
								},
							},
						},
					},
				},
			},
		},
	}

	renderer := Renderer{Colorize: color, Streams: streams}
	plan.renderHuman(renderer, plans.RefreshOnlyMode)

	want := `
Note: Objects have changed outside of Terraform

Terraform detected the following changes made outside of Terraform since the
last "terraform apply" which may have affected this plan:

  # aws_instance.foo has changed
  ~ resource "aws_instance" "foo" {
        id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
    }


This is a refresh-only plan, so Terraform will not take any actions to undo
these. If you were expecting these changes then you can apply this plan to
record the updated values in the Terraform state without changing any remote
objects.

─────────────────────────────────────────────────────────────────────────────

Terraform will perform the following actions:

Plan: 0 to add, 0 to change, 0 to destroy. Actions: 1 to invoke.

Terraform will invoke the following action(s):

  # action.test_action.action will be invoked
    action "test_action" "action" {
        config {
            attr = "one"
        }
    }

`

	got := done(t).Stdout()
	if diff := cmp.Diff(want, got); len(diff) > 0 {
		t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
	}
}

func TestRenderHuman_EmptyPlan(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
	streams, done := terminal.StreamsForTesting(t)

	plan := Plan{}

	renderer := Renderer{Colorize: color, Streams: streams}
	plan.renderHuman(renderer, plans.NormalMode)

	want := `
No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
`

	got := done(t).Stdout()
	if diff := cmp.Diff(want, got); len(diff) > 0 {
		t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
	}
}

func TestRenderHuman_DeferredPlan(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
	streams, done := terminal.StreamsForTesting(t)

	plan := Plan{
		DeferredChanges: []jsonplan.DeferredResourceChange{
			{
				Reason: jsonplan.DeferredReasonDeferredPrereq,
				ResourceChange: jsonplan.ResourceChange{
					Address:      "aws_instance.foo",
					Mode:         "managed",
					Type:         "aws_instance",
					Name:         "foo",
					IndexUnknown: true,
					ProviderName: "aws",
					Change: jsonplan.Change{
						Actions: []string{"update"},
						Before: marshalJson(t, map[string]interface{}{
							"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
							"value": "Hello, World!",
						}),
						After: marshalJson(t, map[string]interface{}{
							"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
							"value": "Hello, World!",
						}),
					},
				},
			},
		},
		ProviderSchemas: map[string]*jsonprovider.Provider{
			"aws": {
				ResourceSchemas: map[string]*jsonprovider.Schema{
					"aws_instance": {
						Block: &jsonprovider.Block{
							Attributes: map[string]*jsonprovider.Attribute{
								"id": {
									AttributeType: marshalJson(t, "string"),
								},
								"ami": {
									AttributeType: marshalJson(t, "string"),
								},
							},
						},
					},
				},
			},
		},
	}

	renderer := Renderer{Colorize: color, Streams: streams}
	plan.renderHuman(renderer, plans.NormalMode)

	want := `
No current changes. This plan requires another plan to be applied first.


Note: This is a partial plan, parts can only be known in the next plan / apply cycle.


  # aws_instance.foo was deferred
  # (because a prerequisite for this resource is deferred)
  ~ resource "aws_instance" "foo" {
        id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
    }

─────────────────────────────────────────────────────────────────────────────
`

	got := done(t).Stdout()
	if diff := cmp.Diff(want, got); len(diff) > 0 {
		t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
	}
}

func TestRenderHuman_EmptyOutputs(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
	streams, done := terminal.StreamsForTesting(t)

	outputVal, _ := json.Marshal("some-text")
	plan := Plan{
		OutputChanges: map[string]jsonplan.Change{
			"a_string": {
				Actions: []string{"no-op"},
				Before:  outputVal,
				After:   outputVal,
			},
		},
	}

	renderer := Renderer{Colorize: color, Streams: streams}
	plan.renderHuman(renderer, plans.NormalMode)

	want := `
No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration
and found no differences, so no changes are needed.
`

	got := done(t).Stdout()
	if diff := cmp.Diff(want, got); len(diff) > 0 {
		t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
	}
}

func TestRenderHuman_Imports(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}

	schemas := map[string]*jsonprovider.Provider{
		"test": {
			ResourceSchemas: map[string]*jsonprovider.Schema{
				"test_resource": {
					Block: &jsonprovider.Block{
						Attributes: map[string]*jsonprovider.Attribute{
							"id": {
								AttributeType: marshalJson(t, "string"),
							},
							"value": {
								AttributeType: marshalJson(t, "string"),
							},
						},
					},
				},
			},
		},
	}

	tcs := map[string]struct {
		plan   Plan
		output string
	}{
		"simple_import": {
			plan: Plan{
				ResourceChanges: []jsonplan.ResourceChange{
					{
						Address:      "test_resource.resource",
						Mode:         "managed",
						Type:         "test_resource",
						Name:         "resource",
						ProviderName: "test",
						Change: jsonplan.Change{
							Actions: []string{"no-op"},
							Before: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							After: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							Importing: &jsonplan.Importing{
								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
							},
						},
					},
				},
			},
			output: `
Terraform will perform the following actions:

  # test_resource.resource will be imported
    resource "test_resource" "resource" {
        id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
        value = "Hello, World!"
    }

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
`,
		},
		"simple_import_with_generated_config": {
			plan: Plan{
				ResourceChanges: []jsonplan.ResourceChange{
					{
						Address:      "test_resource.resource",
						Mode:         "managed",
						Type:         "test_resource",
						Name:         "resource",
						ProviderName: "test",
						Change: jsonplan.Change{
							Actions: []string{"no-op"},
							Before: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							After: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							Importing: &jsonplan.Importing{
								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
							},
							GeneratedConfig: `resource "test_resource" "resource" {
  id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
  value = "Hello, World!"
}`,
						},
					},
				},
			},
			output: `
Terraform will perform the following actions:

  # test_resource.resource will be imported
  # (config will be generated)
    resource "test_resource" "resource" {
        id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
        value = "Hello, World!"
    }

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
`,
		},
		"import_and_move": {
			plan: Plan{
				ResourceChanges: []jsonplan.ResourceChange{
					{
						Address:         "test_resource.after",
						PreviousAddress: "test_resource.before",
						Mode:            "managed",
						Type:            "test_resource",
						Name:            "after",
						ProviderName:    "test",
						Change: jsonplan.Change{
							Actions: []string{"no-op"},
							Before: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							After: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							Importing: &jsonplan.Importing{
								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
							},
						},
					},
				},
			},
			output: `
Terraform will perform the following actions:

  # test_resource.before has moved to test_resource.after
  # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
    resource "test_resource" "after" {
        id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
        value = "Hello, World!"
    }

Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
`,
		},
		"import_move_and_update": {
			plan: Plan{
				ResourceChanges: []jsonplan.ResourceChange{
					{
						Address:         "test_resource.after",
						PreviousAddress: "test_resource.before",
						Mode:            "managed",
						Type:            "test_resource",
						Name:            "after",
						ProviderName:    "test",
						Change: jsonplan.Change{
							Actions: []string{"update"},
							Before: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							After: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, Universe!",
							}),
							Importing: &jsonplan.Importing{
								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
							},
						},
					},
				},
			},
			output: `
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # test_resource.after will be updated in-place
  # (moved from test_resource.before)
  # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
  ~ resource "test_resource" "after" {
        id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
      ~ value = "Hello, World!" -> "Hello, Universe!"
    }

Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.
`,
		},
		"import_and_update": {
			plan: Plan{
				ResourceChanges: []jsonplan.ResourceChange{
					{
						Address:      "test_resource.resource",
						Mode:         "managed",
						Type:         "test_resource",
						Name:         "resource",
						ProviderName: "test",
						Change: jsonplan.Change{
							Actions: []string{"update"},
							Before: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							After: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, Universe!",
							}),
							Importing: &jsonplan.Importing{
								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
							},
						},
					},
				},
			},
			output: `
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # test_resource.resource will be updated in-place
  # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
  ~ resource "test_resource" "resource" {
        id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
      ~ value = "Hello, World!" -> "Hello, Universe!"
    }

Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.
`,
		},
		"import_and_update_with_no_id": {
			plan: Plan{
				ResourceChanges: []jsonplan.ResourceChange{
					{
						Address:      "test_resource.resource",
						Mode:         "managed",
						Type:         "test_resource",
						Name:         "resource",
						ProviderName: "test",
						Change: jsonplan.Change{
							Actions: []string{"update"},
							Before: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							After: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, Universe!",
							}),
							Importing: &jsonplan.Importing{},
						},
					},
				},
			},
			output: `
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # test_resource.resource will be updated in-place
  # (will be imported first)
  ~ resource "test_resource" "resource" {
        id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
      ~ value = "Hello, World!" -> "Hello, Universe!"
    }

Plan: 1 to import, 0 to add, 1 to change, 0 to destroy.
`,
		},
		"import_and_replace": {
			plan: Plan{
				ResourceChanges: []jsonplan.ResourceChange{
					{
						Address:      "test_resource.resource",
						Mode:         "managed",
						Type:         "test_resource",
						Name:         "resource",
						ProviderName: "test",
						Change: jsonplan.Change{
							Actions: []string{"create", "delete"},
							Before: marshalJson(t, map[string]interface{}{
								"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
								"value": "Hello, World!",
							}),
							After: marshalJson(t, map[string]interface{}{
								"id":    "9794FB1F-7260-442F-830C-F2D450E90CE3",
								"value": "Hello, World!",
							}),
							ReplacePaths: marshalJson(t, [][]string{{"id"}}),
							Importing: &jsonplan.Importing{
								ID: "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
							},
						},
						ActionReason: "",
					},
				},
			},
			output: `
Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
+/- create replacement and then destroy

Terraform will perform the following actions:

  # test_resource.resource must be replaced
  # (imported from "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E")
  # Warning: this will destroy the imported resource
+/- resource "test_resource" "resource" {
      ~ id    = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E" -> "9794FB1F-7260-442F-830C-F2D450E90CE3" # forces replacement
        value = "Hello, World!"
    }

Plan: 1 to import, 1 to add, 0 to change, 1 to destroy.
`,
		},
	}
	for name, tc := range tcs {
		t.Run(name, func(t *testing.T) {
			streams, done := terminal.StreamsForTesting(t)

			plan := tc.plan
			plan.PlanFormatVersion = jsonplan.FormatVersion
			plan.ProviderFormatVersion = jsonprovider.FormatVersion
			plan.ProviderSchemas = schemas

			renderer := Renderer{
				Colorize: color,
				Streams:  streams,
			}
			plan.renderHuman(renderer, plans.NormalMode)

			got := done(t).Stdout()
			want := tc.output
			if diff := cmp.Diff(want, got); len(diff) > 0 {
				t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", got, want, diff)
			}
		})
	}
}

func TestResourceChange_primitiveTypes(t *testing.T) {
	testCases := map[string]testCase{
		"creation": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id": {Type: cty.String, Computed: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + id = (known after apply)
    }`,
		},
		"creation (null string)": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"string": cty.StringVal("null"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"string": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + string = "null"
    }`,
		},
		"creation (null string with extra whitespace)": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"string": cty.StringVal("null "),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"string": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + string = "null "
    }`,
		},
		"creation (object with quoted keys)": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"object": cty.ObjectVal(map[string]cty.Value{
					"unquoted":   cty.StringVal("value"),
					"quoted:key": cty.StringVal("some-value"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"object": {Type: cty.Object(map[string]cty.Type{
						"unquoted":   cty.String,
						"quoted:key": cty.String,
					}), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + object = {
          + "quoted:key" = "some-value"
          + unquoted     = "value"
        }
    }`,
		},
		"deletion": {
			Action: plans.Delete,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
			}),
			After: cty.NullVal(cty.EmptyObject),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id": {Type: cty.String, Computed: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be destroyed
  - resource "test_instance" "example" {
      - id = "i-02ae66f368e8518a9" -> null
    }`,
		},
		"deletion of deposed object": {
			Action:     plans.Delete,
			Mode:       addrs.ManagedResourceMode,
			DeposedKey: states.DeposedKey("byebye"),
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
			}),
			After: cty.NullVal(cty.EmptyObject),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id": {Type: cty.String, Computed: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example (deposed object byebye) will be destroyed
  # (left over from a partially-failed replacement of this instance)
  - resource "test_instance" "example" {
      - id = "i-02ae66f368e8518a9" -> null
    }`,
		},
		"deletion (empty string)": {
			Action: plans.Delete,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":                 cty.StringVal("i-02ae66f368e8518a9"),
				"intentionally_long": cty.StringVal(""),
			}),
			After: cty.NullVal(cty.EmptyObject),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":                 {Type: cty.String, Computed: true},
					"intentionally_long": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be destroyed
  - resource "test_instance" "example" {
      - id                 = "i-02ae66f368e8518a9" -> null
        # (1 unchanged attribute hidden)
    }`,
		},
		"forget": {
			Action: plans.Forget,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-123"),
			}),
			After: cty.NullVal(cty.EmptyObject),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Computed: true},
					"ami": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: ` # test_instance.example will no longer be managed by Terraform, but will not be destroyed
 # (destroy = false is set in the configuration)
 . resource "test_instance" "example" {
        id  = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)
    }`,
		},
		"forget (deposed)": {
			Action:     plans.Forget,
			Mode:       addrs.ManagedResourceMode,
			DeposedKey: states.DeposedKey("adios"),
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
			}),
			After: cty.NullVal(cty.EmptyObject),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id": {Type: cty.String, Computed: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: ` # test_instance.example (deposed object adios) will be removed from Terraform state, but will not be destroyed
 # (left over from a partially-failed replacement of this instance)
 # (destroy = false is set in the configuration)
 . resource "test_instance" "example" {
        id = "i-02ae66f368e8518a9"
    }`,
		},
		"create-then-forget": {
			Action: plans.CreateThenForget,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02999999999999999"),
				"ami": cty.StringVal("ami-AFTER"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Computed: true},
					"ami": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "ami"},
			}),
			ExpectedOutput: ` # test_instance.example must be replaced, but the existing object will not be destroyed
 # (destroy = false is set in the configuration)
 +/. resource "test_instance" "example" {
      ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
      ~ id  = "i-02ae66f368e8518a9" -> "i-02999999999999999"
    }`,
		},
		"string in-place update": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Optional: true, Computed: true},
					"ami": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami = "ami-BEFORE" -> "ami-AFTER"
        id  = "i-02ae66f368e8518a9"
    }`,
		},
		"update with quoted key": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":       cty.StringVal("i-02ae66f368e8518a9"),
				"saml:aud": cty.StringVal("https://example.com/saml"),
				"zeta":     cty.StringVal("alpha"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":       cty.StringVal("i-02ae66f368e8518a9"),
				"saml:aud": cty.StringVal("https://saml.example.com"),
				"zeta":     cty.StringVal("alpha"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":       {Type: cty.String, Optional: true, Computed: true},
					"saml:aud": {Type: cty.String, Optional: true},
					"zeta":     {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
        id         = "i-02ae66f368e8518a9"
      ~ "saml:aud" = "https://example.com/saml" -> "https://saml.example.com"
        # (1 unchanged attribute hidden)
    }`,
		},
		"string force-new update": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Optional: true, Computed: true},
					"ami": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "ami"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
        id  = "i-02ae66f368e8518a9"
    }`,
		},
		"string in-place update (null values)": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"ami":       cty.StringVal("ami-BEFORE"),
				"unchanged": cty.NullVal(cty.String),
				"empty":     cty.StringVal(""),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"ami":       cty.StringVal("ami-AFTER"),
				"unchanged": cty.NullVal(cty.String),
				"empty":     cty.NullVal(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"unchanged": {Type: cty.String, Optional: true},
					"empty":     {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)
    }`,
		},
		"string update (non-legacy)": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"from_null": cty.NullVal(cty.String),
				"to_null":   cty.StringVal(""),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"from_null": cty.StringVal(""),
				"to_null":   cty.NullVal(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"from_null": {Type: cty.DynamicPseudoType, Optional: true},
					"to_null":   {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      + from_null = ""
        id        = "i-02ae66f368e8518a9"
      - to_null   = "" -> null
    }`,
		},
		"string update (non-legacy nested object)": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"obj": cty.ObjectVal(map[string]cty.Value{
					"from_null": cty.NullVal(cty.String),
					"to_null":   cty.StringVal(""),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"obj": cty.ObjectVal(map[string]cty.Value{
					"from_null": cty.StringVal(""),
					"to_null":   cty.NullVal(cty.String),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id": {Type: cty.String, Optional: true, Computed: true},
					"obj": {NestedType: &configschema.Object{
						Nesting: configschema.NestingSingle,
						Attributes: map[string]*configschema.Attribute{
							"from_null": {Type: cty.DynamicPseudoType, Optional: true},
							"to_null":   {Type: cty.String, Optional: true},
						},
					}},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
        id  = "i-02ae66f368e8518a9"
      ~ obj = {
          + from_null = ""
          - to_null   = "" -> null
        }
    }`,
		},
		"in-place update of multi-line string field": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"more_lines": cty.StringVal(`original
long
multi-line
string
field`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
				"more_lines": cty.StringVal(`original
extremely long
multi-line
string
field`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"more_lines": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ more_lines = <<-EOT
            original
          - long
          + extremely long
            multi-line
            string
            field
        EOT
    }`,
		},
		"addition of multi-line string field": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"more_lines": cty.NullVal(cty.String),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
				"more_lines": cty.StringVal(`original
new line`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"more_lines": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      + more_lines = <<-EOT
            original
            new line
        EOT
    }`,
		},
		"force-new update of multi-line string field": {
			Action: plans.DeleteThenCreate,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"more_lines": cty.StringVal(`original`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
				"more_lines": cty.StringVal(`original
new line`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"more_lines": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "more_lines"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ more_lines = <<-EOT # forces replacement
            original
          + new line
        EOT
    }`,
		},

		// Sensitive

		"creation with sensitive field": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":       cty.UnknownVal(cty.String),
				"password": cty.StringVal("top-secret"),
				"conn_info": cty.ObjectVal(map[string]cty.Value{
					"user":     cty.StringVal("not-secret"),
					"password": cty.StringVal("top-secret"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":       {Type: cty.String, Computed: true},
					"password": {Type: cty.String, Optional: true, Sensitive: true},
					"conn_info": {
						NestedType: &configschema.Object{
							Nesting: configschema.NestingSingle,
							Attributes: map[string]*configschema.Attribute{
								"user":     {Type: cty.String, Optional: true},
								"password": {Type: cty.String, Optional: true, Sensitive: true},
							},
						},
					},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + conn_info = {
          + password = (sensitive value)
          + user     = "not-secret"
        }
      + id        = (known after apply)
      + password  = (sensitive value)
    }`,
		},
		"update with equal sensitive field": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":       cty.StringVal("blah"),
				"str":      cty.StringVal("before"),
				"password": cty.StringVal("top-secret"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":       cty.UnknownVal(cty.String),
				"str":      cty.StringVal("after"),
				"password": cty.StringVal("top-secret"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":       {Type: cty.String, Computed: true},
					"str":      {Type: cty.String, Optional: true},
					"password": {Type: cty.String, Optional: true, Sensitive: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id       = "blah" -> (known after apply)
      ~ str      = "before" -> "after"
        # (1 unchanged attribute hidden)
    }`,
		},

		// tainted objects
		"replace tainted resource": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseTainted,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-AFTER"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Optional: true, Computed: true},
					"ami": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "ami"},
			}),
			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
-/+ resource "test_instance" "example" {
      ~ ami = "ami-BEFORE" -> "ami-AFTER" # forces replacement
      ~ id  = "i-02ae66f368e8518a9" -> (known after apply)
    }`,
		},
		"force replacement with empty before value": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"name":   cty.StringVal("name"),
				"forced": cty.NullVal(cty.String),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"name":   cty.StringVal("name"),
				"forced": cty.StringVal("example"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"name":   {Type: cty.String, Optional: true},
					"forced": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "forced"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      + forced = "example" # forces replacement
        name   = "name"
    }`,
		},
		"force replacement with empty before value legacy": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"name":   cty.StringVal("name"),
				"forced": cty.StringVal(""),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"name":   cty.StringVal("name"),
				"forced": cty.StringVal("example"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"name":   {Type: cty.String, Optional: true},
					"forced": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "forced"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      + forced = "example" # forces replacement
        name   = "name"
    }`,
		},
		"read during apply because of unknown configuration": {
			Action:       plans.Read,
			ActionReason: plans.ResourceInstanceReadBecauseConfigUnknown,
			Mode:         addrs.DataResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"name": cty.StringVal("name"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"name": cty.StringVal("name"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"name": {Type: cty.String, Optional: true},
				},
			},
			ExpectedOutput: `  # data.test_instance.example will be read during apply
  # (config refers to values not yet known)
 <= data "test_instance" "example" {
        name = "name"
    }`,
		},
		"read during apply because of pending changes to upstream dependency": {
			Action:       plans.Read,
			ActionReason: plans.ResourceInstanceReadBecauseDependencyPending,
			Mode:         addrs.DataResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"name": cty.StringVal("name"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"name": cty.StringVal("name"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"name": {Type: cty.String, Optional: true},
				},
			},
			ExpectedOutput: `  # data.test_instance.example will be read during apply
  # (depends on a resource or a module with changes pending)
 <= data "test_instance" "example" {
        name = "name"
    }`,
		},
		"read during apply for unspecified reason": {
			Action: plans.Read,
			Mode:   addrs.DataResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"name": cty.StringVal("name"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"name": cty.StringVal("name"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"name": {Type: cty.String, Optional: true},
				},
			},
			ExpectedOutput: `  # data.test_instance.example will be read during apply
 <= data "test_instance" "example" {
        name = "name"
    }`,
		},
		"show all identifying attributes even if unchanged": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":   cty.StringVal("i-02ae66f368e8518a9"),
				"ami":  cty.StringVal("ami-BEFORE"),
				"bar":  cty.StringVal("bar"),
				"foo":  cty.StringVal("foo"),
				"name": cty.StringVal("alice"),
				"tags": cty.MapVal(map[string]cty.Value{
					"name": cty.StringVal("bob"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":   cty.StringVal("i-02ae66f368e8518a9"),
				"ami":  cty.StringVal("ami-AFTER"),
				"bar":  cty.StringVal("bar"),
				"foo":  cty.StringVal("foo"),
				"name": cty.StringVal("alice"),
				"tags": cty.MapVal(map[string]cty.Value{
					"name": cty.StringVal("bob"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":   {Type: cty.String, Optional: true, Computed: true},
					"ami":  {Type: cty.String, Optional: true},
					"bar":  {Type: cty.String, Optional: true},
					"foo":  {Type: cty.String, Optional: true},
					"name": {Type: cty.String, Optional: true},
					"tags": {Type: cty.Map(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
        id   = "i-02ae66f368e8518a9"
        name = "alice"
        tags = {
            "name" = "bob"
        }
        # (2 unchanged attributes hidden)
    }`,
		},
	}

	runTestCases(t, testCases)
}

func TestResourceChange_JSON(t *testing.T) {
	testCases := map[string]testCase{
		"creation": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{
					"str": "value",
					"list":["a","b", 234, true],
					"obj": {"key": "val"}
				}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + id         = (known after apply)
      + json_field = jsonencode(
            {
              + list = [
                  + "a",
                  + "b",
                  + 234,
                  + true,
                ]
              + obj  = {
                  + key = "val"
                }
              + str  = "value"
            }
        )
    }`,
		},
		"in-place update of object": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": "value","ccc": 5}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              + bbb = "new_value"
              - ccc = 5
                # (1 unchanged attribute hidden)
            }
        )
    }`,
		},
		"in-place update of object with quoted keys": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": "value", "c:c": "old_value"}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"aaa": "value", "b:bb": "new_value"}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              + "b:bb" = "new_value"
              - "c:c"  = "old_value"
                # (1 unchanged attribute hidden)
            }
        )
    }`,
		},
		"in-place update (from empty tuple)": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": []}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"aaa": ["value"]}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              ~ aaa = [
                  + "value",
                ]
            }
        )
    }`,
		},
		"in-place update (to empty tuple)": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": ["value"]}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"aaa": []}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              ~ aaa = [
                  - "value",
                ]
            }
        )
    }`,
		},
		"in-place update (tuple of different types)": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"baz"}, "value"]}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              ~ aaa = [
                    42,
                  ~ {
                      ~ foo = "bar" -> "baz"
                    },
                    "value",
                ]
            }
        )
    }`,
		},
		"force-new update": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": "value"}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "new_value"}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "json_field"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              + bbb = "new_value"
                # (1 unchanged attribute hidden)
            } # forces replacement
        )
    }`,
		},
		"in-place update (whitespace change)": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"aaa":"value",
					"bbb":"another"}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode( # whitespace changes
            {
                aaa = "value"
                bbb = "another"
            }
        )
    }`,
		},
		"force-new update (whitespace change)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": "value", "bbb": "another"}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"aaa":"value",
					"bbb":"another"}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "json_field"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode( # whitespace changes force replacement
            {
                aaa = "value"
                bbb = "another"
            }
        )
    }`,
		},
		"creation (empty)": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + id         = (known after apply)
      + json_field = jsonencode({})
    }`,
		},
		"JSON list item removal": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`["first","second","third"]`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`["first","second"]`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ [
                # (1 unchanged element hidden)
                "second",
              - "third",
            ]
        )
    }`,
		},
		"JSON list item addition": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`["first","second"]`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`["first","second","third"]`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ [
                # (1 unchanged element hidden)
                "second",
              + "third",
            ]
        )
    }`,
		},
		"JSON list object addition": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"first":"111"}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"first":"111","second":"222"}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              + second = "222"
                # (1 unchanged attribute hidden)
            }
        )
    }`,
		},
		"JSON object with nested list": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{
		  "Statement": ["first"]
		}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{
		  "Statement": ["first", "second"]
		}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              ~ Statement = [
                    "first",
                  + "second",
                ]
            }
        )
    }`,
		},
		"JSON list of objects - adding item": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`[{"one": "111"}]`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}]`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ [
                {
                    one = "111"
                },
              + {
                  + two = "222"
                },
            ]
        )
    }`,
		},
		"JSON list of objects - removing item": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`[{"one": "111"}, {"two": "222"}, {"three": "333"}]`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`[{"one": "111"}, {"three": "333"}]`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ [
                {
                    one = "111"
                },
              - {
                  - two = "222"
                },
                {
                    three = "333"
                },
            ]
        )
    }`,
		},
		"JSON object with list of objects": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"parent":[{"one": "111"}]}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"parent":[{"one": "111"}, {"two": "222"}]}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              ~ parent = [
                    {
                        one = "111"
                    },
                  + {
                      + two = "222"
                    },
                ]
            }
        )
    }`,
		},
		"JSON object double nested lists": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"parent":[{"another_list": ["111"]}]}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`{"parent":[{"another_list": ["111", "222"]}]}`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              ~ parent = [
                  ~ {
                      ~ another_list = [
                            "111",
                          + "222",
                        ]
                    },
                ]
            }
        )
    }`,
		},
		"in-place update from object to tuple": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"json_field": cty.StringVal(`{"aaa": [42, {"foo":"bar"}, "value"]}`),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"json_field": cty.StringVal(`["aaa", 42, "something"]`),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"json_field": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ json_field = jsonencode(
          ~ {
              - aaa = [
                  - 42,
                  - {
                      - foo = "bar"
                    },
                  - "value",
                ]
            } -> [
              + "aaa",
              + 42,
              + "something",
            ]
        )
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_listObject(t *testing.T) {
	testCases := map[string]testCase{
		// https://github.com/hashicorp/terraform/issues/30641
		"updating non-identifying attribute": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"accounts": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"id":     cty.StringVal("1"),
						"name":   cty.StringVal("production"),
						"status": cty.StringVal("ACTIVE"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"id":     cty.StringVal("2"),
						"name":   cty.StringVal("staging"),
						"status": cty.StringVal("ACTIVE"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"id":     cty.StringVal("3"),
						"name":   cty.StringVal("disaster-recovery"),
						"status": cty.StringVal("ACTIVE"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.UnknownVal(cty.String),
				"accounts": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"id":     cty.StringVal("1"),
						"name":   cty.StringVal("production"),
						"status": cty.StringVal("ACTIVE"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"id":     cty.StringVal("2"),
						"name":   cty.StringVal("staging"),
						"status": cty.StringVal("EXPLODED"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"id":     cty.StringVal("3"),
						"name":   cty.StringVal("disaster-recovery"),
						"status": cty.StringVal("ACTIVE"),
					}),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id": {Type: cty.String, Optional: true, Computed: true},
					"accounts": {
						Type: cty.List(cty.Object(map[string]cty.Type{
							"id":     cty.String,
							"name":   cty.String,
							"status": cty.String,
						})),
					},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ accounts = [
            {
                id     = "1"
                name   = "production"
                status = "ACTIVE"
            },
          ~ {
                id     = "2"
                name   = "staging"
              ~ status = "ACTIVE" -> "EXPLODED"
            },
            {
                id     = "3"
                name   = "disaster-recovery"
                status = "ACTIVE"
            },
        ]
      ~ id       = "i-02ae66f368e8518a9" -> (known after apply)
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_primitiveList(t *testing.T) {
	testCases := map[string]testCase{
		"in-place update - creation": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"ami":        cty.StringVal("ami-STATIC"),
				"list_field": cty.NullVal(cty.List(cty.String)),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("new-element"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      + list_field = [
          + "new-element",
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - first addition": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"ami":        cty.StringVal("ami-STATIC"),
				"list_field": cty.ListValEmpty(cty.String),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("new-element"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ list_field = [
          + "new-element",
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("dddd"),
					cty.StringVal("eeee"),
					cty.StringVal("ffff"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
					cty.StringVal("dddd"),
					cty.StringVal("eeee"),
					cty.StringVal("ffff"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ list_field = [
            # (1 unchanged element hidden)
            "bbbb",
          + "cccc",
            "dddd",
            # (2 unchanged elements hidden)
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"force-new update - insertion": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "list_field"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ list_field = [ # forces replacement
            "aaaa",
          + "bbbb",
            "cccc",
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
					cty.StringVal("dddd"),
					cty.StringVal("eeee"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("bbbb"),
					cty.StringVal("dddd"),
					cty.StringVal("eeee"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ list_field = [
          - "aaaa",
            "bbbb",
          - "cccc",
            "dddd",
            # (1 unchanged element hidden)
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"creation - empty list": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"ami":        cty.StringVal("ami-STATIC"),
				"list_field": cty.ListValEmpty(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami        = "ami-STATIC"
      + id         = (known after apply)
      + list_field = []
    }`,
		},
		"in-place update - full to empty": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"ami":        cty.StringVal("ami-STATIC"),
				"list_field": cty.ListValEmpty(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ list_field = [
          - "aaaa",
          - "bbbb",
          - "cccc",
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - null to empty": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"ami":        cty.StringVal("ami-STATIC"),
				"list_field": cty.NullVal(cty.List(cty.String)),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.UnknownVal(cty.String),
				"ami":        cty.StringVal("ami-STATIC"),
				"list_field": cty.ListValEmpty(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      + list_field = []
        # (1 unchanged attribute hidden)
    }`,
		},
		"update to unknown element": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.UnknownVal(cty.String),
					cty.StringVal("cccc"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ list_field = [
            "aaaa",
          ~ "bbbb" -> (known after apply),
            "cccc",
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"update - two new unknown elements": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
					cty.StringVal("dddd"),
					cty.StringVal("eeee"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.UnknownVal(cty.String),
					cty.UnknownVal(cty.String),
					cty.StringVal("cccc"),
					cty.StringVal("dddd"),
					cty.StringVal("eeee"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> (known after apply)
      ~ list_field = [
            "aaaa",
          - "bbbb",
          + (known after apply),
          + (known after apply),
            "cccc",
            # (2 unchanged elements hidden)
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_primitiveTuple(t *testing.T) {
	testCases := map[string]testCase{
		"in-place update": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"tuple_field": cty.TupleVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("dddd"),
					cty.StringVal("eeee"),
					cty.StringVal("ffff"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"tuple_field": cty.TupleVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
					cty.StringVal("eeee"),
					cty.StringVal("ffff"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":          {Type: cty.String, Required: true},
					"tuple_field": {Type: cty.Tuple([]cty.Type{cty.String, cty.String, cty.String, cty.String, cty.String}), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
        id          = "i-02ae66f368e8518a9"
      ~ tuple_field = [
            # (1 unchanged element hidden)
            "bbbb",
          ~ "dddd" -> "cccc",
            "eeee",
            # (1 unchanged element hidden)
        ]
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_primitiveSet(t *testing.T) {
	testCases := map[string]testCase{
		"in-place update - creation": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"ami":       cty.StringVal("ami-STATIC"),
				"set_field": cty.NullVal(cty.Set(cty.String)),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("new-element"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      + set_field = [
          + "new-element",
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - first insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"ami":       cty.StringVal("ami-STATIC"),
				"set_field": cty.SetValEmpty(cty.String),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("new-element"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ set_field = [
          + "new-element",
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ set_field = [
          + "bbbb",
            # (2 unchanged elements hidden)
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"force-new update - insertion": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "set_field"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ set_field = [ # forces replacement
          + "bbbb",
            # (2 unchanged elements hidden)
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
					cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("bbbb"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ set_field = [
          - "aaaa",
          - "cccc",
            # (1 unchanged element hidden)
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"creation - empty set": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.UnknownVal(cty.String),
				"ami":       cty.StringVal("ami-STATIC"),
				"set_field": cty.SetValEmpty(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami       = "ami-STATIC"
      + id        = (known after apply)
      + set_field = []
    }`,
		},
		"in-place update - full to empty set": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.UnknownVal(cty.String),
				"ami":       cty.StringVal("ami-STATIC"),
				"set_field": cty.SetValEmpty(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ set_field = [
          - "aaaa",
          - "bbbb",
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - null to empty set": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"ami":       cty.StringVal("ami-STATIC"),
				"set_field": cty.NullVal(cty.Set(cty.String)),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.UnknownVal(cty.String),
				"ami":       cty.StringVal("ami-STATIC"),
				"set_field": cty.SetValEmpty(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      + set_field = []
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update to unknown": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.UnknownVal(cty.String),
				"ami":       cty.StringVal("ami-STATIC"),
				"set_field": cty.UnknownVal(cty.Set(cty.String)),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ set_field = [
          - "aaaa",
          - "bbbb",
        ] -> (known after apply)
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update to unknown element": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.StringVal("bbbb"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"set_field": cty.SetVal([]cty.Value{
					cty.StringVal("aaaa"),
					cty.UnknownVal(cty.String),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"set_field": {Type: cty.Set(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ set_field = [
          - "bbbb",
          + (known after apply),
            # (1 unchanged element hidden)
        ]
        # (1 unchanged attribute hidden)
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_map(t *testing.T) {
	testCases := map[string]testCase{
		"in-place update - creation": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"ami":       cty.StringVal("ami-STATIC"),
				"map_field": cty.NullVal(cty.Map(cty.String)),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"new-key": cty.StringVal("new-element"),
					"be:ep":   cty.StringVal("boop"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"map_field": {Type: cty.Map(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      + map_field = {
          + "be:ep"   = "boop"
          + "new-key" = "new-element"
        }
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - first insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"ami":       cty.StringVal("ami-STATIC"),
				"map_field": cty.MapValEmpty(cty.String),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"new-key": cty.StringVal("new-element"),
					"be:ep":   cty.StringVal("boop"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"map_field": {Type: cty.Map(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ map_field = {
          + "be:ep"   = "boop"
          + "new-key" = "new-element"
        }
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"a": cty.StringVal("aaaa"),
					"c": cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"a":   cty.StringVal("aaaa"),
					"b":   cty.StringVal("bbbb"),
					"b:b": cty.StringVal("bbbb"),
					"c":   cty.StringVal("cccc"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"map_field": {Type: cty.Map(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ map_field = {
          + "b"   = "bbbb"
          + "b:b" = "bbbb"
            # (2 unchanged elements hidden)
        }
        # (1 unchanged attribute hidden)
    }`,
		},
		"force-new update - insertion": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"a": cty.StringVal("aaaa"),
					"c": cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"a": cty.StringVal("aaaa"),
					"b": cty.StringVal("bbbb"),
					"c": cty.StringVal("cccc"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"map_field": {Type: cty.Map(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "map_field"},
			}),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ map_field = { # forces replacement
          + "b" = "bbbb"
            # (2 unchanged elements hidden)
        }
        # (1 unchanged attribute hidden)
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"a": cty.StringVal("aaaa"),
					"b": cty.StringVal("bbbb"),
					"c": cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"b": cty.StringVal("bbbb"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"map_field": {Type: cty.Map(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ map_field = {
          - "a" = "aaaa" -> null
          - "c" = "cccc" -> null
            # (1 unchanged element hidden)
        }
        # (1 unchanged attribute hidden)
    }`,
		},
		"creation - empty": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.UnknownVal(cty.String),
				"ami":       cty.StringVal("ami-STATIC"),
				"map_field": cty.MapValEmpty(cty.String),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"map_field": {Type: cty.Map(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami       = "ami-STATIC"
      + id        = (known after apply)
      + map_field = {}
    }`,
		},
		"update to unknown element": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"a": cty.StringVal("aaaa"),
					"b": cty.StringVal("bbbb"),
					"c": cty.StringVal("cccc"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.UnknownVal(cty.String),
				"ami": cty.StringVal("ami-STATIC"),
				"map_field": cty.MapVal(map[string]cty.Value{
					"a": cty.StringVal("aaaa"),
					"b": cty.UnknownVal(cty.String),
					"c": cty.StringVal("cccc"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":        {Type: cty.String, Optional: true, Computed: true},
					"ami":       {Type: cty.String, Optional: true},
					"map_field": {Type: cty.Map(cty.String), Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id        = "i-02ae66f368e8518a9" -> (known after apply)
      ~ map_field = {
          ~ "b" = "bbbb" -> (known after apply)
            # (2 unchanged elements hidden)
        }
        # (1 unchanged attribute hidden)
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedList(t *testing.T) {
	testCases := map[string]testCase{
		"in-place update - equal": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

        # (1 unchanged block hidden)
    }`,
		},
		"in-place update - creation": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				})}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.NullVal(cty.String),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          + {
              + mount_point = "/var/diska"
              + size        = "50GB"
            },
        ]
        id    = "i-02ae66f368e8518a9"

      + root_block_device {}
    }`,
		},
		"in-place update - first insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          + {
              + mount_point = "/var/diska"
            },
        ]
        id    = "i-02ae66f368e8518a9"

      + root_block_device {
          + volume_type = "gp2"
        }
    }`,
		},
		"in-place update - insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.NullVal(cty.String),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          ~ {
              + size        = "50GB"
                # (1 unchanged attribute hidden)
            },
            # (1 unchanged element hidden)
        ]
        id    = "i-02ae66f368e8518a9"

      ~ root_block_device {
          + new_field   = "new_value"
            # (1 unchanged attribute hidden)
        }
    }`,
		},
		"force-new update (inside blocks)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("different"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.Path{
					cty.GetAttrStep{Name: "root_block_device"},
					cty.IndexStep{Key: cty.NumberIntVal(0)},
					cty.GetAttrStep{Name: "volume_type"},
				},
				cty.Path{
					cty.GetAttrStep{Name: "disks"},
					cty.IndexStep{Key: cty.NumberIntVal(0)},
					cty.GetAttrStep{Name: "mount_point"},
				},
			),
			Schema: testSchema(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          ~ {
              ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement
                # (1 unchanged attribute hidden)
            },
        ]
        id    = "i-02ae66f368e8518a9"

      ~ root_block_device {
          ~ volume_type = "gp2" -> "different" # forces replacement
        }
    }`,
		},
		"force-new update (whole block)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("different"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
				cty.Path{cty.GetAttrStep{Name: "disks"}},
			),
			Schema: testSchema(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [ # forces replacement
          ~ {
              ~ mount_point = "/var/diska" -> "/var/diskb"
                # (1 unchanged attribute hidden)
            },
        ]
        id    = "i-02ae66f368e8518a9"

      ~ root_block_device { # forces replacement
          ~ volume_type = "gp2" -> "different"
        }
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.ListValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          - {
              - mount_point = "/var/diska" -> null
              - size        = "50GB" -> null
            },
        ]
        id    = "i-02ae66f368e8518a9"

      - root_block_device {
          - volume_type = "gp2" -> null
        }
    }`,
		},
		"with dynamically-typed attribute": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"block": cty.EmptyTupleVal,
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"block": cty.TupleVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"attr": cty.StringVal("foo"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"attr": cty.True,
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"block": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"attr": {Type: cty.DynamicPseudoType, Optional: true},
							},
						},
						Nesting: configschema.NestingList,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      + block {
          + attr = "foo"
        }
      + block {
          + attr = true
        }
    }`,
		},
		"in-place sequence update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"list": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("x")}),
					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"list": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("y")}),
					cty.ObjectVal(map[string]cty.Value{"attr": cty.StringVal("z")}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"list": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"attr": {
									Type:     cty.String,
									Required: true,
								},
							},
						},
						Nesting: configschema.NestingList,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ list {
          ~ attr = "x" -> "y"
        }
      ~ list {
          ~ attr = "y" -> "z"
        }
    }`,
		},
		"in-place update - unknown": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          - {
              - mount_point = "/var/diska" -> null
              - size        = "50GB" -> null
            },
        ] -> (known after apply)
        id    = "i-02ae66f368e8518a9"

        # (1 unchanged block hidden)
    }`,
		},
		"in-place create - unknown block": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(testSchemaPlus(configschema.NestingList).ImpliedType()),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami   = "ami-AFTER"
      + disks = [
          + {
              + mount_point = "/var/diska"
              + size        = "50GB"
            },
        ]
      + id    = "i-02ae66f368e8518a9"

      + root_block_device (known after apply)
    }`,
		},
		"in-place update - unknown block": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp1"),
						"new_field":   cty.StringVal("new_value"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device (known after apply)
      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp1" -> null
        }
      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp2" -> null
        }
    }`,
		},
		"in-place update - unknown block with replace": {
			Action: plans.CreateThenDelete,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp1"),
						"new_field":   cty.StringVal("new_value"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.GetAttrPath("root_block_device").IndexInt(0).GetAttr("volume_type"),
				cty.GetAttrPath("root_block_device").IndexInt(1).GetAttr("volume_type"),
			),
			Schema: testSchemaPlus(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example must be replaced
+/- resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device (known after apply)
      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp1" -> null # forces replacement
        }
      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp2" -> null # forces replacement
        }
    }`,
		},
		"in-place update - modification": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("50GB"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskc"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("75GB"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskc"),
						"size":        cty.StringVal("25GB"),
					}),
				}),
				"root_block_device": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          ~ {
              ~ size        = "50GB" -> "75GB"
                # (1 unchanged attribute hidden)
            },
          ~ {
              ~ size        = "50GB" -> "25GB"
                # (1 unchanged attribute hidden)
            },
            # (1 unchanged element hidden)
        ]
        id    = "i-02ae66f368e8518a9"

        # (1 unchanged block hidden)
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedSet(t *testing.T) {
	testCases := map[string]testCase{
		"creation from null - sensitive set": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.Object(map[string]cty.Type{
				"id":  cty.String,
				"ami": cty.String,
				"disks": cty.Set(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.Set(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			})),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			AfterSensitivePaths: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "disks"}},
			},
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami   = "ami-AFTER"
      + disks = (sensitive value)
      + id    = "i-02ae66f368e8518a9"

      + root_block_device {
          + volume_type = "gp2"
        }
    }`,
		},
		"in-place update - creation": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          + {
              + mount_point = "/var/diska"
            },
        ]
        id    = "i-02ae66f368e8518a9"

      + root_block_device {
          + volume_type = "gp2"
        }
    }`,
		},
		"in-place update - creation - sensitive set": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			AfterSensitivePaths: []cty.Path{
				cty.GetAttrPath("disks"),
			},
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      # Warning: this attribute value will be marked as sensitive and will not
      # display in UI output after applying this change.
      ~ disks = (sensitive value)
        id    = "i-02ae66f368e8518a9"

      + root_block_device {
          + volume_type = "gp2"
        }
    }`,
		},
		"in-place update - marking set sensitive": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			}),
			AfterSensitivePaths: []cty.Path{
				cty.GetAttrPath("disks"),
			},
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      # Warning: this attribute value will be marked as sensitive and will not
      # display in UI output after applying this change. The value is unchanged.
      ~ disks = (sensitive value)
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update - insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("100GB"),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.NullVal(cty.String),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("100GB"),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          - {
              - mount_point = "/var/diska" -> null
            },
          + {
              + mount_point = "/var/diska"
              + size        = "50GB"
            },
            # (1 unchanged element hidden)
        ]
        id    = "i-02ae66f368e8518a9"

      - root_block_device {
          - volume_type = "gp2" -> null
        }
      + root_block_device {
          + new_field   = "new_value"
          + volume_type = "gp2"
        }
    }`,
		},
		"force-new update (whole block)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("different"),
					}),
				}),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diskb"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
				cty.Path{cty.GetAttrStep{Name: "disks"}},
			),
			Schema: testSchema(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          - { # forces replacement
              - mount_point = "/var/diska" -> null
              - size        = "50GB" -> null
            },
          + { # forces replacement
              + mount_point = "/var/diskb"
              + size        = "50GB"
            },
        ]
        id    = "i-02ae66f368e8518a9"

      - root_block_device { # forces replacement
          - volume_type = "gp2" -> null
        }
      + root_block_device { # forces replacement
          + volume_type = "different"
        }
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				})),
				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          - {
              - mount_point = "/var/diska" -> null
              - size        = "50GB" -> null
            },
        ]
        id    = "i-02ae66f368e8518a9"

      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp2" -> null
        }
    }`,
		},
		"in-place update - empty nested sets": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      + disks = []
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update - null insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.NullVal(cty.String),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      + disks = [
          + {
              + mount_point = "/var/diska"
              + size        = "50GB"
            },
        ]
        id    = "i-02ae66f368e8518a9"

      - root_block_device {
          - volume_type = "gp2" -> null
        }
      + root_block_device {
          + new_field   = "new_value"
          + volume_type = "gp2"
        }
    }`,
		},
		"in-place update - unknown": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = [
          - {
              - mount_point = "/var/diska" -> null
              - size        = "50GB" -> null
            },
        ] -> (known after apply)
        id    = "i-02ae66f368e8518a9"

        # (1 unchanged block hidden)
    }`,
		},
		"in-place create - unknown block": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(testSchemaPlus(configschema.NestingSet).ImpliedType()),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami   = "ami-AFTER"
      + disks = [
          + {
              + mount_point = "/var/diska"
              + size        = "50GB"
            },
        ]
      + id    = "i-02ae66f368e8518a9"

      + root_block_device (known after apply)
    }`,
		},
		"in-place update - unknown block": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp1"),
						"new_field":   cty.StringVal("new_value"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device (known after apply)
      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp1" -> null
        }
      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp2" -> null
        }
    }`,
		},
		"in-place update - unknown block with replace": {
			Action: plans.CreateThenDelete,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp1"),
						"new_field":   cty.StringVal("new_value"),
					}),
					cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.GetAttrPath("root_block_device").IndexInt(0).GetAttr("volume_type"),
				cty.GetAttrPath("root_block_device").IndexInt(1).GetAttr("volume_type"),
			),
			Schema: testSchemaPlus(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example must be replaced
+/- resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device (known after apply)
      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp1" -> null # forces replacement
        }
      - root_block_device {
          - new_field   = "new_value" -> null
          - volume_type = "gp2" -> null # forces replacement
        }
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedMap(t *testing.T) {
	testCases := map[string]testCase{
		"creation from null": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.NullVal(cty.String),
				"ami": cty.NullVal(cty.String),
				"disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
				"root_block_device": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				}))),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      + ami   = "ami-AFTER"
      + disks = {
          + "disk_a" = {
              + mount_point = "/var/diska"
            },
        }
      + id    = "i-02ae66f368e8518a9"

      + root_block_device "a" {
          + volume_type = "gp2"
        }
    }`,
		},
		"in-place update - creation": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = {
          + "disk_a" = {
              + mount_point = "/var/diska"
            },
        }
        id    = "i-02ae66f368e8518a9"

      + root_block_device "a" {
          + volume_type = "gp2"
        }
    }`,
		},
		"in-place update - change attr": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.NullVal(cty.String),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = {
          ~ "disk_a" = {
              + size        = "50GB"
                # (1 unchanged attribute hidden)
            },
        }
        id    = "i-02ae66f368e8518a9"

      ~ root_block_device "a" {
          + new_field   = "new_value"
            # (1 unchanged attribute hidden)
        }
    }`,
		},
		"in-place update - insertion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.NullVal(cty.String),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
					"disk_2": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/disk2"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.NullVal(cty.String),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = {
          + "disk_2" = {
              + mount_point = "/var/disk2"
              + size        = "50GB"
            },
            # (1 unchanged element hidden)
        }
        id    = "i-02ae66f368e8518a9"

      + root_block_device "b" {
          + new_field   = "new_value"
          + volume_type = "gp2"
        }

        # (1 unchanged block hidden)
    }`,
		},
		"force-new update (whole block)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("standard"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("100GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("different"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("standard"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(cty.Path{
				cty.GetAttrStep{Name: "root_block_device"},
				cty.IndexStep{Key: cty.StringVal("a")},
			},
				cty.Path{cty.GetAttrStep{Name: "disks"}},
			),
			Schema: testSchema(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = {
          ~ "disk_a" = { # forces replacement
              ~ size        = "50GB" -> "100GB"
                # (1 unchanged attribute hidden)
            },
        }
        id    = "i-02ae66f368e8518a9"

      ~ root_block_device "a" { # forces replacement
          ~ volume_type = "gp2" -> "different"
        }

        # (1 unchanged block hidden)
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				})),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = {
          - "disk_a" = {
              - mount_point = "/var/diska" -> null
              - size        = "50GB" -> null
            },
        }
        id    = "i-02ae66f368e8518a9"

      - root_block_device "a" {
          - new_field   = "new_value" -> null
          - volume_type = "gp2" -> null
        }
    }`,
		},
		"in-place update - unknown": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = {
          - "disk_a" = {
              - mount_point = "/var/diska" -> null
              - size        = "50GB" -> null
            },
        } -> (known after apply)
        id    = "i-02ae66f368e8518a9"

        # (1 unchanged block hidden)
    }`,
		},
		"in-place create - unknown block": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(testSchemaPlus(configschema.NestingMap).ImpliedType()),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"diska": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami   = "ami-AFTER"
      + disks = {
          + "diska" = {
              + mount_point = "/var/diska"
              + size        = "50GB"
            },
        }
      + id    = "i-02ae66f368e8518a9"

      + root_block_device (known after apply)
    }`,
		},
		"in-place update - unknown block": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"diska": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"gp1": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp1"),
						"new_field":   cty.StringVal("new_value"),
					}),
					"gp2": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"diska": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device (known after apply)
      - root_block_device "gp1" {
          - new_field   = "new_value" -> null
          - volume_type = "gp1" -> null
        }
      - root_block_device "gp2" {
          - new_field   = "new_value" -> null
          - volume_type = "gp2" -> null
        }
    }`,
		},
		"in-place update - unknown block with replace": {
			Action: plans.CreateThenDelete,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"diska": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"gp1": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp1"),
						"new_field":   cty.StringVal("new_value"),
					}),
					"gp2": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"diska": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.GetAttrPath("root_block_device").IndexString("gp1").GetAttr("volume_type"),
				cty.GetAttrPath("root_block_device").IndexString("gp2").GetAttr("volume_type"),
			),
			Schema: testSchemaPlus(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example must be replaced
+/- resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device (known after apply)
      - root_block_device "gp1" {
          - new_field   = "new_value" -> null
          - volume_type = "gp1" -> null # forces replacement
        }
      - root_block_device "gp2" {
          - new_field   = "new_value" -> null
          - volume_type = "gp2" -> null # forces replacement
        }
    }`,
		},
		"in-place update - insertion sensitive": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
						"new_field":   cty.StringVal("new_value"),
					}),
				}),
			}),
			AfterSensitivePaths: []cty.Path{
				cty.GetAttrPath("disks").IndexString("disk_a").GetAttr("mount_point"),
			},
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = {
          + "disk_a" = {
              + mount_point = (sensitive value)
              + size        = "50GB"
            },
        }
        id    = "i-02ae66f368e8518a9"

        # (1 unchanged block hidden)
    }`,
		},
		"in-place update - multiple unchanged blocks": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

        # (2 unchanged blocks hidden)
    }`,
		},
		"in-place update - multiple blocks first changed": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device "b" {
          ~ volume_type = "gp2" -> "gp3"
        }

        # (1 unchanged block hidden)
    }`,
		},
		"in-place update - multiple blocks second changed": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device "a" {
          ~ volume_type = "gp2" -> "gp3"
        }

        # (1 unchanged block hidden)
    }`,
		},
		"in-place update - multiple blocks changed": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device "a" {
          ~ volume_type = "gp2" -> "gp3"
        }
      ~ root_block_device "b" {
          ~ volume_type = "gp2" -> "gp3"
        }
    }`,
		},
		"in-place update - multiple different unchanged blocks": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

        # (2 unchanged blocks hidden)
    }`,
		},
		"in-place update - multiple different blocks first changed": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ leaf_block_device "b" {
          ~ volume_type = "gp2" -> "gp3"
        }

        # (1 unchanged block hidden)
    }`,
		},
		"in-place update - multiple different blocks second changed": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device "a" {
          ~ volume_type = "gp2" -> "gp3"
        }

        # (1 unchanged block hidden)
    }`,
		},
		"in-place update - multiple different blocks changed": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ leaf_block_device "b" {
          ~ volume_type = "gp2" -> "gp3"
        }

      ~ root_block_device "a" {
          ~ volume_type = "gp2" -> "gp3"
        }
    }`,
		},
		"in-place update - mixed blocks unchanged": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

        # (4 unchanged blocks hidden)
    }`,
		},
		"in-place update - mixed blocks changed": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
				"root_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
				}),
				"leaf_block_device": cty.MapVal(map[string]cty.Value{
					"a": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp2"),
					}),
					"b": cty.ObjectVal(map[string]cty.Value{
						"volume_type": cty.StringVal("gp3"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaMultipleBlocks(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
        id    = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ leaf_block_device "b" {
          ~ volume_type = "gp2" -> "gp3"
        }

      ~ root_block_device "b" {
          ~ volume_type = "gp2" -> "gp3"
        }

        # (2 unchanged blocks hidden)
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedSingle(t *testing.T) {
	testCases := map[string]testCase{
		"in-place update - equal": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
				}),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
				}),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
        id   = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

        # (1 unchanged block hidden)
    }`,
		},
		"in-place update - creation": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
				"disk": cty.NullVal(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.NullVal(cty.String),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
      + disk = {
          + mount_point = "/var/diska"
          + size        = "50GB"
        }
        id   = "i-02ae66f368e8518a9"

      + root_block_device {}
    }`,
		},
		"force-new update (inside blocks)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diskb"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("different"),
				}),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.Path{
					cty.GetAttrStep{Name: "root_block_device"},
					cty.GetAttrStep{Name: "volume_type"},
				},
				cty.Path{
					cty.GetAttrStep{Name: "disk"},
					cty.GetAttrStep{Name: "mount_point"},
				},
			),
			Schema: testSchema(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
      ~ disk = {
          ~ mount_point = "/var/diska" -> "/var/diskb" # forces replacement
            # (1 unchanged attribute hidden)
        }
        id   = "i-02ae66f368e8518a9"

      ~ root_block_device {
          ~ volume_type = "gp2" -> "different" # forces replacement
        }
    }`,
		},
		"force-new update (whole block)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diskb"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("different"),
				}),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.Path{cty.GetAttrStep{Name: "root_block_device"}},
				cty.Path{cty.GetAttrStep{Name: "disk"}},
			),
			Schema: testSchema(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
      ~ disk = { # forces replacement
          ~ mount_point = "/var/diska" -> "/var/diskb"
            # (1 unchanged attribute hidden)
        }
        id   = "i-02ae66f368e8518a9"

      ~ root_block_device { # forces replacement
          ~ volume_type = "gp2" -> "different"
        }
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
				})),
				"disk": cty.NullVal(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchema(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
      - disk = {
          - mount_point = "/var/diska" -> null
          - size        = "50GB" -> null
        } -> null
        id   = "i-02ae66f368e8518a9"

      - root_block_device {
          - volume_type = "gp2" -> null
        }
    }`,
		},
		"with dynamically-typed attribute": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"block": cty.NullVal(cty.Object(map[string]cty.Type{
					"attr": cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"block": cty.ObjectVal(map[string]cty.Value{
					"attr": cty.StringVal("foo"),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				BlockTypes: map[string]*configschema.NestedBlock{
					"block": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"attr": {Type: cty.DynamicPseudoType, Optional: true},
							},
						},
						Nesting: configschema.NestingSingle,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      + block {
          + attr = "foo"
        }
    }`,
		},
		"in-place update - unknown": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
					"new_field":   cty.StringVal("new_value"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disk": cty.UnknownVal(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
					"new_field":   cty.StringVal("new_value"),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
      ~ disk = {
          ~ mount_point = "/var/diska" -> (known after apply)
          ~ size        = "50GB" -> (known after apply)
        } -> (known after apply)
        id   = "i-02ae66f368e8518a9"

        # (1 unchanged block hidden)
    }`,
		},
		"in-place create - unknown block": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(testSchemaPlus(configschema.NestingSingle).ImpliedType()),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				})),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami  = "ami-AFTER"
      + disk = {
          + mount_point = "/var/diska"
          + size        = "50GB"
        }
      + id   = "i-02ae66f368e8518a9"

      + root_block_device (known after apply)
    }`,
		},
		"in-place update - unknown block": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp1"),
					"new_field":   cty.StringVal("new_value"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				})),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
        id   = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device {
          ~ new_field   = "new_value" -> (known after apply)
          ~ volume_type = "gp1" -> (known after apply)
        } -> (known after apply)
    }`,
		},
		"in-place update - unknown block with replace": {
			Action: plans.CreateThenDelete,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp1"),
					"new_field":   cty.StringVal("new_value"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{
					"volume_type": cty.String,
					"new_field":   cty.String,
				})),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.GetAttrPath("root_block_device").GetAttr("volume_type"),
			),
			Schema: testSchemaPlus(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example must be replaced
+/- resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
        id   = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      ~ root_block_device {
          ~ new_field   = "new_value" -> (known after apply)
          ~ volume_type = "gp1" -> (known after apply) # forces replacement
        } -> (known after apply)
    }`,
		},
		"in-place update - modification": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("50GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
					"new_field":   cty.StringVal("new_value"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disk": cty.ObjectVal(map[string]cty.Value{
					"mount_point": cty.StringVal("/var/diska"),
					"size":        cty.StringVal("25GB"),
				}),
				"root_block_device": cty.ObjectVal(map[string]cty.Value{
					"volume_type": cty.StringVal("gp2"),
					"new_field":   cty.StringVal("new_value"),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaPlus(configschema.NestingSingle),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami  = "ami-BEFORE" -> "ami-AFTER"
      ~ disk = {
          ~ size        = "50GB" -> "25GB"
            # (1 unchanged attribute hidden)
        }
        id   = "i-02ae66f368e8518a9"

        # (1 unchanged block hidden)
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedMapSensitiveSchema(t *testing.T) {
	testCases := map[string]testCase{
		"creation from null": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.NullVal(cty.String),
				"ami": cty.NullVal(cty.String),
				"disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      + ami   = "ami-AFTER"
      + disks = (sensitive value)
      + id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value)
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"force-new update (whole block)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("100GB"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.Path{cty.GetAttrStep{Name: "disks"}},
			),
			Schema: testSchemaSensitive(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value) # forces replacement
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      - disks = (sensitive value) -> null
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update - unknown": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.MapVal(map[string]cty.Value{
					"disk_a": cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.UnknownVal(cty.Map(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingMap),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value)
        id    = "i-02ae66f368e8518a9"
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedListSensitiveSchema(t *testing.T) {
	testCases := map[string]testCase{
		"creation from null": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.NullVal(cty.String),
				"ami": cty.NullVal(cty.String),
				"disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      + ami   = "ami-AFTER"
      + disks = (sensitive value)
      + id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value)
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"force-new update (whole block)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("100GB"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.Path{cty.GetAttrStep{Name: "disks"}},
			),
			Schema: testSchemaSensitive(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value) # forces replacement
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      - disks = (sensitive value) -> null
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update - unknown": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.UnknownVal(cty.List(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingList),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value)
        id    = "i-02ae66f368e8518a9"
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedSetSensitiveSchema(t *testing.T) {
	testCases := map[string]testCase{
		"creation from null": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.NullVal(cty.String),
				"ami": cty.NullVal(cty.String),
				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      + ami   = "ami-AFTER"
      + disks = (sensitive value)
      + id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetValEmpty(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				})),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.NullVal(cty.String),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value)
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"force-new update (whole block)": {
			Action:       plans.DeleteThenCreate,
			ActionReason: plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:         addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("100GB"),
					}),
				}),
			}),
			RequiredReplace: cty.NewPathSet(
				cty.Path{cty.GetAttrStep{Name: "disks"}},
			),
			Schema: testSchemaSensitive(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value) # forces replacement
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update - deletion": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      - disks = (sensitive value) -> null
        id    = "i-02ae66f368e8518a9"
    }`,
		},
		"in-place update - unknown": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"disks": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"mount_point": cty.StringVal("/var/diska"),
						"size":        cty.StringVal("50GB"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"disks": cty.UnknownVal(cty.Set(cty.Object(map[string]cty.Type{
					"mount_point": cty.String,
					"size":        cty.String,
				}))),
			}),
			RequiredReplace: cty.NewPathSet(),
			Schema:          testSchemaSensitive(configschema.NestingSet),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami   = "ami-BEFORE" -> "ami-AFTER"
      ~ disks = (sensitive value)
        id    = "i-02ae66f368e8518a9"
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_actionReason(t *testing.T) {
	emptySchema := &configschema.Block{}
	nullVal := cty.NullVal(cty.EmptyObject)
	emptyVal := cty.EmptyObjectVal

	testCases := map[string]testCase{
		"delete for no particular reason": {
			Action:          plans.Delete,
			ActionReason:    plans.ResourceInstanceChangeNoReason,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be destroyed
  - resource "test_instance" "example" {}`,
		},
		"delete because of wrong repetition mode (NoKey)": {
			Action:          plans.Delete,
			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
			Mode:            addrs.ManagedResourceMode,
			InstanceKey:     addrs.NoKey,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be destroyed
  # (because resource uses count or for_each)
  - resource "test_instance" "example" {}`,
		},
		"delete because of wrong repetition mode (IntKey)": {
			Action:          plans.Delete,
			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
			Mode:            addrs.ManagedResourceMode,
			InstanceKey:     addrs.IntKey(1),
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example[1] will be destroyed
  # (because resource does not use count)
  - resource "test_instance" "example" {}`,
		},
		"delete because of wrong repetition mode (StringKey)": {
			Action:          plans.Delete,
			ActionReason:    plans.ResourceInstanceDeleteBecauseWrongRepetition,
			Mode:            addrs.ManagedResourceMode,
			InstanceKey:     addrs.StringKey("a"),
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example["a"] will be destroyed
  # (because resource does not use for_each)
  - resource "test_instance" "example" {}`,
		},
		"delete because no resource configuration": {
			Action:          plans.Delete,
			ActionReason:    plans.ResourceInstanceDeleteBecauseNoResourceConfig,
			ModuleInst:      addrs.RootModuleInstance.Child("foo", addrs.NoKey),
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # module.foo.test_instance.example will be destroyed
  # (because test_instance.example is not in configuration)
  - resource "test_instance" "example" {}`,
		},
		"delete because no module": {
			Action:          plans.Delete,
			ActionReason:    plans.ResourceInstanceDeleteBecauseNoModule,
			ModuleInst:      addrs.RootModuleInstance.Child("foo", addrs.IntKey(1)),
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # module.foo[1].test_instance.example will be destroyed
  # (because module.foo[1] is not in configuration)
  - resource "test_instance" "example" {}`,
		},
		"delete because out of range for count": {
			Action:          plans.Delete,
			ActionReason:    plans.ResourceInstanceDeleteBecauseCountIndex,
			Mode:            addrs.ManagedResourceMode,
			InstanceKey:     addrs.IntKey(1),
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example[1] will be destroyed
  # (because index [1] is out of range for count)
  - resource "test_instance" "example" {}`,
		},
		"delete because out of range for for_each": {
			Action:          plans.Delete,
			ActionReason:    plans.ResourceInstanceDeleteBecauseEachKey,
			Mode:            addrs.ManagedResourceMode,
			InstanceKey:     addrs.StringKey("boop"),
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example["boop"] will be destroyed
  # (because key ["boop"] is not in for_each map)
  - resource "test_instance" "example" {}`,
		},
		"replace for no particular reason (delete first)": {
			Action:          plans.DeleteThenCreate,
			ActionReason:    plans.ResourceInstanceChangeNoReason,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {}`,
		},
		"replace for no particular reason (create first)": {
			Action:          plans.CreateThenDelete,
			ActionReason:    plans.ResourceInstanceChangeNoReason,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example must be replaced
+/- resource "test_instance" "example" {}`,
		},
		"replace by request (delete first)": {
			Action:          plans.DeleteThenCreate,
			ActionReason:    plans.ResourceInstanceReplaceByRequest,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be replaced, as requested
-/+ resource "test_instance" "example" {}`,
		},
		"replace by request (create first)": {
			Action:          plans.CreateThenDelete,
			ActionReason:    plans.ResourceInstanceReplaceByRequest,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be replaced, as requested
+/- resource "test_instance" "example" {}`,
		},
		"replace because tainted (delete first)": {
			Action:          plans.DeleteThenCreate,
			ActionReason:    plans.ResourceInstanceReplaceBecauseTainted,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
-/+ resource "test_instance" "example" {}`,
		},
		"replace because tainted (create first)": {
			Action:          plans.CreateThenDelete,
			ActionReason:    plans.ResourceInstanceReplaceBecauseTainted,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example is tainted, so must be replaced
+/- resource "test_instance" "example" {}`,
		},
		"replace because cannot update (delete first)": {
			Action:          plans.DeleteThenCreate,
			ActionReason:    plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			// This one has no special message, because the fuller explanation
			// typically appears inline as a "# forces replacement" comment.
			// (not shown here)
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {}`,
		},
		"replace because cannot update (create first)": {
			Action:          plans.CreateThenDelete,
			ActionReason:    plans.ResourceInstanceReplaceBecauseCannotUpdate,
			Mode:            addrs.ManagedResourceMode,
			Before:          emptyVal,
			After:           nullVal,
			Schema:          emptySchema,
			RequiredReplace: cty.NewPathSet(),
			// This one has no special message, because the fuller explanation
			// typically appears inline as a "# forces replacement" comment.
			// (not shown here)
			ExpectedOutput: `  # test_instance.example must be replaced
+/- resource "test_instance" "example" {}`,
		},
	}

	runTestCases(t, testCases)
}

func TestResourceChange_sensitiveVariable(t *testing.T) {
	testCases := map[string]testCase{
		"creation": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.EmptyObject),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-123"),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(2000),
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("pizza"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("friends"),
					cty.StringVal("!"),
				}),
				"nested_block_list": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secretval"),
						"another": cty.StringVal("not secret"),
					}),
				}),
				"nested_block_set": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secretval"),
						"another": cty.StringVal("not secret"),
					}),
				}),
			}),
			AfterSensitivePaths: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "ami"}},
				cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
				cty.Path{cty.GetAttrStep{Name: "map_whole"}},
				cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
				// Nested blocks/sets will mark the whole set/block as sensitive
				cty.Path{cty.GetAttrStep{Name: "nested_block_list"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
			},
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"nested_block_list": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
								"another": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingList,
					},
					"nested_block_set": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
								"another": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingSet,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + ami        = (sensitive value)
      + id         = "i-02ae66f368e8518a9"
      + list_field = [
          + "hello",
          + (sensitive value),
          + "!",
        ]
      + map_key    = {
          + "breakfast" = 800
          + "dinner"    = (sensitive value)
        }
      + map_whole  = (sensitive value)

      + nested_block_list {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }

      + nested_block_set {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
    }`,
		},
		"in-place update - before sensitive": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":          cty.StringVal("i-02ae66f368e8518a9"),
				"ami":         cty.StringVal("ami-BEFORE"),
				"special":     cty.BoolVal(true),
				"some_number": cty.NumberIntVal(1),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("friends"),
					cty.StringVal("!"),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(2000), // sensitive key
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("pizza"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secretval"),
					}),
				}),
				"nested_block_set": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secretval"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":          cty.StringVal("i-02ae66f368e8518a9"),
				"ami":         cty.StringVal("ami-AFTER"),
				"special":     cty.BoolVal(false),
				"some_number": cty.NumberIntVal(2),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("friends"),
					cty.StringVal("."),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(1900),
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("cereal"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("changed"),
					}),
				}),
				"nested_block_set": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("changed"),
					}),
				}),
			}),
			BeforeSensitivePaths: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "ami"}},
				cty.Path{cty.GetAttrStep{Name: "special"}},
				cty.Path{cty.GetAttrStep{Name: "some_number"}},
				cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
				cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
				cty.Path{cty.GetAttrStep{Name: "map_whole"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
			},
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":          {Type: cty.String, Optional: true, Computed: true},
					"ami":         {Type: cty.String, Optional: true},
					"list_field":  {Type: cty.List(cty.String), Optional: true},
					"special":     {Type: cty.Bool, Optional: true},
					"some_number": {Type: cty.Number, Optional: true},
					"map_key":     {Type: cty.Map(cty.Number), Optional: true},
					"map_whole":   {Type: cty.Map(cty.String), Optional: true},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"nested_block": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingList,
					},
					"nested_block_set": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingSet,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change.
      ~ ami         = (sensitive value)
        id          = "i-02ae66f368e8518a9"
      ~ list_field  = [
            # (1 unchanged element hidden)
            "friends",
          # Warning: this attribute value will no longer be marked as sensitive
          # after applying this change.
          ~ (sensitive value),
        ]
      ~ map_key     = {
          # Warning: this attribute value will no longer be marked as sensitive
          # after applying this change.
          ~ "dinner"    = (sensitive value)
            # (1 unchanged element hidden)
        }
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change.
      ~ map_whole   = (sensitive value)
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change.
      ~ some_number = (sensitive value)
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change.
      ~ special     = (sensitive value)

      # Warning: this block will no longer be marked as sensitive
      # after applying this change.
      ~ nested_block {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }

      - nested_block_set {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + nested_block_set {
          + an_attr = "changed"
        }
    }`,
		},
		"in-place update - after sensitive": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("friends"),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(2000), // sensitive key
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("pizza"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block_single": cty.ObjectVal(map[string]cty.Value{
					"an_attr": cty.StringVal("original"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("goodbye"),
					cty.StringVal("friends"),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(700),
					"dinner":    cty.NumberIntVal(2100), // sensitive key
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("cereal"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block_single": cty.ObjectVal(map[string]cty.Value{
					"an_attr": cty.StringVal("changed"),
				}),
			}),
			AfterSensitivePaths: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}},
				cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
				cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
				cty.Path{cty.GetAttrStep{Name: "map_whole"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block_single"}},
			},
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"nested_block_single": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingSingle,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
        id         = "i-02ae66f368e8518a9"
      ~ list_field = [
          # Warning: this attribute value will be marked as sensitive and will not
          # display in UI output after applying this change.
          ~ (sensitive value),
            "friends",
        ]
      ~ map_key    = {
          ~ "breakfast" = 800 -> 700
          # Warning: this attribute value will be marked as sensitive and will not
          # display in UI output after applying this change.
          ~ "dinner"    = (sensitive value)
        }
      # Warning: this attribute value will be marked as sensitive and will not
      # display in UI output after applying this change.
      ~ map_whole  = (sensitive value)

      # Warning: this block will be marked as sensitive and will not
      # display in UI output after applying this change.
      ~ nested_block_single {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
    }`,
		},
		"in-place update - both sensitive": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("friends"),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(2000), // sensitive key
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("pizza"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block_map": cty.MapVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("original"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("goodbye"),
					cty.StringVal("friends"),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(1800), // sensitive key
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("cereal"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block_map": cty.MapVal(map[string]cty.Value{
					"foo": cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.UnknownVal(cty.String),
					}),
				}),
			}),
			BeforeSensitivePaths: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "ami"}},
				cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
				cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
				cty.Path{cty.GetAttrStep{Name: "map_whole"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
			},
			AfterSensitivePaths: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "ami"}},
				cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
				cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
				cty.Path{cty.GetAttrStep{Name: "map_whole"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
			},
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"nested_block_map": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingMap,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami        = (sensitive value)
        id         = "i-02ae66f368e8518a9"
      ~ list_field = [
          ~ (sensitive value),
            "friends",
        ]
      ~ map_key    = {
          ~ "dinner"    = (sensitive value)
            # (1 unchanged element hidden)
        }
      ~ map_whole  = (sensitive value)

      ~ nested_block_map "foo" {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
    }`,
		},
		"in-place update - value unchanged, sensitivity changes": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":          cty.StringVal("i-02ae66f368e8518a9"),
				"ami":         cty.StringVal("ami-BEFORE"),
				"special":     cty.BoolVal(true),
				"some_number": cty.NumberIntVal(1),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("friends"),
					cty.StringVal("!"),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(2000), // sensitive key
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("pizza"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secretval"),
					}),
				}),
				"nested_block_set": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secretval"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":          cty.StringVal("i-02ae66f368e8518a9"),
				"ami":         cty.StringVal("ami-BEFORE"),
				"special":     cty.BoolVal(true),
				"some_number": cty.NumberIntVal(1),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("friends"),
					cty.StringVal("!"),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(2000), // sensitive key
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("pizza"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secretval"),
					}),
				}),
				"nested_block_set": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secretval"),
					}),
				}),
			}),
			BeforeSensitivePaths: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "ami"}},
				cty.Path{cty.GetAttrStep{Name: "special"}},
				cty.Path{cty.GetAttrStep{Name: "some_number"}},
				cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
				cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
				cty.Path{cty.GetAttrStep{Name: "map_whole"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
			},
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":          {Type: cty.String, Optional: true, Computed: true},
					"ami":         {Type: cty.String, Optional: true},
					"list_field":  {Type: cty.List(cty.String), Optional: true},
					"special":     {Type: cty.Bool, Optional: true},
					"some_number": {Type: cty.Number, Optional: true},
					"map_key":     {Type: cty.Map(cty.Number), Optional: true},
					"map_whole":   {Type: cty.Map(cty.String), Optional: true},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"nested_block": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingList,
					},
					"nested_block_set": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingSet,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change. The value is unchanged.
      ~ ami         = (sensitive value)
        id          = "i-02ae66f368e8518a9"
      ~ list_field  = [
            # (1 unchanged element hidden)
            "friends",
          # Warning: this attribute value will no longer be marked as sensitive
          # after applying this change. The value is unchanged.
          ~ (sensitive value),
        ]
      ~ map_key     = {
          # Warning: this attribute value will no longer be marked as sensitive
          # after applying this change. The value is unchanged.
          ~ "dinner"    = (sensitive value)
            # (1 unchanged element hidden)
        }
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change. The value is unchanged.
      ~ map_whole   = (sensitive value)
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change. The value is unchanged.
      ~ some_number = (sensitive value)
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change. The value is unchanged.
      ~ special     = (sensitive value)

      # Warning: this block will no longer be marked as sensitive
      # after applying this change.
      ~ nested_block {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }

      # Warning: this block will no longer be marked as sensitive
      # after applying this change.
      ~ nested_block_set {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
    }`,
		},
		"deletion": {
			Action: plans.Delete,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"list_field": cty.ListVal([]cty.Value{
					cty.StringVal("hello"),
					cty.StringVal("friends"),
				}),
				"map_key": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.NumberIntVal(800),
					"dinner":    cty.NumberIntVal(2000), // sensitive key
				}),
				"map_whole": cty.MapVal(map[string]cty.Value{
					"breakfast": cty.StringVal("pizza"),
					"dinner":    cty.StringVal("pizza"),
				}),
				"nested_block": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secret"),
						"another": cty.StringVal("not secret"),
					}),
				}),
				"nested_block_set": cty.ListVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secret"),
						"another": cty.StringVal("not secret"),
					}),
				}),
			}),
			After: cty.NullVal(cty.EmptyObject),
			BeforeSensitivePaths: []cty.Path{
				cty.Path{cty.GetAttrStep{Name: "ami"}},
				cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
				cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
				cty.Path{cty.GetAttrStep{Name: "map_whole"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block"}},
				cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
			},
			RequiredReplace: cty.NewPathSet(),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"ami":        {Type: cty.String, Optional: true},
					"list_field": {Type: cty.List(cty.String), Optional: true},
					"map_key":    {Type: cty.Map(cty.Number), Optional: true},
					"map_whole":  {Type: cty.Map(cty.String), Optional: true},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"nested_block_set": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Optional: true},
								"another": {Type: cty.String, Optional: true},
							},
						},
						Nesting: configschema.NestingSet,
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be destroyed
  - resource "test_instance" "example" {
      - ami        = (sensitive value) -> null
      - id         = "i-02ae66f368e8518a9" -> null
      - list_field = [
          - "hello",
          - (sensitive value),
        ] -> null
      - map_key    = {
          - "breakfast" = 800
          - "dinner"    = (sensitive value)
        } -> null
      - map_whole  = (sensitive value) -> null

      - nested_block_set {
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
    }`,
		},
		"update with sensitive value forcing replacement": {
			Action: plans.DeleteThenCreate,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
				"nested_block_set": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("secret"),
					}),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
				"nested_block_set": cty.SetVal([]cty.Value{
					cty.ObjectVal(map[string]cty.Value{
						"an_attr": cty.StringVal("changed"),
					}),
				}),
			}),
			BeforeSensitivePaths: []cty.Path{
				cty.GetAttrPath("ami"),
				cty.GetAttrPath("nested_block_set"),
			},
			AfterSensitivePaths: []cty.Path{
				cty.GetAttrPath("ami"),
				cty.GetAttrPath("nested_block_set"),
			},
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Optional: true, Computed: true},
					"ami": {Type: cty.String, Optional: true},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"nested_block_set": {
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"an_attr": {Type: cty.String, Required: true},
							},
						},
						Nesting: configschema.NestingSet,
					},
				},
			},
			RequiredReplace: cty.NewPathSet(
				cty.GetAttrPath("ami"),
				cty.GetAttrPath("nested_block_set"),
			),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami = (sensitive value) # forces replacement
        id  = "i-02ae66f368e8518a9"

      - nested_block_set { # forces replacement
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
      + nested_block_set { # forces replacement
          # At least one attribute in this block is (or was) sensitive,
          # so its contents will not be displayed.
        }
    }`,
		},
		"update with sensitive attribute forcing replacement": {
			Action: plans.DeleteThenCreate,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-BEFORE"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("i-02ae66f368e8518a9"),
				"ami": cty.StringVal("ami-AFTER"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Optional: true, Computed: true},
					"ami": {Type: cty.String, Optional: true, Computed: true, Sensitive: true},
				},
			},
			RequiredReplace: cty.NewPathSet(
				cty.GetAttrPath("ami"),
			),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ ami = (sensitive value) # forces replacement
        id  = "i-02ae66f368e8518a9"
    }`,
		},
		"update with sensitive nested type attribute forcing replacement": {
			Action: plans.DeleteThenCreate,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"conn_info": cty.ObjectVal(map[string]cty.Value{
					"user":     cty.StringVal("not-secret"),
					"password": cty.StringVal("top-secret"),
				}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id": cty.StringVal("i-02ae66f368e8518a9"),
				"conn_info": cty.ObjectVal(map[string]cty.Value{
					"user":     cty.StringVal("not-secret"),
					"password": cty.StringVal("new-secret"),
				}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id": {Type: cty.String, Optional: true, Computed: true},
					"conn_info": {
						NestedType: &configschema.Object{
							Nesting: configschema.NestingSingle,
							Attributes: map[string]*configschema.Attribute{
								"user":     {Type: cty.String, Optional: true},
								"password": {Type: cty.String, Optional: true, Sensitive: true},
							},
						},
					},
				},
			},
			RequiredReplace: cty.NewPathSet(
				cty.GetAttrPath("conn_info"),
				cty.GetAttrPath("password"),
			),
			ExpectedOutput: `  # test_instance.example must be replaced
-/+ resource "test_instance" "example" {
      ~ conn_info = { # forces replacement
          ~ password = (sensitive value)
            # (1 unchanged attribute hidden)
        }
        id        = "i-02ae66f368e8518a9"
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_writeOnlyAttributes(t *testing.T) {
	testCases := map[string]testCase{
		"create": {
			Action: plans.Create,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.NullVal(cty.Object(map[string]cty.Type{
				"id":         cty.String,
				"write_only": cty.String,
				"conn_info": cty.Object(map[string]cty.Type{
					"user":                    cty.String,
					"write_only_set_password": cty.String,
				}),
				"block": cty.List(cty.Object(map[string]cty.Type{
					"user":               cty.String,
					"block_set_password": cty.String,
				})),
			})),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"write_only": cty.NullVal(cty.String),
				"conn_info": cty.ObjectVal(map[string]cty.Value{
					"user":                    cty.StringVal("not-secret"),
					"write_only_set_password": cty.NullVal(cty.String),
				}),
				"block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"user":               cty.StringVal("this-is-not-secret"),
					"block_set_password": cty.NullVal(cty.String),
				})}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"write_only": {Type: cty.String, WriteOnly: true},
					"conn_info": {
						NestedType: &configschema.Object{
							Nesting: configschema.NestingSingle,
							Attributes: map[string]*configschema.Attribute{
								"user":                    {Type: cty.String, Optional: true},
								"write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true},
							},
						},
					},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"block": {
						Nesting: configschema.NestingList,
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"user":               {Type: cty.String, Optional: true},
								"block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true},
							},
						},
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + conn_info  = {
          + user                    = "not-secret"
          + write_only_set_password = (sensitive, write-only attribute)
        }
      + id         = "i-02ae66f368e8518a9"
      + write_only = (write-only attribute)

      + block {
          + block_set_password = (write-only attribute)
          + user               = "this-is-not-secret"
        }
    }`,
		},
		"update attribute": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"write_only": cty.NullVal(cty.String),
				"conn_info": cty.ObjectVal(map[string]cty.Value{
					"user":                    cty.StringVal("not-secret"),
					"write_only_set_password": cty.NullVal(cty.String),
				}),
				"block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"user":               cty.StringVal("not-so-secret"),
					"block_set_password": cty.NullVal(cty.String),
				})}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a10"),
				"write_only": cty.NullVal(cty.String),
				"conn_info": cty.ObjectVal(map[string]cty.Value{
					"user":                    cty.StringVal("not-secret"),
					"write_only_set_password": cty.NullVal(cty.String),
				}),
				"block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"user":               cty.StringVal("not-so-secret"),
					"block_set_password": cty.NullVal(cty.String),
				})}),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"write_only": {Type: cty.String, WriteOnly: true},
					"conn_info": {
						NestedType: &configschema.Object{
							Nesting: configschema.NestingSingle,
							Attributes: map[string]*configschema.Attribute{
								"user":                    {Type: cty.String, Optional: true},
								"write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true},
							},
						},
					},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"block": {
						Nesting: configschema.NestingList,
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"user":               {Type: cty.String, Optional: true},
								"block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true},
							},
						},
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ id         = "i-02ae66f368e8518a9" -> "i-02ae66f368e8518a10"
        # (2 unchanged attributes hidden)

        # (1 unchanged block hidden)
    }`,
		},
		"update - delete block": {
			Action: plans.Update,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"write_only": cty.NullVal(cty.String),
				"conn_info": cty.ObjectVal(map[string]cty.Value{
					"user":                    cty.StringVal("not-secret"),
					"write_only_set_password": cty.NullVal(cty.String),
				}),
				"block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"user":               cty.StringVal("not-secret"),
					"block_set_password": cty.NullVal(cty.String),
				})}),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"write_only": cty.NullVal(cty.String),
				"conn_info": cty.NullVal(cty.Object(map[string]cty.Type{
					"user":                    cty.String,
					"write_only_set_password": cty.String,
				})),
				"block": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{
					"user":               cty.String,
					"block_set_password": cty.String,
				}))),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"write_only": {Type: cty.String, WriteOnly: true},
					"conn_info": {
						NestedType: &configschema.Object{
							Nesting: configschema.NestingSingle,
							Attributes: map[string]*configschema.Attribute{
								"user":                    {Type: cty.String, Optional: true},
								"write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true},
							},
						},
					},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"block": {
						Nesting: configschema.NestingList,
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"user":               {Type: cty.String, Optional: true},
								"block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true},
							},
						},
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      - conn_info  = {
          - user                    = "not-secret" -> null
          - write_only_set_password = (sensitive, write-only attribute) -> null
        } -> null
        id         = "i-02ae66f368e8518a9"
        # (1 unchanged attribute hidden)

      - block {
          - block_set_password = (write-only attribute) -> null
          - user               = "not-secret" -> null
        }
    }`,
		},
		"delete": {
			Action: plans.Delete,
			Mode:   addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":         cty.StringVal("i-02ae66f368e8518a9"),
				"write_only": cty.NullVal(cty.String),
				"conn_info": cty.ObjectVal(map[string]cty.Value{
					"user":                    cty.StringVal("not-secret"),
					"write_only_set_password": cty.NullVal(cty.String),
				}),
				"block": cty.ListVal([]cty.Value{cty.ObjectVal(map[string]cty.Value{
					"user":               cty.StringVal("not-secret"),
					"block_set_password": cty.NullVal(cty.String),
				})}),
			}),
			After: cty.NullVal(cty.Object(map[string]cty.Type{
				"id":         cty.String,
				"write_only": cty.String,
				"conn_info": cty.Object(map[string]cty.Type{
					"user":                    cty.String,
					"write_only_set_password": cty.String,
				}),
				"block": cty.List(cty.Object(map[string]cty.Type{
					"user":               cty.String,
					"block_set_password": cty.String,
				})),
			})),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":         {Type: cty.String, Optional: true, Computed: true},
					"write_only": {Type: cty.String, WriteOnly: true},
					"conn_info": {
						NestedType: &configschema.Object{
							Nesting: configschema.NestingSingle,
							Attributes: map[string]*configschema.Attribute{
								"user":                    {Type: cty.String, Optional: true},
								"write_only_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true},
							},
						},
					},
				},
				BlockTypes: map[string]*configschema.NestedBlock{
					"block": {
						Nesting: configschema.NestingList,
						Block: configschema.Block{
							Attributes: map[string]*configschema.Attribute{
								"user":               {Type: cty.String, Optional: true},
								"block_set_password": {Type: cty.String, Optional: true, Sensitive: true, WriteOnly: true},
							},
						},
					},
				},
			},
			ExpectedOutput: `  # test_instance.example will be destroyed
  - resource "test_instance" "example" {
      - conn_info  = {
          - user                    = "not-secret" -> null
          - write_only_set_password = (sensitive, write-only attribute) -> null
        } -> null
      - id         = "i-02ae66f368e8518a9" -> null
      - write_only = (write-only attribute) -> null

      - block {
          - block_set_password = (write-only attribute) -> null
          - user               = "not-secret" -> null
        }
    }`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_moved(t *testing.T) {
	prevRunAddr := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_instance",
		Name: "previous",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)

	testCases := map[string]testCase{
		"moved and updated": {
			PrevRunAddr: prevRunAddr,
			Action:      plans.Update,
			Mode:        addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("12345"),
				"foo": cty.StringVal("hello"),
				"bar": cty.StringVal("baz"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("12345"),
				"foo": cty.StringVal("hello"),
				"bar": cty.StringVal("boop"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Computed: true},
					"foo": {Type: cty.String, Optional: true},
					"bar": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  # (moved from test_instance.previous)
  ~ resource "test_instance" "example" {
      ~ bar = "baz" -> "boop"
        id  = "12345"
        # (1 unchanged attribute hidden)
    }`,
		},
		"moved without changes": {
			PrevRunAddr: prevRunAddr,
			Action:      plans.NoOp,
			Mode:        addrs.ManagedResourceMode,
			Before: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("12345"),
				"foo": cty.StringVal("hello"),
				"bar": cty.StringVal("baz"),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":  cty.StringVal("12345"),
				"foo": cty.StringVal("hello"),
				"bar": cty.StringVal("baz"),
			}),
			Schema: &configschema.Block{
				Attributes: map[string]*configschema.Attribute{
					"id":  {Type: cty.String, Computed: true},
					"foo": {Type: cty.String, Optional: true},
					"bar": {Type: cty.String, Optional: true},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.previous has moved to test_instance.example
    resource "test_instance" "example" {
        id  = "12345"
        # (2 unchanged attributes hidden)
    }`,
		},
	}

	runTestCases(t, testCases)
}

type testCase struct {
	Action               plans.Action
	ActionReason         plans.ResourceInstanceChangeActionReason
	ModuleInst           addrs.ModuleInstance
	Mode                 addrs.ResourceMode
	InstanceKey          addrs.InstanceKey
	DeposedKey           states.DeposedKey
	Before               cty.Value
	BeforeSensitivePaths []cty.Path
	After                cty.Value
	AfterSensitivePaths  []cty.Path
	Schema               *configschema.Block
	RequiredReplace      cty.PathSet
	ExpectedOutput       string
	PrevRunAddr          addrs.AbsResourceInstance
}

func runTestCases(t *testing.T, testCases map[string]testCase) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			ty := tc.Schema.ImpliedType()

			beforeVal := tc.Before
			switch { // Some fixups to make the test cases a little easier to write
			case beforeVal.IsNull():
				beforeVal = cty.NullVal(ty) // allow mistyped nulls
			case !beforeVal.IsKnown():
				beforeVal = cty.UnknownVal(ty) // allow mistyped unknowns
			}

			afterVal := tc.After
			switch { // Some fixups to make the test cases a little easier to write
			case afterVal.IsNull():
				afterVal = cty.NullVal(ty) // allow mistyped nulls
			case !afterVal.IsKnown():
				afterVal = cty.UnknownVal(ty) // allow mistyped unknowns
			}

			addr := addrs.Resource{
				Mode: tc.Mode,
				Type: "test_instance",
				Name: "example",
			}.Instance(tc.InstanceKey).Absolute(tc.ModuleInst)

			prevRunAddr := tc.PrevRunAddr
			// If no previous run address is given, reuse the current address
			// to make initialization easier
			if prevRunAddr.Resource.Resource.Type == "" {
				prevRunAddr = addr
			}

			beforeDynamicValue, err := plans.NewDynamicValue(beforeVal, ty)
			if err != nil {
				t.Fatalf("failed to create dynamic before value: %s", err)
			}

			afterDynamicValue, err := plans.NewDynamicValue(afterVal, ty)
			if err != nil {
				t.Fatalf("failed to create dynamic after value: %s", err)
			}

			src := &plans.ResourceInstanceChangeSrc{
				ChangeSrc: plans.ChangeSrc{
					Action:               tc.Action,
					Before:               beforeDynamicValue,
					BeforeSensitivePaths: tc.BeforeSensitivePaths,
					After:                afterDynamicValue,
					AfterSensitivePaths:  tc.AfterSensitivePaths,
				},

				Addr:        addr,
				PrevRunAddr: prevRunAddr,
				DeposedKey:  tc.DeposedKey,
				ProviderAddr: addrs.AbsProviderConfig{
					Provider: addrs.NewDefaultProvider("test"),
					Module:   addrs.RootModule,
				},
				ActionReason:    tc.ActionReason,
				RequiredReplace: tc.RequiredReplace,
			}

			tfschemas := &terraform.Schemas{
				Providers: map[addrs.Provider]providers.ProviderSchema{
					src.ProviderAddr.Provider: {
						ResourceTypes: map[string]providers.Schema{
							src.Addr.Resource.Resource.Type: {
								Body: tc.Schema,
							},
						},
						DataSources: map[string]providers.Schema{
							src.Addr.Resource.Resource.Type: {
								Body: tc.Schema,
							},
						},
					},
				},
			}
			jsonchanges, err := jsonplan.MarshalResourceChanges([]*plans.ResourceInstanceChangeSrc{src}, tfschemas)
			if err != nil {
				t.Errorf("failed to marshal resource changes: %s", err)
				return
			}

			jsonschemas := jsonprovider.MarshalForRenderer(tfschemas)
			change := structured.FromJsonChange(jsonchanges[0].Change, attribute_path.AlwaysMatcher())
			renderer := Renderer{Colorize: color}
			diff := diff{
				change: jsonchanges[0],
				diff:   differ.ComputeDiffForBlock(change, jsonschemas[jsonchanges[0].ProviderName].ResourceSchemas[jsonchanges[0].Type].Block),
			}
			output, _ := renderHumanDiff(renderer, diff, proposedChange)
			if diff := cmp.Diff(output, tc.ExpectedOutput); diff != "" {
				t.Errorf("wrong output\nexpected:\n%s\nactual:\n%s\ndiff:\n%s\n", tc.ExpectedOutput, output, diff)
			}
		})
	}
}

func TestOutputChanges(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}

	testCases := map[string]struct {
		changes []*plans.OutputChangeSrc
		output  string
	}{
		"new output value": {
			[]*plans.OutputChangeSrc{
				outputChange(
					"foo",
					cty.NullVal(cty.DynamicPseudoType),
					cty.StringVal("bar"),
					false,
				),
			},
			`  + foo = "bar"`,
		},
		"removed output": {
			[]*plans.OutputChangeSrc{
				outputChange(
					"foo",
					cty.StringVal("bar"),
					cty.NullVal(cty.DynamicPseudoType),
					false,
				),
			},
			`  - foo = "bar" -> null`,
		},
		"single string change": {
			[]*plans.OutputChangeSrc{
				outputChange(
					"foo",
					cty.StringVal("bar"),
					cty.StringVal("baz"),
					false,
				),
			},
			`  ~ foo = "bar" -> "baz"`,
		},
		"element added to list": {
			[]*plans.OutputChangeSrc{
				outputChange(
					"foo",
					cty.ListVal([]cty.Value{
						cty.StringVal("alpha"),
						cty.StringVal("beta"),
						cty.StringVal("delta"),
						cty.StringVal("epsilon"),
					}),
					cty.ListVal([]cty.Value{
						cty.StringVal("alpha"),
						cty.StringVal("beta"),
						cty.StringVal("gamma"),
						cty.StringVal("delta"),
						cty.StringVal("epsilon"),
					}),
					false,
				),
			},
			`  ~ foo = [
        # (1 unchanged element hidden)
        "beta",
      + "gamma",
        "delta",
        # (1 unchanged element hidden)
    ]`,
		},
		"multiple outputs changed, one sensitive": {
			[]*plans.OutputChangeSrc{
				outputChange(
					"a",
					cty.NumberIntVal(1),
					cty.NumberIntVal(2),
					false,
				),
				outputChange(
					"b",
					cty.StringVal("hunter2"),
					cty.StringVal("correct-horse-battery-staple"),
					true,
				),
				outputChange(
					"c",
					cty.BoolVal(false),
					cty.BoolVal(true),
					false,
				),
			},
			`  ~ a = 1 -> 2
  ~ b = (sensitive value)
  ~ c = false -> true`,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			changes := &plans.ChangesSrc{
				Outputs: tc.changes,
			}

			outputs, err := jsonplan.MarshalOutputChanges(changes)
			if err != nil {
				t.Fatalf("failed to marshal output changes")
			}

			renderer := Renderer{Colorize: color}
			diffs := precomputeDiffs(Plan{
				OutputChanges: outputs,
			}, plans.NormalMode)

			output := renderHumanDiffOutputs(renderer, diffs.outputs)
			if output != tc.output {
				t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output)
			}
		})
	}
}

func TestResourceChange_deferredActions(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
	providerAddr := addrs.AbsProviderConfig{
		Provider: addrs.NewDefaultProvider("test"),
		Module:   addrs.RootModule,
	}
	testCases := map[string]struct {
		changes []*plans.DeferredResourceInstanceChange
		output  string
	}{
		"deferred create action": {
			changes: []*plans.DeferredResourceInstanceChange{
				{
					DeferredReason: providers.DeferredReasonAbsentPrereq,
					Change: &plans.ResourceInstanceChange{
						Addr: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_instance",
							Name: "instance",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
						ProviderAddr: providerAddr,
						Change: plans.Change{
							Action: plans.Create,
							Before: cty.NullVal(cty.DynamicPseudoType),
							After: cty.ObjectVal(map[string]cty.Value{
								"id":  cty.UnknownVal(cty.String),
								"ami": cty.StringVal("bar"),
								"disk": cty.UnknownVal(cty.Object(map[string]cty.Type{
									"mount_point": cty.String,
									"size":        cty.Number,
								})),
								"root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{
									"volume_type": cty.String,
								})),
							}),
						},
					},
				},
			},
			output: `  # test_instance.instance was deferred
  # (because a prerequisite for this resource has not yet been created)
  + resource "test_instance" "instance" {
      + ami  = "bar"
      + disk = (known after apply)
      + id   = (known after apply)

      + root_block_device (known after apply)
    }`,
		},

		"deferred create action unknown for_each": {
			changes: []*plans.DeferredResourceInstanceChange{
				{
					DeferredReason: providers.DeferredReasonInstanceCountUnknown,
					Change: &plans.ResourceInstanceChange{
						Addr: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_instance",
							Name: "instance",
						}.Instance(addrs.WildcardKey).Absolute(addrs.RootModuleInstance),
						ProviderAddr: providerAddr,
						Change: plans.Change{
							Action: plans.Create,
							Before: cty.NullVal(cty.DynamicPseudoType),
							After: cty.ObjectVal(map[string]cty.Value{
								"id":  cty.UnknownVal(cty.String),
								"ami": cty.StringVal("bar"),
								"disk": cty.UnknownVal(cty.Object(map[string]cty.Type{
									"mount_point": cty.String,
									"size":        cty.Number,
								})),
								"root_block_device": cty.UnknownVal(cty.Object(map[string]cty.Type{
									"volume_type": cty.String,
								})),
							}),
						},
					},
				},
			},
			output: `  # test_instance.instance[*] was deferred
  # (because the number of resource instances is unknown)
  + resource "test_instance" "instance" {
      + ami  = "bar"
      + disk = (known after apply)
      + id   = (known after apply)

      + root_block_device (known after apply)
    }`,
		},

		"deferred update action": {
			changes: []*plans.DeferredResourceInstanceChange{
				{
					DeferredReason: providers.DeferredReasonProviderConfigUnknown,
					Change: &plans.ResourceInstanceChange{
						Addr: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_instance",
							Name: "instance",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
						ProviderAddr: providerAddr,
						Change: plans.Change{
							Action: plans.Update,
							Before: cty.ObjectVal(map[string]cty.Value{
								"id":  cty.StringVal("foo"),
								"ami": cty.StringVal("bar"),
								"disk": cty.NullVal(cty.Object(map[string]cty.Type{
									"mount_point": cty.String,
									"size":        cty.Number,
								})),
								"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
									"volume_type": cty.String,
								})),
							}),
							After: cty.ObjectVal(map[string]cty.Value{
								"id":  cty.StringVal("foo"),
								"ami": cty.StringVal("baz"),
								"disk": cty.NullVal(cty.Object(map[string]cty.Type{
									"mount_point": cty.String,
									"size":        cty.Number,
								})),
								"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
									"volume_type": cty.String,
								})),
							}),
						},
					},
				},
			},
			output: `  # test_instance.instance was deferred
  # (because the provider configuration is unknown)
  ~ resource "test_instance" "instance" {
      ~ ami = "bar" -> "baz"
        id  = "foo"
    }`,
		},

		"deferred destroy action": {
			changes: []*plans.DeferredResourceInstanceChange{
				{
					DeferredReason: providers.DeferredReasonResourceConfigUnknown,
					Change: &plans.ResourceInstanceChange{
						Addr: addrs.Resource{
							Mode: addrs.ManagedResourceMode,
							Type: "test_instance",
							Name: "instance",
						}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
						ProviderAddr: providerAddr,
						Change: plans.Change{
							Action: plans.Update,
							Before: cty.ObjectVal(map[string]cty.Value{
								"id":  cty.StringVal("foo"),
								"ami": cty.StringVal("bar"),
								"disk": cty.NullVal(cty.Object(map[string]cty.Type{
									"mount_point": cty.String,
									"size":        cty.Number,
								})),
								"root_block_device": cty.NullVal(cty.Object(map[string]cty.Type{
									"volume_type": cty.String,
								})),
							}),
							After: cty.NullVal(cty.Object(map[string]cty.Type{
								"id":  cty.String,
								"ami": cty.String,
								"disk": cty.Object(map[string]cty.Type{
									"mount_point": cty.String,
									"size":        cty.Number,
								}),
								"root_block_device": cty.Object(map[string]cty.Type{
									"volume_type": cty.String,
								}),
							})),
						},
					},
				},
			},
			output: `  # test_instance.instance was deferred
  # (because the resource configuration is unknown)
  ~ resource "test_instance" "instance" {
      - ami = "bar" -> null
      - id  = "foo" -> null
    }`,
		},
	}

	for name, tc := range testCases {
		t.Run(name, func(t *testing.T) {
			blockSchema := testSchema(configschema.NestingSingle)
			fullSchema := &terraform.Schemas{
				Providers: map[addrs.Provider]providers.ProviderSchema{
					providerAddr.Provider: {
						ResourceTypes: map[string]providers.Schema{
							"test_instance": {
								Body: blockSchema,
							},
						},
					},
				},
			}
			var changes []*plans.DeferredResourceInstanceChangeSrc
			for _, change := range tc.changes {
				changeSrc, err := change.Encode(providers.Schema{
					Body: blockSchema,
				})
				if err != nil {
					t.Fatalf("Failed to encode change: %s", err)
				}
				changes = append(changes, changeSrc)
			}

			deferredChanges, err := jsonplan.MarshalDeferredResourceChanges(changes, fullSchema)
			if err != nil {
				t.Fatalf("failed to marshal deferred changes: %s", err)
			}

			renderer := Renderer{Colorize: color}
			jsonschemas := jsonprovider.MarshalForRenderer(fullSchema)
			diffs := precomputeDiffs(Plan{
				DeferredChanges: deferredChanges,
				ProviderSchemas: jsonschemas,
			}, plans.NormalMode)

			// TODO: Add diffing for outputs
			// TODO: Make sure it's true and either make it a single entity in the test case or deal with a list here
			output, _ := renderHumanDeferredDiff(renderer, diffs.deferred[0])
			if diff := cmp.Diff(tc.output, output); len(diff) > 0 {
				t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", output, tc.output, diff)
			}
		})
	}
}

func TestResourceChange_actions(t *testing.T) {
	color := &colorstring.Colorize{Colors: colorstring.DefaultColors, Disable: true}
	triggeringResourceAddr := addrs.Resource{
		Mode: addrs.ManagedResourceMode,
		Type: "test_instance",
		Name: "example",
	}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance)
	defaultResourceChange := jsonplan.ResourceChange{
		Address:      triggeringResourceAddr.String(),
		Mode:         "managed",
		Type:         "test_instance",
		Name:         "example",
		IndexUnknown: true,
		ProviderName: "registry.terraform.io/hashicorp/test",
		Change: jsonplan.Change{
			Actions: []string{"create"},
			After: marshalJson(t, map[string]interface{}{
				"id":    "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E",
				"value": "Hello, World!",
			}),
		},
	}

	for name, tc := range map[string]struct {
		actionInvocations []jsonplan.ActionInvocation
		output            string
	}{
		"before actions": {
			actionInvocations: []jsonplan.ActionInvocation{
				{
					Address:      "action.test_action.hello",
					Type:         "test_action",
					Name:         "hello",
					ProviderName: "registry.terraform.io/hashicorp/test",
					LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{
						ActionTriggerBlockIndex:   0,
						ActionsListIndex:          0,
						TriggeringResourceAddress: triggeringResourceAddr.String(),
						ActionTriggerEvent:        "BeforeCreate",
					},
					ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{
						"id": cty.StringVal("1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"),
						"disk": cty.ObjectVal(map[string]cty.Value{
							"size": cty.StringVal("100"),
						}),
						"root_block_device": cty.ObjectVal(map[string]cty.Value{
							"volume_type": cty.StringVal("gp2"),
						}),
					})),
				},
			},
			output: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
    }

    # Actions to be invoked before this change in order:
    action "test_action" "hello" {
        config {
            disk = {
                size = "100"
            }
            id   = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
        
            root_block_device {
                volume_type = "gp2"
            }
        }
    }
`,
		},
		"after actions": {
			actionInvocations: []jsonplan.ActionInvocation{
				{
					Address:      "action.test_action.hello",
					Type:         "test_action",
					Name:         "hello",
					ProviderName: "registry.terraform.io/hashicorp/test",
					LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{
						ActionTriggerBlockIndex:   0,
						ActionsListIndex:          0,
						TriggeringResourceAddress: triggeringResourceAddr.String(),
						ActionTriggerEvent:        "AfterCreate",
					},
					ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{
						"id": cty.StringVal("1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"),
						"disk": cty.ObjectVal(map[string]cty.Value{
							"size": cty.StringVal("100"),
						}),
						"root_block_device": cty.ObjectVal(map[string]cty.Value{
							"volume_type": cty.StringVal("gp2"),
						}),
					})),
				},
			},
			output: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
    }

    # Actions to be invoked after this change in order:
    action "test_action" "hello" {
        config {
            disk = {
                size = "100"
            }
            id   = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
        
            root_block_device {
                volume_type = "gp2"
            }
        }
    }
`,
		},
		"before and after actions": {
			actionInvocations: []jsonplan.ActionInvocation{
				{
					Address:      "action.test_action.hello",
					Type:         "test_action",
					Name:         "hello",
					ProviderName: "registry.terraform.io/hashicorp/test",
					LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{
						ActionTriggerBlockIndex:   0,
						ActionsListIndex:          0,
						TriggeringResourceAddress: triggeringResourceAddr.String(),
						ActionTriggerEvent:        "BeforeCreate",
					},
					ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{
						"id": cty.StringVal("first-block-and-action"),
					})),
				},
				{
					Address:      "action.test_action.hello",
					Type:         "test_action",
					Name:         "hello",
					ProviderName: "registry.terraform.io/hashicorp/test",
					LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{
						ActionTriggerBlockIndex:   0,
						ActionsListIndex:          1,
						TriggeringResourceAddress: triggeringResourceAddr.String(),
						ActionTriggerEvent:        "BeforeCreate",
					},
					ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{
						"id": cty.StringVal("first-block-second-action"),
					})),
				},
				{
					Address:      "action.test_action.hello",
					Type:         "test_action",
					Name:         "hello",
					ProviderName: "registry.terraform.io/hashicorp/test",
					LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{
						ActionTriggerBlockIndex:   1,
						ActionsListIndex:          0,
						TriggeringResourceAddress: triggeringResourceAddr.String(),
						ActionTriggerEvent:        "AfterCreate",
					},
					ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{
						"id": cty.StringVal("second-block-first-action"),
					})),
				},
				{
					Address:      "action.test_action.hello",
					Type:         "test_action",
					Name:         "hello",
					ProviderName: "registry.terraform.io/hashicorp/test",
					LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{
						ActionTriggerBlockIndex:   2,
						ActionsListIndex:          0,
						TriggeringResourceAddress: triggeringResourceAddr.String(),
						ActionTriggerEvent:        "AfterCreate",
					},
					ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{
						"id": cty.StringVal("third-block-first-action"),
					})),
				},
				{
					Address:      "action.test_action.hello",
					Type:         "test_action",
					Name:         "hello",
					ProviderName: "registry.terraform.io/hashicorp/test",
					LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{
						ActionTriggerBlockIndex:   3,
						ActionsListIndex:          0,
						TriggeringResourceAddress: triggeringResourceAddr.String(),
						ActionTriggerEvent:        "BeforeCreate",
					},
					ConfigValues: marshalCtyJson(t, cty.ObjectVal(map[string]cty.Value{
						"id": cty.StringVal("fourth-block-first-action"),
					})),
				},
			},
			output: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
    }

    # Actions to be invoked before this change in order:
    action "test_action" "hello" {
        config {
            id = "first-block-and-action"
        }
    }
    action "test_action" "hello" {
        config {
            id = "first-block-second-action"
        }
    }
    action "test_action" "hello" {
        config {
            id = "fourth-block-first-action"
        }
    }


    # Actions to be invoked after this change in order:
    action "test_action" "hello" {
        config {
            id = "second-block-first-action"
        }
    }
    action "test_action" "hello" {
        config {
            id = "third-block-first-action"
        }
    }
`,
		},
		"no config value": {
			actionInvocations: []jsonplan.ActionInvocation{
				{
					Address:      "action.test_action.hello",
					Type:         "test_action",
					Name:         "hello",
					ProviderName: "registry.terraform.io/hashicorp/test",
					LifecycleActionTrigger: &jsonplan.LifecycleActionTrigger{
						ActionTriggerBlockIndex:   0,
						ActionsListIndex:          0,
						TriggeringResourceAddress: triggeringResourceAddr.String(),
						ActionTriggerEvent:        "BeforeCreate",
					},
				},
			},
			output: `  # test_instance.example will be created
  + resource "test_instance" "example" {
      + id = "1D5F5E9E-F2E5-401B-9ED5-692A215AC67E"
    }

    # Actions to be invoked before this change in order:
    action "test_action" "hello" {
    }
`,
		},
	} {
		t.Run(name, func(t *testing.T) {
			renderer := Renderer{Colorize: color}
			providerAddr := addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
			}
			blockSchema := testSchema(configschema.NestingSingle)
			fullSchema := &terraform.Schemas{
				Providers: map[addrs.Provider]providers.ProviderSchema{
					providerAddr.Provider: {
						ResourceTypes: map[string]providers.Schema{
							"test_instance": {
								Body: blockSchema,
							},
						},
						Actions: map[string]providers.ActionSchema{
							"test_action": {
								ConfigSchema: blockSchema,
							},
						},
					},
				},
			}
			jsonschemas := jsonprovider.MarshalForRenderer(fullSchema)
			diffs := precomputeDiffs(Plan{
				ResourceChanges:   []jsonplan.ResourceChange{defaultResourceChange},
				ActionInvocations: tc.actionInvocations,
				ProviderSchemas:   jsonschemas,
			}, plans.NormalMode)

			if len(diffs.changes) != 1 {
				t.Fatalf("expected exactly one change, got %d", len(diffs.changes))
			}

			output, _ := renderHumanDiff(renderer, diffs.changes[0], proposedChange)
			if diff := cmp.Diff(tc.output, output); len(diff) > 0 {
				t.Errorf("unexpected output\ngot:\n%s\nwant:\n%s\ndiff:\n%s", output, tc.output, diff)
			}
		})
	}
}

func outputChange(name string, before, after cty.Value, sensitive bool) *plans.OutputChangeSrc {
	addr := addrs.AbsOutputValue{
		OutputValue: addrs.OutputValue{Name: name},
	}

	change := &plans.OutputChange{
		Addr: addr, Change: plans.Change{
			Before: before,
			After:  after,
		},
		Sensitive: sensitive,
	}

	changeSrc, err := change.Encode()
	if err != nil {
		panic(fmt.Sprintf("failed to encode change for %s: %s", addr, err))
	}

	return changeSrc
}

// A basic test schema using a configurable NestingMode for one (NestedType) attribute and one block
func testSchema(nesting configschema.NestingMode) *configschema.Block {
	var diskKey = "disks"
	if nesting == configschema.NestingSingle {
		diskKey = "disk"
	}

	return &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"id":  {Type: cty.String, Optional: true, Computed: true},
			"ami": {Type: cty.String, Optional: true},
			diskKey: {
				NestedType: &configschema.Object{
					Attributes: map[string]*configschema.Attribute{
						"mount_point": {Type: cty.String, Optional: true},
						"size":        {Type: cty.String, Optional: true},
					},
					Nesting: nesting,
				},
			},
		},
		BlockTypes: map[string]*configschema.NestedBlock{
			"root_block_device": {
				Block: configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"volume_type": {
							Type:     cty.String,
							Optional: true,
							Computed: true,
						},
					},
				},
				Nesting: nesting,
			},
		},
	}
}

// A basic test schema using a configurable NestingMode for one (NestedType)
// attribute marked sensitive.
func testSchemaSensitive(nesting configschema.NestingMode) *configschema.Block {
	return &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"id":  {Type: cty.String, Optional: true, Computed: true},
			"ami": {Type: cty.String, Optional: true},
			"disks": {
				Sensitive: true,
				NestedType: &configschema.Object{
					Attributes: map[string]*configschema.Attribute{
						"mount_point": {Type: cty.String, Optional: true},
						"size":        {Type: cty.String, Optional: true},
					},
					Nesting: nesting,
				},
			},
		},
	}
}

func testSchemaMultipleBlocks(nesting configschema.NestingMode) *configschema.Block {
	return &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"id":  {Type: cty.String, Optional: true, Computed: true},
			"ami": {Type: cty.String, Optional: true},
			"disks": {
				NestedType: &configschema.Object{
					Attributes: map[string]*configschema.Attribute{
						"mount_point": {Type: cty.String, Optional: true},
						"size":        {Type: cty.String, Optional: true},
					},
					Nesting: nesting,
				},
			},
		},
		BlockTypes: map[string]*configschema.NestedBlock{
			"root_block_device": {
				Block: configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"volume_type": {
							Type:     cty.String,
							Optional: true,
							Computed: true,
						},
					},
				},
				Nesting: nesting,
			},
			"leaf_block_device": {
				Block: configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"volume_type": {
							Type:     cty.String,
							Optional: true,
							Computed: true,
						},
					},
				},
				Nesting: nesting,
			},
		},
	}
}

// similar to testSchema with the addition of a "new_field" block
func testSchemaPlus(nesting configschema.NestingMode) *configschema.Block {
	var diskKey = "disks"
	if nesting == configschema.NestingSingle {
		diskKey = "disk"
	}

	return &configschema.Block{
		Attributes: map[string]*configschema.Attribute{
			"id":  {Type: cty.String, Optional: true, Computed: true},
			"ami": {Type: cty.String, Optional: true},
			diskKey: {
				NestedType: &configschema.Object{
					Attributes: map[string]*configschema.Attribute{
						"mount_point": {Type: cty.String, Optional: true},
						"size":        {Type: cty.String, Optional: true},
					},
					Nesting: nesting,
				},
			},
		},
		BlockTypes: map[string]*configschema.NestedBlock{
			"root_block_device": {
				Block: configschema.Block{
					Attributes: map[string]*configschema.Attribute{
						"volume_type": {
							Type:     cty.String,
							Optional: true,
							Computed: true,
						},
						"new_field": {
							Type:     cty.String,
							Optional: true,
							Computed: true,
						},
					},
				},
				Nesting: nesting,
			},
		},
	}
}

func marshalJson(t *testing.T, data interface{}) json.RawMessage {
	result, err := json.Marshal(data)
	if err != nil {
		t.Fatalf("failed to marshal json: %v", err)
	}
	return result
}

func marshalCtyJson(t *testing.T, data cty.Value) json.RawMessage {
	result, err := ctyjson.Marshal(data, data.Type())
	if err != nil {
		t.Fatalf("failed to marshal json: %v", err)
	}
	return result
}
