// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package tofu

import (
	"bytes"
	"encoding/json"
	"fmt"
	"os"
	"reflect"
	"sort"
	"strings"
	"testing"

	"github.com/davecgh/go-spew/spew"
	"github.com/opentofu/opentofu/internal/addrs"
	"github.com/opentofu/opentofu/internal/legacy/hcl2shim"
)

func TestStateValidate(t *testing.T) {
	cases := map[string]struct {
		In  *State
		Err bool
	}{
		"empty state": {
			&State{},
			false,
		},

		"multiple modules": {
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: []string{"root", "foo"},
					},
					&ModuleState{
						Path: []string{"root", "foo"},
					},
				},
			},
			true,
		},
	}

	for name, tc := range cases {
		// Init the state
		tc.In.init()

		err := tc.In.Validate()
		if (err != nil) != tc.Err {
			t.Fatalf("%s: err: %s", name, err)
		}
	}
}

func TestStateAddModule(t *testing.T) {
	cases := []struct {
		In  []addrs.ModuleInstance
		Out [][]string
	}{
		{
			[]addrs.ModuleInstance{
				addrs.RootModuleInstance,
				addrs.RootModuleInstance.Child("child", addrs.NoKey),
			},
			[][]string{
				[]string{"root"},
				[]string{"root", "child"},
			},
		},

		{
			[]addrs.ModuleInstance{
				addrs.RootModuleInstance.Child("foo", addrs.NoKey).Child("bar", addrs.NoKey),
				addrs.RootModuleInstance.Child("foo", addrs.NoKey),
				addrs.RootModuleInstance,
				addrs.RootModuleInstance.Child("bar", addrs.NoKey),
			},
			[][]string{
				[]string{"root"},
				[]string{"root", "bar"},
				[]string{"root", "foo"},
				[]string{"root", "foo", "bar"},
			},
		},
		// Same last element, different middle element
		{
			[]addrs.ModuleInstance{
				addrs.RootModuleInstance.Child("foo", addrs.NoKey).Child("bar", addrs.NoKey), // This one should sort after...
				addrs.RootModuleInstance.Child("foo", addrs.NoKey),
				addrs.RootModuleInstance,
				addrs.RootModuleInstance.Child("bar", addrs.NoKey).Child("bar", addrs.NoKey), // ...this one.
				addrs.RootModuleInstance.Child("bar", addrs.NoKey),
			},
			[][]string{
				[]string{"root"},
				[]string{"root", "bar"},
				[]string{"root", "foo"},
				[]string{"root", "bar", "bar"},
				[]string{"root", "foo", "bar"},
			},
		},
	}

	for _, tc := range cases {
		s := new(State)
		for _, p := range tc.In {
			s.AddModule(p)
		}

		actual := make([][]string, 0, len(tc.In))
		for _, m := range s.Modules {
			actual = append(actual, m.Path)
		}

		if !reflect.DeepEqual(actual, tc.Out) {
			t.Fatalf("wrong result\ninput: %sgot:   %#v\nwant:  %#v", spew.Sdump(tc.In), actual, tc.Out)
		}
	}
}

func TestStateOutputTypeRoundTrip(t *testing.T) {
	state := &State{
		Modules: []*ModuleState{
			&ModuleState{
				Path: []string{"root"},
				Outputs: map[string]*OutputState{
					"string_output": &OutputState{
						Value: "String Value",
						Type:  "string",
					},
				},
			},
		},
	}
	state.init()

	buf := new(bytes.Buffer)
	if err := WriteState(state, buf); err != nil {
		t.Fatalf("err: %s", err)
	}

	roundTripped, err := ReadState(buf)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	if !reflect.DeepEqual(state, roundTripped) {
		t.Logf("expected:\n%#v", state)
		t.Fatalf("got:\n%#v", roundTripped)
	}
}

