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

package workdir

import (
	"encoding/json"
	"strings"
	"testing"

	"github.com/google/go-cmp/cmp"
	tfaddr "github.com/hashicorp/terraform-registry-address"
	"github.com/hashicorp/terraform/internal/getproviders"
	"github.com/hashicorp/terraform/version"
)

func TestParseBackendStateFile(t *testing.T) {
	tests := map[string]struct {
		Input   string
		Want    *BackendStateFile
		WantErr string
	}{
		"empty": {
			Input:   ``,
			WantErr: `invalid syntax: unexpected end of JSON input`,
		},
		"empty but valid JSON syntax": {
			Input:   `{}`,
			WantErr: `invalid syntax: no format version number`,
		},
		"older version": {
			Input: `{
				"version": 2,
				"terraform_version": "0.3.0"
			}`,
			WantErr: `unsupported backend state version 2; you may need to use Terraform CLI v0.3.0 to work in this directory`,
		},
		"newer version": {
			Input: `{
				"version": 4,
				"terraform_version": "54.23.9"
			}`,
			WantErr: `unsupported backend state version 4; you may need to use Terraform CLI v54.23.9 to work in this directory`,
		},
		"legacy remote state is active": {
			Input: `{
				"version": 3,
				"terraform_version": "0.8.0",
				"remote": {
					"anything": "goes"
				}
			}`,
			WantErr: `this working directory uses legacy remote state and so must first be upgraded using Terraform v0.9`,
		},
		"active backend": {
			Input: `{
				"version": 3,
				"terraform_version": "0.8.0",
				"backend": {
					"type": "treasure_chest_buried_on_a_remote_island",
					"config": {},
					"hash" : 12345
				}
			}`,
			Want: &BackendStateFile{
				Version:   3,
				TFVersion: "0.8.0",
				Backend: &BackendConfigState{
					Type:      "treasure_chest_buried_on_a_remote_island",
					ConfigRaw: json.RawMessage("{}"),
					Hash:      12345,
				},
			},
		},
		"active state_store": {
			Input: `{
				"version": 3,
				"terraform_version": "9.9.9",
				"state_store": {
					"type": "foobar_baz",
					"config": {
						"bucket": "my-bucket",
						"region": "saturn"
					},
					"provider": {
						"version": "1.2.3",
						"source": "registry.terraform.io/my-org/foobar",
						"config": {
							"credentials": "./creds.json"
						},
						"hash" : 12345
					},
					"hash" : 12345,
					"provider_supply_mode": "managed_by_terraform"
				}
			}`,
			Want: &BackendStateFile{
				Version:   3,
				TFVersion: "9.9.9",
				StateStore: &StateStoreConfigState{
					Type: "foobar_baz",
					// Watch out - the number of tabs in the last argument here are load-bearing
					Provider: getTestProviderState(t, "1.2.3", "registry.terraform.io", "my-org", "foobar", `{
							"credentials": "./creds.json"
						}`),
					ConfigRaw: json.RawMessage(`{
						"bucket": "my-bucket",
						"region": "saturn"
					}`),
					Hash:               12345,
					ProviderSupplyMode: getproviders.ManagedByTerraform,
				},
			},
		},
		"detection of malformed state: conflicting 'backend' and 'state_store' sections": {
			Input: `{
				"version": 3,
				"terraform_version": "9.9.9",
				"backend": {
					"type": "treasure_chest_buried_on_a_remote_island",
					"config": {},
					"hash" : 12345
				},
				"state_store": {
					"type": "foobar_baz",
					"config": {
						"provider": "foobar",
						"bucket": "my-bucket"
					},
					"provider": {
						"version": "1.2.3",
						"source": "registry.terraform.io/my-org/foobar",
						"hash" : 12345
					},
					"hash" : 12345
				}
			}`,
			WantErr: `encountered a malformed backend state file that contains state for both a 'backend' and a 'state_store' block`,
		},
		"detection of malformed state: missing 'provider_supply_mode' when state store is in use": {
			Input: `{
				"version": 3,
				"terraform_version": "9.9.9",
				"state_store": {
					"type": "foobar_baz",
					"config": {
						"provider": "foobar",
						"bucket": "my-bucket"
					},
					"provider": {
						"version": "1.2.3",
						"source": "registry.terraform.io/my-org/foobar",
						"hash" : 12345
					},
					"hash" : 12345
				}
			}`,
			WantErr: `encountered a malformed backend state file with a 'state_store' block that is missing the required 'provider_supply_mode' property`,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			got, err := ParseBackendStateFile([]byte(test.Input))

			if test.WantErr != "" {
				if err == nil {
					t.Fatalf("unexpected success\nwant error: %s", test.WantErr)
				}
				if got, want := err.Error(), test.WantErr; got != want {
					t.Errorf("wrong error\ngot:  %s\nwant: %s", got, want)
				}
				return
			}

			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}
			if diff := cmp.Diff(test.Want, got); diff != "" {
				t.Errorf("wrong result\n%s", diff)
			}
		})
	}
}

