package format

import (
	"fmt"
	"testing"

	"github.com/google/go-cmp/cmp"
	"github.com/hashicorp/terraform/addrs"
	"github.com/hashicorp/terraform/configs/configschema"
	"github.com/hashicorp/terraform/plans"
	"github.com/mitchellh/colorstring"
	"github.com/zclconf/go-cty/cty"
)

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 "
    }
`,
		},
		"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 (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
    }
`,
		},
		"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"
    }
`,
		},
		"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),
			}),
			After: cty.ObjectVal(map[string]cty.Value{
				"id":        cty.StringVal("i-02ae66f368e8518a9"),
				"ami":       cty.StringVal("ami-AFTER"),
				"unchanged": 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},
				},
			},
			RequiredReplace: cty.NewPathSet(),
			ExpectedOutput: `  # test_instance.example will be updated in-place
  ~ resource "test_instance" "example" {
      ~ ami = "ami-BEFORE" -> "ami-AFTER"
        id  = "i-02ae66f368e8518a9"
    }
`,
		},
		"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"
    }
`,
		},
		"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 -> null
                # (1 unchanged element 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 element 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 element 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_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"),
				}),
			}),
			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 = {
          + "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"),
				}),
			}),
			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 = {
          + "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"),
					"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"
            # (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"
        }
    }
`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedSet(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-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 - 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),
					}),
				}),
				"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"
          },
          - {
            - mount_point = "/var/diska" -> null
          },
        ]
        id    = "i-02ae66f368e8518a9"

      + root_block_device {
          + new_field   = "new_value"
          + volume_type = "gp2"
        }
      - root_block_device {
          - volume_type = "gp2" -> null
        }
    }
`,
		},
		"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 = "different"
        }
      - root_block_device { # forces replacement
          - volume_type = "gp2" -> null
        }
    }
`,
		},
		"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
        }
    }
`,
		},
	}
	runTestCases(t, testCases)
}

func TestResourceChange_nestedMap(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-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
        }
    }
`,
		},
	}
	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"),
					}),
				}),
			}),
			AfterValMarks: []cty.PathValueMarks{
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					// Nested blocks/sets will mark the whole set/block as sensitive
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_list"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			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)
      + id         = "i-02ae66f368e8518a9"
      + list_field = [
          + "hello",
          + (sensitive),
          + "!",
        ]
      + map_key    = {
          + "breakfast" = 800
          + "dinner"    = (sensitive)
        }
      + map_whole  = (sensitive)

      + 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"),
					}),
				}),
			}),
			BeforeValMarks: []cty.PathValueMarks{
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "special"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "some_number"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			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)
        id          = "i-02ae66f368e8518a9"
      ~ list_field  = [
            # (1 unchanged element hidden)
            "friends",
          - (sensitive),
          + ".",
        ]
      ~ map_key     = {
          # Warning: this attribute value will no longer be marked as sensitive
          # after applying this change
          ~ "dinner"    = (sensitive)
            # (1 unchanged element hidden)
        }
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change
      ~ map_whole   = (sensitive)
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change
      ~ some_number = (sensitive)
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change
      ~ special     = (sensitive)

      # 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.
        }
    }
`,
		},
		"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"),
				}),
			}),
			AfterValMarks: []cty.PathValueMarks{
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "tags"}, cty.IndexStep{Key: cty.StringVal("address")}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_single"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			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 = [
          - "hello",
          + (sensitive),
            "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)
        }
      # Warning: this attribute value will be marked as sensitive and will
      # not display in UI output after applying this change
      ~ map_whole  = (sensitive)

      # 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),
					}),
				}),
			}),
			BeforeValMarks: []cty.PathValueMarks{
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			AfterValMarks: []cty.PathValueMarks{
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(0)}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_map"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			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)
        id         = "i-02ae66f368e8518a9"
      ~ list_field = [
          - (sensitive),
          + (sensitive),
            "friends",
        ]
      ~ map_key    = {
          ~ "dinner"    = (sensitive)
            # (1 unchanged element hidden)
        }
      ~ map_whole  = (sensitive)

      ~ nested_block_map {
          # 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"),
					}),
				}),
			}),
			BeforeValMarks: []cty.PathValueMarks{
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "special"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "some_number"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(2)}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			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)
        id          = "i-02ae66f368e8518a9"
      ~ list_field  = [
            # (1 unchanged element hidden)
            "friends",
          - (sensitive),
          + "!",
        ]
      ~ map_key     = {
          # Warning: this attribute value will no longer be marked as sensitive
          # after applying this change
          ~ "dinner"    = (sensitive)
            # (1 unchanged element hidden)
        }
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change
      ~ map_whole   = (sensitive)
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change
      ~ some_number = (sensitive)
      # Warning: this attribute value will no longer be marked as sensitive
      # after applying this change
      ~ special     = (sensitive)

      # 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),
			BeforeValMarks: []cty.PathValueMarks{
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "ami"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "list_field"}, cty.IndexStep{Key: cty.NumberIntVal(1)}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_key"}, cty.IndexStep{Key: cty.StringVal("dinner")}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "map_whole"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.Path{cty.GetAttrStep{Name: "nested_block_set"}},
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			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) -> null
      - id         = "i-02ae66f368e8518a9" -> null
      - list_field = [
          - "hello",
          - (sensitive),
        ] -> null
      - map_key    = {
          - "breakfast" = 800
          - "dinner"    = (sensitive)
        } -> null
      - map_whole  = (sensitive) -> 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"),
					}),
				}),
			}),
			BeforeValMarks: []cty.PathValueMarks{
				{
					Path:  cty.GetAttrPath("ami"),
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.GetAttrPath("nested_block_set"),
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			AfterValMarks: []cty.PathValueMarks{
				{
					Path:  cty.GetAttrPath("ami"),
					Marks: cty.NewValueMarks("sensitive"),
				},
				{
					Path:  cty.GetAttrPath("nested_block_set"),
					Marks: cty.NewValueMarks("sensitive"),
				},
			},
			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) # 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.
        }
    }