func TestStateDeepCopy(t *testing.T) {
	cases := []struct {
		State *State
	}{
		// Nil
		{nil},

		// Version
		{
			&State{Version: 5},
		},
		// TFVersion
		{
			&State{TFVersion: "5"},
		},
		// Modules
		{
			&State{
				Version: 6,
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Primary: &InstanceState{
									Meta: map[string]interface{}{},
								},
							},
						},
					},
				},
			},
		},
		// Deposed
		// The nil values shouldn't be there if the State was properly init'ed,
		// but the Copy should still work anyway.
		{
			&State{
				Version: 6,
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Primary: &InstanceState{
									Meta: map[string]interface{}{},
								},
								Deposed: []*InstanceState{
									{ID: "test"},
									nil,
								},
							},
						},
					},
				},
			},
		},
	}

	for i, tc := range cases {
		t.Run(fmt.Sprintf("copy-%d", i), func(t *testing.T) {
			actual := tc.State.DeepCopy()
			expected := tc.State
			if !reflect.DeepEqual(actual, expected) {
				t.Fatalf("Expected: %#v\nRecevied: %#v\n", expected, actual)
			}
		})
	}
}

func TestStateEqual(t *testing.T) {
	cases := []struct {
		Name     string
		Result   bool
		One, Two *State
	}{
		// Nils
		{
			"one nil",
			false,
			nil,
			&State{Version: 2},
		},

		{
			"both nil",
			true,
			nil,
			nil,
		},

		// Different versions
		{
			"different state versions",
			false,
			&State{Version: 5},
			&State{Version: 2},
		},

		// Different modules
		{
			"different module states",
			false,
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: []string{"root"},
					},
				},
			},
			&State{},
		},

		{
			"same module states",
			true,
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: []string{"root"},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: []string{"root"},
					},
				},
			},
		},

		// Meta differs
		{
			"differing meta values with primitives",
			false,
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Primary: &InstanceState{
									Meta: map[string]interface{}{
										"schema_version": "1",
									},
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Primary: &InstanceState{
									Meta: map[string]interface{}{
										"schema_version": "2",
									},
								},
							},
						},
					},
				},
			},
		},

		// Meta with complex types
		{
			"same meta with complex types",
			true,
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Primary: &InstanceState{
									Meta: map[string]interface{}{
										"timeouts": map[string]interface{}{
											"create": 42,
											"read":   "27",
										},
									},
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Primary: &InstanceState{
									Meta: map[string]interface{}{
										"timeouts": map[string]interface{}{
											"create": 42,
											"read":   "27",
										},
									},
								},
							},
						},
					},
				},
			},
		},

		// Meta with complex types that have been altered during serialization
		{
			"same meta with complex types that have been json-ified",
			true,
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Primary: &InstanceState{
									Meta: map[string]interface{}{
										"timeouts": map[string]interface{}{
											"create": int(42),
											"read":   "27",
										},
									},
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Primary: &InstanceState{
									Meta: map[string]interface{}{
										"timeouts": map[string]interface{}{
											"create": float64(42),
											"read":   "27",
										},
									},
								},
							},
						},
					},
				},
			},
		},
	}

	for i, tc := range cases {
		t.Run(fmt.Sprintf("%d-%s", i, tc.Name), func(t *testing.T) {
			if tc.One.Equal(tc.Two) != tc.Result {
				t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
			}
			if tc.Two.Equal(tc.One) != tc.Result {
				t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
			}
		})
	}
}

func TestStateCompareAges(t *testing.T) {
	cases := []struct {
		Result   StateAgeComparison
		Err      bool
		One, Two *State
	}{
		{
			StateAgeEqual, false,
			&State{
				Lineage: "1",
				Serial:  2,
			},
			&State{
				Lineage: "1",
				Serial:  2,
			},
		},
		{
			StateAgeReceiverOlder, false,
			&State{
				Lineage: "1",
				Serial:  2,
			},
			&State{
				Lineage: "1",
				Serial:  3,
			},
		},
		{
			StateAgeReceiverNewer, false,
			&State{
				Lineage: "1",
				Serial:  3,
			},
			&State{
				Lineage: "1",
				Serial:  2,
			},
		},
		{
			StateAgeEqual, true,
			&State{
				Lineage: "1",
				Serial:  2,
			},
			&State{
				Lineage: "2",
				Serial:  2,
			},
		},
		{
			StateAgeEqual, true,
			&State{
				Lineage: "1",
				Serial:  3,
			},
			&State{
				Lineage: "2",
				Serial:  2,
			},
		},
	}

	for i, tc := range cases {
		result, err := tc.One.CompareAges(tc.Two)

		if err != nil && !tc.Err {
			t.Errorf(
				"%d: got error, but want success\n\n%s\n\n%s",
				i, tc.One, tc.Two,
			)
			continue
		}

		if err == nil && tc.Err {
			t.Errorf(
				"%d: got success, but want error\n\n%s\n\n%s",
				i, tc.One, tc.Two,
			)
			continue
		}

		if result != tc.Result {
			t.Errorf(
				"%d: got result %d, but want %d\n\n%s\n\n%s",
				i, result, tc.Result, tc.One, tc.Two,
			)
			continue
		}
	}
}