func TestEncodeBackendStateFile(t *testing.T) {
	noVersionData := ""

	tfVersion := version.Version
	tests := map[string]struct {
		Input   *BackendStateFile
		Want    []byte
		WantErr string
	}{
		"encoding a backend state file when state_store is in use": {
			Input: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type:               "foobar_baz",
					Provider:           getTestProviderState(t, "1.2.3", "registry.terraform.io", "my-org", "foobar", `{"foo": "bar"}`),
					ProviderSupplyMode: getproviders.ManagedByTerraform,
					ConfigRaw:          json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:               123,
				},
			},
			Want: []byte(`{
  "version": 3,
  "terraform_version": "` + tfVersion + `",
  "state_store": {
    "type": "foobar_baz",
    "provider": {
      "version": "1.2.3",
      "source": "registry.terraform.io/my-org/foobar",
      "config": {
        "foo": "bar"
      }
    },
    "config": {
      "foo": "bar"
    },
    "hash": 123,
    "provider_supply_mode": "managed_by_terraform"
  }
}`),
		},
		"it's valid to record no version data when a builtin provider is used for state store": {
			Input: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type: "foobar_baz",
					// Note - no version data in the provider description below
					Provider:           getTestProviderState(t, noVersionData, string(tfaddr.BuiltInProviderHost), string(tfaddr.BuiltInProviderNamespace), "foobar", `{"foo": "bar"}`),
					ProviderSupplyMode: getproviders.BuiltIn,
					ConfigRaw:          json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:               123,
				},
			},
			Want: []byte(`{
  "version": 3,
  "terraform_version": "` + tfVersion + `",
  "state_store": {
    "type": "foobar_baz",
    "provider": {
      "version": null,
      "source": "terraform.io/builtin/foobar",
      "config": {
        "foo": "bar"
      }
    },
    "config": {
      "foo": "bar"
    },
    "hash": 123,
    "provider_supply_mode": "built_in"
  }
}`),
		},
		"it's valid to record no version data when a re-attached provider is used for state store": {
			Input: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type: "foobar_baz",
					// Note - no version data in the provider description below
					Provider:           getTestProviderState(t, noVersionData, "registry.terraform.io", "hashicorp", "foobar", `{"foo": "bar"}`),
					ProviderSupplyMode: getproviders.Reattached,
					ConfigRaw:          json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:               123,
				},
			},
			Want: []byte(`{
  "version": 3,
  "terraform_version": "` + tfVersion + `",
  "state_store": {
    "type": "foobar_baz",
    "provider": {
      "version": null,
      "source": "registry.terraform.io/hashicorp/foobar",
      "config": {
        "foo": "bar"
      }
    },
    "config": {
      "foo": "bar"
    },
    "hash": 123,
    "provider_supply_mode": "reattached"
  }
}`),
		},
		"it's valid to record no version data when a developer override provider is used for state store": {
			Input: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type: "foobar_baz",
					// Note - no version data in the provider description below
					Provider:           getTestProviderState(t, noVersionData, "registry.terraform.io", "hashicorp", "foobar", `{"foo": "bar"}`),
					ProviderSupplyMode: getproviders.DevOverride,
					ConfigRaw:          json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:               123,
				},
			},
			Want: []byte(`{
  "version": 3,
  "terraform_version": "` + tfVersion + `",
  "state_store": {
    "type": "foobar_baz",
    "provider": {
      "version": null,
      "source": "registry.terraform.io/hashicorp/foobar",
      "config": {
        "foo": "bar"
      }
    },
    "config": {
      "foo": "bar"
    },
    "hash": 123,
    "provider_supply_mode": "dev_override"
  }
}`),
		},
		"error when neither backend nor state_store config state are present": {
			Input: &BackendStateFile{},
			Want: []byte(`{
  "version": 3,
  "terraform_version": "` + tfVersion + `"
}`),
		},
		"error when the provider is managed by Terraform and the provider version is missing": {
			Input: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type: "foobar_baz",
					// Note - no version data in the provider description below
					Provider:           getTestProviderState(t, noVersionData, "registry.terraform.io", "my-org", "foobar", ""),
					ProviderSupplyMode: getproviders.ManagedByTerraform,
					ConfigRaw:          json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:               123,
				},
			},
			WantErr: `state store is not valid: provider version data is missing`,
		},
		"error when the provider source's hostname is missing": {
			Input: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type:      "foobar_baz",
					Provider:  getTestProviderState(t, "1.2.3", "", "my-org", "foobar", ""),
					ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:      123,
				},
			},
			WantErr: `state store is not valid: Unknown hostname: Expected hostname in the provider address to be set`,
		},
		"error when the provider source's hostname and namespace are missing ": {
			Input: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type:      "foobar_baz",
					Provider:  getTestProviderState(t, "1.2.3", "", "", "foobar", ""),
					ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:      123,
				},
			},
			WantErr: `state store is not valid: Unknown hostname: Expected hostname in the provider address to be set`,
		},
		"error when the provider source is completely missing ": {
			Input: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type:      "foobar_baz",
					Provider:  getTestProviderState(t, "1.2.3", "", "", "", ""),
					ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:      123,
				},
			},
			WantErr: `state store is not valid: Empty provider address: Expected address composed of hostname, provider namespace and name`,
		},
		"error when both backend and state_store config state are present": {
			Input: &BackendStateFile{
				Backend: &BackendConfigState{
					Type:      "foobar",
					ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:      123,
				},
				StateStore: &StateStoreConfigState{
					Type:      "foobar_baz",
					Provider:  getTestProviderState(t, "1.2.3", "registry.terraform.io", "my-org", "foobar", ""),
					ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:      123,
				},
			},
			WantErr: `attempted to encode a malformed backend state file; it contains state for both a 'backend' and a 'state_store' block`,
		},
	}

	for name, test := range tests {
		t.Run(name, func(t *testing.T) {
			got, err := EncodeBackendStateFile(test.Input)

			if test.WantErr != "" {
				if err == nil {
					t.Fatalf("unexpected success\nwant error: %s", test.WantErr)
				}
				if !strings.Contains(err.Error(), test.WantErr) {
					t.Errorf("wrong error\ngot:  %s\nwant: %s", err.Error(), test.WantErr)
				}
				return
			}

			if err != nil {
				t.Fatalf("unexpected error: %s", err)
			}
			if diff := cmp.Diff(test.Want, got); diff != "" {
				t.Errorf("wrong result\n%s", diff)
			}
		})
	}
}

func TestBackendStateFile_DeepCopy(t *testing.T) {
	tests := map[string]struct {
		file *BackendStateFile
	}{
		"Deep copy preserves state_store data": {
			file: &BackendStateFile{
				StateStore: &StateStoreConfigState{
					Type:      "foo_bar",
					Provider:  getTestProviderState(t, "1.2.3", "A", "B", "C", ""),
					ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:      123,
				},
			},
		},
		"Deep copy preserves backend data": {
			file: &BackendStateFile{
				Backend: &BackendConfigState{
					Type:      "foobar",
					ConfigRaw: json.RawMessage([]byte(`{"foo":"bar"}`)),
					Hash:      123,
				},
			},
		},
		"Deep copy preserves version and Terraform version data": {
			file: &BackendStateFile{
				Version:   3,
				TFVersion: "9.9.9",
			},
		},
	}

	for tn, tc := range tests {
		t.Run(tn, func(t *testing.T) {
			copy := tc.file.DeepCopy()

			if diff := cmp.Diff(copy, tc.file); diff != "" {
				t.Fatalf("unexpected difference in backend state data:\n %s", diff)
			}
		})
	}
}