`,
		},
		"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)
}

type testCase struct {
	Action          plans.Action
	ActionReason    plans.ResourceInstanceChangeActionReason
	Mode            addrs.ResourceMode
	Before          cty.Value
	BeforeValMarks  []cty.PathValueMarks
	AfterValMarks   []cty.PathValueMarks
	After           cty.Value
	Schema          *configschema.Block
	RequiredReplace cty.PathSet
	ExpectedOutput  string
}

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
			}
			before, err := plans.NewDynamicValue(beforeVal, ty)
			if err != nil {
				t.Fatal(err)
			}

			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
			}
			after, err := plans.NewDynamicValue(afterVal, ty)
			if err != nil {
				t.Fatal(err)
			}

			change := &plans.ResourceInstanceChangeSrc{
				Addr: addrs.Resource{
					Mode: tc.Mode,
					Type: "test_instance",
					Name: "example",
				}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
				ProviderAddr: addrs.AbsProviderConfig{
					Provider: addrs.NewDefaultProvider("test"),
					Module:   addrs.RootModule,
				},
				ChangeSrc: plans.ChangeSrc{
					Action:         tc.Action,
					Before:         before,
					After:          after,
					BeforeValMarks: tc.BeforeValMarks,
					AfterValMarks:  tc.AfterValMarks,
				},
				ActionReason:    tc.ActionReason,
				RequiredReplace: tc.RequiredReplace,
			}

			output := ResourceChange(change, tc.Schema, color)
			if output != tc.ExpectedOutput {
				t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.ExpectedOutput)
				t.Errorf("%s", cmp.Diff(output, tc.ExpectedOutput))
			}
		})
	}
}

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) {
			output := OutputChanges(tc.changes, color)
			if output != tc.output {
				t.Errorf("Unexpected diff.\ngot:\n%s\nwant:\n%s\n", output, tc.output)
			}
		})
	}
}

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 {
	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,
			},
		},
	}
}

// similar to testSchema with the addition of a "new_field" block
func testSchemaPlus(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,
						},
						"new_field": {
							Type:     cty.String,
							Optional: true,
							Computed: true,
						},
					},
				},
				Nesting: nesting,
			},
		},
	}
}