func TestStateSameLineage(t *testing.T) {
	cases := []struct {
		Result   bool
		One, Two *State
	}{
		{
			true,
			&State{
				Lineage: "1",
			},
			&State{
				Lineage: "1",
			},
		},
		{
			// Empty lineage is compatible with all
			true,
			&State{
				Lineage: "",
			},
			&State{
				Lineage: "1",
			},
		},
		{
			// Empty lineage is compatible with all
			true,
			&State{
				Lineage: "1",
			},
			&State{
				Lineage: "",
			},
		},
		{
			false,
			&State{
				Lineage: "1",
			},
			&State{
				Lineage: "2",
			},
		},
	}

	for i, tc := range cases {
		result := tc.One.SameLineage(tc.Two)

		if result != tc.Result {
			t.Errorf(
				"%d: got %v, but want %v\n\n%s\n\n%s",
				i, result, tc.Result, tc.One, tc.Two,
			)
			continue
		}
	}
}

func TestStateMarshalEqual(t *testing.T) {
	tests := map[string]struct {
		S1, S2 *State
		Want   bool
	}{
		"both nil": {
			nil,
			nil,
			true,
		},
		"first zero, second nil": {
			&State{},
			nil,
			false,
		},
		"first nil, second zero": {
			nil,
			&State{},
			false,
		},
		"both zero": {
			// These are not equal because they both implicitly init with
			// different lineage.
			&State{},
			&State{},
			false,
		},
		"both set, same lineage": {
			&State{
				Lineage: "abc123",
			},
			&State{
				Lineage: "abc123",
			},
			true,
		},
		"both set, same lineage, different serial": {
			&State{
				Lineage: "abc123",
				Serial:  1,
			},
			&State{
				Lineage: "abc123",
				Serial:  2,
			},
			false,
		},
		"both set, same lineage, same serial, same resources": {
			&State{
				Lineage: "abc123",
				Serial:  1,
				Modules: []*ModuleState{
					{
						Path: []string{"root"},
						Resources: map[string]*ResourceState{
							"foo_bar.baz": {},
						},
					},
				},
			},
			&State{
				Lineage: "abc123",
				Serial:  1,
				Modules: []*ModuleState{
					{
						Path: []string{"root"},
						Resources: map[string]*ResourceState{
							"foo_bar.baz": {},
						},
					},
				},
			},
			true,
		},
		"both set, same lineage, same serial, different resources": {
			&State{
				Lineage: "abc123",
				Serial:  1,
				Modules: []*ModuleState{
					{
						Path: []string{"root"},
						Resources: map[string]*ResourceState{
							"foo_bar.baz": {},
						},
					},
				},
			},
			&State{
				Lineage: "abc123",
				Serial:  1,
				Modules: []*ModuleState{
					{
						Path: []string{"root"},
						Resources: map[string]*ResourceState{
							"pizza_crust.tasty": {},
						},
					},
				},
			},
			false,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			got := test.S1.MarshalEqual(test.S2)
			if got != test.Want {
				t.Errorf("wrong result %#v; want %#v", got, test.Want)
				s1Buf := &bytes.Buffer{}
				s2Buf := &bytes.Buffer{}
				_ = WriteState(test.S1, s1Buf)
				_ = WriteState(test.S2, s2Buf)
				t.Logf("\nState 1: %s\nState 2: %s", s1Buf.Bytes(), s2Buf.Bytes())
			}
		})
	}
}

func TestStateRemove(t *testing.T) {
	cases := map[string]struct {
		Address  string
		One, Two *State
	}{
		"simple resource": {
			"test_instance.foo",
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},

							"test_instance.bar": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.bar": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
		},

		"single instance": {
			"test_instance.foo.primary",
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path:      rootModulePath,
						Resources: map[string]*ResourceState{},
					},
				},
			},
		},

		"single instance in multi-count": {
			"test_instance.foo[0]",
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo.0": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},

							"test_instance.foo.1": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo.1": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
		},

		"single resource, multi-count": {
			"test_instance.foo",
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo.0": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},

							"test_instance.foo.1": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path:      rootModulePath,
						Resources: map[string]*ResourceState{},
					},
				},
			},
		},

		"full module": {
			"module.foo",
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},

					&ModuleState{
						Path: []string{"root", "foo"},
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},

							"test_instance.bar": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
		},

		"module and children": {
			"module.foo",
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},

					&ModuleState{
						Path: []string{"root", "foo"},
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},

							"test_instance.bar": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},

					&ModuleState{
						Path: []string{"root", "foo", "bar"},
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},

							"test_instance.bar": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
			&State{
				Modules: []*ModuleState{
					&ModuleState{
						Path: rootModulePath,
						Resources: map[string]*ResourceState{
							"test_instance.foo": &ResourceState{
								Type: "test_instance",
								Primary: &InstanceState{
									ID: "foo",
								},
							},
						},
					},
				},
			},
		},
	}

	for k, tc := range cases {
		if err := tc.One.Remove(tc.Address); err != nil {
			t.Fatalf("bad: %s\n\n%s", k, err)
		}

		if !tc.One.Equal(tc.Two) {
			t.Fatalf("Bad: %s\n\n%s\n\n%s", k, tc.One.String(), tc.Two.String())
		}
	}
}

func TestResourceStateEqual(t *testing.T) {
	cases := []struct {
		Result   bool
		One, Two *ResourceState
	}{
		// Different types
		{
			false,
			&ResourceState{Type: "foo"},
			&ResourceState{Type: "bar"},
		},

		// Different dependencies
		{
			false,
			&ResourceState{Dependencies: []string{"foo"}},
			&ResourceState{Dependencies: []string{"bar"}},
		},

		{
			false,
			&ResourceState{Dependencies: []string{"foo", "bar"}},
			&ResourceState{Dependencies: []string{"foo"}},
		},

		{
			true,
			&ResourceState{Dependencies: []string{"bar", "foo"}},
			&ResourceState{Dependencies: []string{"foo", "bar"}},
		},

		// Different primaries
		{
			false,
			&ResourceState{Primary: nil},
			&ResourceState{Primary: &InstanceState{ID: "foo"}},
		},

		{
			true,
			&ResourceState{Primary: &InstanceState{ID: "foo"}},
			&ResourceState{Primary: &InstanceState{ID: "foo"}},
		},

		// Different tainted
		{
			false,
			&ResourceState{
				Primary: &InstanceState{
					ID: "foo",
				},
			},
			&ResourceState{
				Primary: &InstanceState{
					ID:      "foo",
					Tainted: true,
				},
			},
		},

		{
			true,
			&ResourceState{
				Primary: &InstanceState{
					ID:      "foo",
					Tainted: true,
				},
			},
			&ResourceState{
				Primary: &InstanceState{
					ID:      "foo",
					Tainted: true,
				},
			},
		},
	}

	for i, tc := range cases {
		if tc.One.Equal(tc.Two) != tc.Result {
			t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
		}
		if tc.Two.Equal(tc.One) != tc.Result {
			t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
		}
	}
}

func TestResourceStateTaint(t *testing.T) {
	cases := map[string]struct {
		Input  *ResourceState
		Output *ResourceState
	}{
		"no primary": {
			&ResourceState{},
			&ResourceState{},
		},

		"primary, not tainted": {
			&ResourceState{
				Primary: &InstanceState{ID: "foo"},
			},
			&ResourceState{
				Primary: &InstanceState{
					ID:      "foo",
					Tainted: true,
				},
			},
		},

		"primary, tainted": {
			&ResourceState{
				Primary: &InstanceState{
					ID:      "foo",
					Tainted: true,
				},
			},
			&ResourceState{
				Primary: &InstanceState{
					ID:      "foo",
					Tainted: true,
				},
			},
		},
	}

	for k, tc := range cases {
		tc.Input.Taint()
		if !reflect.DeepEqual(tc.Input, tc.Output) {
			t.Fatalf(
				"Failure: %s\n\nExpected: %#v\n\nGot: %#v",
				k, tc.Output, tc.Input)
		}
	}
}

func TestResourceStateUntaint(t *testing.T) {
	cases := map[string]struct {
		Input          *ResourceState
		ExpectedOutput *ResourceState
	}{
		"no primary, err": {
			Input:          &ResourceState{},
			ExpectedOutput: &ResourceState{},
		},

		"primary, not tainted": {
			Input: &ResourceState{
				Primary: &InstanceState{ID: "foo"},
			},
			ExpectedOutput: &ResourceState{
				Primary: &InstanceState{ID: "foo"},
			},
		},
		"primary, tainted": {
			Input: &ResourceState{
				Primary: &InstanceState{
					ID:      "foo",
					Tainted: true,
				},
			},
			ExpectedOutput: &ResourceState{
				Primary: &InstanceState{ID: "foo"},
			},
		},
	}

	for k, tc := range cases {
		tc.Input.Untaint()
		if !reflect.DeepEqual(tc.Input, tc.ExpectedOutput) {
			t.Fatalf(
				"Failure: %s\n\nExpected: %#v\n\nGot: %#v",
				k, tc.ExpectedOutput, tc.Input)
		}
	}
}

func TestInstanceStateEmpty(t *testing.T) {
	cases := map[string]struct {
		In     *InstanceState
		Result bool
	}{
		"nil is empty": {
			nil,
			true,
		},
		"non-nil but without ID is empty": {
			&InstanceState{},
			true,
		},
		"with ID is not empty": {
			&InstanceState{
				ID: "i-abc123",
			},
			false,
		},
	}

	for tn, tc := range cases {
		if tc.In.Empty() != tc.Result {
			t.Fatalf("%q expected %#v to be empty: %#v", tn, tc.In, tc.Result)
		}
	}
}

func TestInstanceStateEqual(t *testing.T) {
	cases := []struct {
		Result   bool
		One, Two *InstanceState
	}{
		// Nils
		{
			false,
			nil,
			&InstanceState{},
		},

		{
			false,
			&InstanceState{},
			nil,
		},

		// Different IDs
		{
			false,
			&InstanceState{ID: "foo"},
			&InstanceState{ID: "bar"},
		},

		// Different Attributes
		{
			false,
			&InstanceState{Attributes: map[string]string{"foo": "bar"}},
			&InstanceState{Attributes: map[string]string{"foo": "baz"}},
		},

		// Different Attribute keys
		{
			false,
			&InstanceState{Attributes: map[string]string{"foo": "bar"}},
			&InstanceState{Attributes: map[string]string{"bar": "baz"}},
		},

		{
			false,
			&InstanceState{Attributes: map[string]string{"bar": "baz"}},
			&InstanceState{Attributes: map[string]string{"foo": "bar"}},
		},
	}

	for i, tc := range cases {
		if tc.One.Equal(tc.Two) != tc.Result {
			t.Fatalf("Bad: %d\n\n%s\n\n%s", i, tc.One.String(), tc.Two.String())
		}
	}
}

func TestStateEmpty(t *testing.T) {
	cases := []struct {
		In     *State
		Result bool
	}{
		{
			nil,
			true,
		},
		{
			&State{},
			true,
		},
		{
			&State{
				Remote: &RemoteState{Type: "foo"},
			},
			true,
		},
		{
			&State{
				Modules: []*ModuleState{
					&ModuleState{},
				},
			},
			false,
		},
	}

	for i, tc := range cases {
		if tc.In.Empty() != tc.Result {
			t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
		}
	}
}

func TestStateHasResources(t *testing.T) {
	cases := []struct {
		In     *State
		Result bool
	}{
		{
			nil,
			false,
		},
		{
			&State{},
			false,
		},
		{
			&State{
				Remote: &RemoteState{Type: "foo"},
			},
			false,
		},
		{
			&State{
				Modules: []*ModuleState{
					&ModuleState{},
				},
			},
			false,
		},
		{
			&State{
				Modules: []*ModuleState{
					&ModuleState{},
					&ModuleState{},
				},
			},
			false,
		},
		{
			&State{
				Modules: []*ModuleState{
					&ModuleState{},
					&ModuleState{
						Resources: map[string]*ResourceState{
							"foo.foo": &ResourceState{},
						},
					},
				},
			},
			true,
		},
	}

	for i, tc := range cases {
		if tc.In.HasResources() != tc.Result {
			t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
		}
	}
}

func TestStateFromFutureTofu(t *testing.T) {
	cases := []struct {
		In     string
		Result bool
	}{
		{
			"",
			false,
		},
		{
			"0.1",
			false,
		},
		{
			"999.15.1",
			true,
		},
	}

	for _, tc := range cases {
		state := &State{TFVersion: tc.In}
		actual := state.FromFutureTofu()
		if actual != tc.Result {
			t.Fatalf("%s: bad: %v", tc.In, actual)
		}
	}
}

func TestStateIsRemote(t *testing.T) {
	cases := []struct {
		In     *State
		Result bool
	}{
		{
			nil,
			false,
		},
		{
			&State{},
			false,
		},
		{
			&State{
				Remote: &RemoteState{Type: "foo"},
			},
			true,
		},
	}

	for i, tc := range cases {
		if tc.In.IsRemote() != tc.Result {
			t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
		}
	}
}

func TestInstanceState_MergeDiff(t *testing.T) {
	is := InstanceState{
		ID: "foo",
		Attributes: map[string]string{
			"foo":  "bar",
			"port": "8000",
		},
	}

	diff := &InstanceDiff{
		Attributes: map[string]*ResourceAttrDiff{
			"foo": &ResourceAttrDiff{
				Old: "bar",
				New: "baz",
			},
			"bar": &ResourceAttrDiff{
				Old: "",
				New: "foo",
			},
			"baz": &ResourceAttrDiff{
				Old:         "",
				New:         "foo",
				NewComputed: true,
			},
			"port": &ResourceAttrDiff{
				NewRemoved: true,
			},
		},
	}

	is2 := is.MergeDiff(diff)

	expected := map[string]string{
		"foo": "baz",
		"bar": "foo",
		"baz": hcl2shim.UnknownVariableValue,
	}

	if !reflect.DeepEqual(expected, is2.Attributes) {
		t.Fatalf("bad: %#v", is2.Attributes)
	}
}

// GH-12183. This tests that a list with a computed set generates the
// right partial state. This never failed but is put here for completion
// of the test case for GH-12183.
func TestInstanceState_MergeDiff_computedSet(t *testing.T) {
	is := InstanceState{}

	diff := &InstanceDiff{
		Attributes: map[string]*ResourceAttrDiff{
			"config.#": &ResourceAttrDiff{
				Old:         "0",
				New:         "1",
				RequiresNew: true,
			},

			"config.0.name": &ResourceAttrDiff{
				Old: "",
				New: "hello",
			},

			"config.0.rules.#": &ResourceAttrDiff{
				Old:         "",
				NewComputed: true,
			},
		},
	}

	is2 := is.MergeDiff(diff)

	expected := map[string]string{
		"config.#":         "1",
		"config.0.name":    "hello",
		"config.0.rules.#": hcl2shim.UnknownVariableValue,
	}

	if !reflect.DeepEqual(expected, is2.Attributes) {
		t.Fatalf("bad: %#v", is2.Attributes)
	}
}

func TestInstanceState_MergeDiff_nil(t *testing.T) {
	var is *InstanceState

	diff := &InstanceDiff{
		Attributes: map[string]*ResourceAttrDiff{
			"foo": &ResourceAttrDiff{
				Old: "",
				New: "baz",
			},
		},
	}

	is2 := is.MergeDiff(diff)

	expected := map[string]string{
		"foo": "baz",
	}

	if !reflect.DeepEqual(expected, is2.Attributes) {
		t.Fatalf("bad: %#v", is2.Attributes)
	}
}

func TestInstanceState_MergeDiff_nilDiff(t *testing.T) {
	is := InstanceState{
		ID: "foo",
		Attributes: map[string]string{
			"foo": "bar",
		},
	}

	is2 := is.MergeDiff(nil)

	expected := map[string]string{
		"foo": "bar",
	}

	if !reflect.DeepEqual(expected, is2.Attributes) {
		t.Fatalf("bad: %#v", is2.Attributes)
	}
}

func TestReadWriteState(t *testing.T) {
	state := &State{
		Serial:  9,
		Lineage: "5d1ad1a1-4027-4665-a908-dbe6adff11d8",
		Remote: &RemoteState{
			Type: "http",
			Config: map[string]string{
				"url": "http://my-cool-server.com/",
			},
		},
		Modules: []*ModuleState{
			&ModuleState{
				Path: rootModulePath,
				Dependencies: []string{
					"aws_instance.bar",
				},
				Resources: map[string]*ResourceState{
					"foo": &ResourceState{
						Primary: &InstanceState{
							ID: "bar",
							Ephemeral: EphemeralState{
								ConnInfo: map[string]string{
									"type":     "ssh",
									"user":     "root",
									"password": "supersecret",
								},
							},
						},
					},
				},
			},
		},
	}
	state.init()

	buf := new(bytes.Buffer)
	if err := WriteState(state, buf); err != nil {
		t.Fatalf("err: %s", err)
	}

	// Verify that the version and serial are set
	if state.Version != StateVersion {
		t.Fatalf("bad version number: %d", state.Version)
	}

	actual, err := ReadState(buf)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	// ReadState should not restore sensitive information!
	mod := state.RootModule()
	mod.Resources["foo"].Primary.Ephemeral = EphemeralState{}
	mod.Resources["foo"].Primary.Ephemeral.init()

	if !reflect.DeepEqual(actual, state) {
		t.Logf("expected:\n%#v", state)
		t.Fatalf("got:\n%#v", actual)
	}
}

func TestReadStateNewVersion(t *testing.T) {
	type out struct {
		Version int
	}

	buf, err := json.Marshal(&out{StateVersion + 1})
	if err != nil {
		t.Fatalf("err: %v", err)
	}

	s, err := ReadState(bytes.NewReader(buf))
	if s != nil {
		t.Fatalf("unexpected: %#v", s)
	}
	if !strings.Contains(err.Error(), "does not support state version") {
		t.Fatalf("err: %v", err)
	}
}

func TestReadStateEmptyOrNilFile(t *testing.T) {
	var emptyState bytes.Buffer
	_, err := ReadState(&emptyState)
	if err != ErrNoState {
		t.Fatal("expected ErrNostate, got", err)
	}

	var nilFile *os.File
	_, err = ReadState(nilFile)
	if err != ErrNoState {
		t.Fatal("expected ErrNostate, got", err)
	}
}

func TestReadStateTFVersion(t *testing.T) {
	type tfVersion struct {
		Version   int    `json:"version"`
		TFVersion string `json:"terraform_version"`
	}

	cases := []struct {
		Written string
		Read    string
		Err     bool
	}{
		{
			"0.0.0",
			"0.0.0",
			false,
		},
		{
			"",
			"",
			false,
		},
		{
			"bad",
			"",
			true,
		},
	}

	for _, tc := range cases {
		buf, err := json.Marshal(&tfVersion{
			Version:   2,
			TFVersion: tc.Written,
		})
		if err != nil {
			t.Fatalf("err: %v", err)
		}

		s, err := ReadState(bytes.NewReader(buf))
		if (err != nil) != tc.Err {
			t.Fatalf("%s: err: %s", tc.Written, err)
		}
		if err != nil {
			continue
		}

		if s.TFVersion != tc.Read {
			t.Fatalf("%s: bad: %s", tc.Written, s.TFVersion)
		}
	}
}

func TestWriteStateTFVersion(t *testing.T) {
	cases := []struct {
		Write string
		Read  string
		Err   bool
	}{
		{
			"0.0.0",
			"0.0.0",
			false,
		},
		{
			"",
			"",
			false,
		},
		{
			"bad",
			"",
			true,
		},
	}

	for _, tc := range cases {
		var buf bytes.Buffer
		err := WriteState(&State{TFVersion: tc.Write}, &buf)
		if (err != nil) != tc.Err {
			t.Fatalf("%s: err: %s", tc.Write, err)
		}
		if err != nil {
			continue
		}

		s, err := ReadState(&buf)
		if err != nil {
			t.Fatalf("%s: err: %s", tc.Write, err)
		}

		if s.TFVersion != tc.Read {
			t.Fatalf("%s: bad: %s", tc.Write, s.TFVersion)
		}
	}
}

func TestParseResourceStateKey(t *testing.T) {
	cases := []struct {
		Input       string
		Expected    *ResourceStateKey
		ExpectedErr bool
	}{
		{
			Input: "aws_instance.foo.3",
			Expected: &ResourceStateKey{
				Mode:  ManagedResourceMode,
				Type:  "aws_instance",
				Name:  "foo",
				Index: 3,
			},
		},
		{
			Input: "aws_instance.foo.0",
			Expected: &ResourceStateKey{
				Mode:  ManagedResourceMode,
				Type:  "aws_instance",
				Name:  "foo",
				Index: 0,
			},
		},
		{
			Input: "aws_instance.foo",
			Expected: &ResourceStateKey{
				Mode:  ManagedResourceMode,
				Type:  "aws_instance",
				Name:  "foo",
				Index: -1,
			},
		},
		{
			Input: "data.aws_ami.foo",
			Expected: &ResourceStateKey{
				Mode:  DataResourceMode,
				Type:  "aws_ami",
				Name:  "foo",
				Index: -1,
			},
		},
		{
			Input:       "aws_instance.foo.malformed",
			ExpectedErr: true,
		},
		{
			Input:       "aws_instance.foo.malformedwithnumber.123",
			ExpectedErr: true,
		},
		{
			Input:       "malformed",
			ExpectedErr: true,
		},
	}
	for _, tc := range cases {
		rsk, err := ParseResourceStateKey(tc.Input)
		if rsk != nil && tc.Expected != nil && !rsk.Equal(tc.Expected) {
			t.Fatalf("%s: expected %s, got %s", tc.Input, tc.Expected, rsk)
		}
		if (err != nil) != tc.ExpectedErr {
			t.Fatalf("%s: expected err: %t, got %s", tc.Input, tc.ExpectedErr, err)
		}
	}
}

func TestReadState_prune(t *testing.T) {
	state := &State{
		Modules: []*ModuleState{
			&ModuleState{Path: rootModulePath},
			nil,
		},
	}
	state.init()

	buf := new(bytes.Buffer)
	if err := WriteState(state, buf); err != nil {
		t.Fatalf("err: %s", err)
	}

	actual, err := ReadState(buf)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	expected := &State{
		Version: state.Version,
		Lineage: state.Lineage,
	}
	expected.init()

	if !reflect.DeepEqual(actual, expected) {
		t.Fatalf("got:\n%#v", actual)
	}
}

func TestReadState_pruneDependencies(t *testing.T) {
	state := &State{
		Serial:  9,
		Lineage: "5d1ad1a1-4027-4665-a908-dbe6adff11d8",
		Remote: &RemoteState{
			Type: "http",
			Config: map[string]string{
				"url": "http://my-cool-server.com/",
			},
		},
		Modules: []*ModuleState{
			&ModuleState{
				Path: rootModulePath,
				Dependencies: []string{
					"aws_instance.bar",
					"aws_instance.bar",
				},
				Resources: map[string]*ResourceState{
					"foo": &ResourceState{
						Dependencies: []string{
							"aws_instance.baz",
							"aws_instance.baz",
						},
						Primary: &InstanceState{
							ID: "bar",
						},
					},
				},
			},
		},
	}
	state.init()

	buf := new(bytes.Buffer)
	if err := WriteState(state, buf); err != nil {
		t.Fatalf("err: %s", err)
	}

	actual, err := ReadState(buf)
	if err != nil {
		t.Fatalf("err: %s", err)
	}

	// make sure the duplicate Dependencies are filtered
	modDeps := actual.Modules[0].Dependencies
	resourceDeps := actual.Modules[0].Resources["foo"].Dependencies

	if len(modDeps) > 1 || modDeps[0] != "aws_instance.bar" {
		t.Fatalf("expected 1 module depends_on entry, got %q", modDeps)
	}

	if len(resourceDeps) > 1 || resourceDeps[0] != "aws_instance.baz" {
		t.Fatalf("expected 1 resource depends_on entry, got  %q", resourceDeps)
	}
}

func TestReadState_bigHash(t *testing.T) {
	expected := uint64(14885267135666261723)
	s := strings.NewReader(`{"version": 3, "backend":{"hash":14885267135666261723}}`)

	actual, err := ReadState(s)
	if err != nil {
		t.Fatal(err)
	}

	if actual.Backend.Hash != expected {
		t.Fatalf("expected backend hash %d, got %d", expected, actual.Backend.Hash)
	}
}

func TestResourceNameSort(t *testing.T) {
	names := []string{
		"a",
		"b",
		"a.0",
		"a.c",
		"a.d",
		"c",
		"a.b.0",
		"a.b.1",
		"a.b.10",
		"a.b.2",
	}

	sort.Sort(resourceNameSort(names))

	expected := []string{
		"a",
		"a.0",
		"a.b.0",
		"a.b.1",
		"a.b.2",
		"a.b.10",
		"a.c",
		"a.d",
		"b",
		"c",
	}

	if !reflect.DeepEqual(names, expected) {
		t.Fatalf("got: %q\nexpected: %q\n", names, expected)
	}
}
