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

package stackmigrate

import (
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path"
	"path/filepath"
	"strings"
	"testing"

	svchost "github.com/hashicorp/terraform-svchost"
	"github.com/hashicorp/terraform-svchost/auth"
	"github.com/hashicorp/terraform-svchost/disco"
	"github.com/hashicorp/terraform/internal/addrs"
	"github.com/hashicorp/terraform/internal/httpclient"
	"github.com/hashicorp/terraform/internal/states"
	"github.com/hashicorp/terraform/internal/states/statefile"
	"github.com/hashicorp/terraform/version"
	"github.com/zclconf/go-cty/cty"
)

func TestLoad_Local(t *testing.T) {
	state := states.BuildState(func(s *states.SyncState) {
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "baz",
			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON:    []byte(`{"id":"foo","foo":"value","bar":"value"}`),
				Status:       states.ObjectReady,
				Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)
	})
	statePath := TestStateFile(t, state)
	loader := &Loader{}
	loadedState, diags := loader.LoadState(strings.TrimSuffix(statePath, "/terraform.tfstate"))
	if diags.HasErrors() {
		t.Fatalf("failed to load state: %s", diags.Err())
	}

	if !statefile.StatesMarshalEqual(state, loadedState) {
		t.Fatalf("loaded state does not match original state")
	}
}

func TestLoad(t *testing.T) {
	state := states.BuildState(func(s *states.SyncState) {
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "foo",
			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON: []byte(`{"id":"bar","foo":"value","bar":"value"}`),
				Status:    states.ObjectReady,
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)
		s.SetResourceInstanceCurrent(
			addrs.Resource{
				Mode: addrs.ManagedResourceMode,
				Type: "test_instance",
				Name: "baz",
			}.Instance(addrs.NoKey).Absolute(addrs.RootModuleInstance),
			&states.ResourceInstanceObjectSrc{
				AttrsJSON:    []byte(`{"id":"foo","foo":"value","bar":"value"}`),
				Status:       states.ObjectReady,
				Dependencies: []addrs.ConfigResource{mustResourceAddr("test_instance.foo")},
			},
			addrs.AbsProviderConfig{
				Provider: addrs.NewDefaultProvider("test"),
				Module:   addrs.RootModule,
			},
		)
	})
	statePath := TestStateFile(t, state)

	s := testServer(t, statePath)
	backendStatePath := testBackendStateFile(t, cty.ObjectVal(map[string]cty.Value{
		"organization": cty.StringVal("hashicorp"),
		"hostname":     cty.StringVal("localhost"),
		"workspaces": cty.ObjectVal(map[string]cty.Value{
			"name":   cty.NullVal(cty.String),
			"prefix": cty.StringVal("my-app-"),
		}),
	}))
	dir := strings.TrimSuffix(backendStatePath, ".terraform/.terraform.tfstate")
	defer s.Close()
	loader := Loader{Discovery: testDisco(s)}
	t.Setenv(WorkspaceNameEnvVar, "test")
	loadedState, diags := loader.LoadState(dir)
	if diags.HasErrors() {
		t.Fatalf("failed to load state: %s", diags.Err())
	}

	if !statefile.StatesMarshalEqual(state, loadedState) {
		t.Fatalf("loaded state does not match original state")
	}
}

func mustResourceAddr(s string) addrs.ConfigResource {
	addr, diags := addrs.ParseAbsResourceStr(s)
	if diags.HasErrors() {
		panic(diags.Err())
	}
	return addr.Config()
}

func testBackendStateFile(t *testing.T, value cty.Value) string {
	t.Helper()

	path := filepath.Join(t.TempDir(), ".terraform", ".terraform.tfstate")

	err := os.MkdirAll(filepath.Dir(path), 0755)
	if err != nil {
		t.Fatalf("failed to create directories for temporary state file %s: %s", path, err)
	}

	f, err := os.Create(path)
	if err != nil {
		t.Fatalf("failed to create temporary state file %s: %s", path, err)
	}

	fmt.Fprintf(f, `{
	  "version": 3,
	  "terraform_version": "1.9.4",
	  "backend": {
	    "type": "remote",
	    "config": {
	      "hostname": %q,
	      "organization": %q,
	      "token": "foo",
	      "workspaces": {
	        "name": null,
	        "prefix": %q
	      }
	    },
	    "hash": 2143736989
	  }
	}`, value.GetAttr("hostname").AsString(),
		value.GetAttr("organization").AsString(),
		value.GetAttr("workspaces").GetAttr("prefix").AsString())

	f.Close()
	return path
}

func createTempFile(t *testing.T, dir, filename, content string) string {
	t.Helper()
	filePath := filepath.Join(dir, filename)
	err := os.WriteFile(filePath, []byte(content), 0644)
	if err != nil {
		t.Fatalf("failed to write temp file: %v", err)
	}
	return filePath
}

// testDisco returns a *disco.Disco mapping app.terraform.io and
// localhost to a local test server.
func testDisco(s *httptest.Server) *disco.Disco {
	services := map[string]interface{}{
		"state.v2":    fmt.Sprintf("%s/api/v2/", s.URL),
		"tfe.v2.1":    fmt.Sprintf("%s/api/v2/", s.URL),
		"versions.v1": fmt.Sprintf("%s/v1/versions/", s.URL),
	}
	d := disco.NewWithCredentialsSource(auth.NoCredentials)
	d.SetUserAgent(httpclient.TerraformUserAgent(version.String()))

	d.ForceHostServices(svchost.Hostname("localhost"), services)
	d.ForceHostServices(svchost.Hostname("app.terraform.io"), services)
	return d
}

// testServer returns a *httptest.Server used for local testing.
// This server simulates the APIs needed to load a remote state.
func testServer(t *testing.T, statePath string) *httptest.Server {
	mux := http.NewServeMux()

	f, err := os.Open(statePath)
	if err != nil {
		t.Fatalf("failed to open state file: %s", err)
	}

	// Respond to service discovery calls.
	mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		io.WriteString(w, `{
  "state.v2": "/api/v2/",
  "tfe.v2.1": "/api/v2/",
  "versions.v1": "/v1/versions/"
	}`)
	})

	// Respond to service version constraints calls.
	mux.HandleFunc("/v1/versions/", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		io.WriteString(w, fmt.Sprintf(`{
  "service": "%s",
  "product": "terraform",
  "minimum": "0.1.0",
  "maximum": "10.0.0"
}`, path.Base(r.URL.Path)))
	})

	// Respond to pings to get the API version header.
	mux.HandleFunc("/api/v2/ping", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.Header().Set("TFP-API-Version", "2.4")
	})

	// Respond to the initial query to read the hashicorp org entitlements.
	mux.HandleFunc("/api/v2/organizations/hashicorp/entitlement-set", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/vnd.api+json")
		io.WriteString(w, `{
  "data": {
    "id": "org-GExadygjSbKP8hsY",
    "type": "entitlement-sets",
    "attributes": {
      "operations": true,
      "private-module-registry": true,
      "sentinel": true,
      "state-storage": true,
      "teams": true,
      "vcs-integrations": true
    }
  }
}`)
	})

	// Respond to the initial query to read the no-operations org entitlements.
	mux.HandleFunc("/api/v2/organizations/no-operations/entitlement-set", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/vnd.api+json")
		io.WriteString(w, `{
  "data": {
    "id": "org-ufxa3y8jSbKP8hsT",
    "type": "entitlement-sets",
    "attributes": {
      "operations": false,
      "private-module-registry": true,
      "sentinel": true,
      "state-storage": true,
      "teams": true,
      "vcs-integrations": true
    }
  }
}`)
	})

	mux.HandleFunc("/api/v2/organizations/hashicorp/workspaces/my-app-test", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		io.WriteString(w, `{
	"data": {
		"id": "ws-EUht4zmoJaZTZMv8",
		"type": "workspaces",
		"attributes": {
			"locked": false,
			"name": "my-app-test",
			"queue-all-runs": false,
			"speculative-enabled": true,
			"structured-run-output-enabled": true,
			"terraform-version": "1.9.4",
			"operations": true,
			"execution-mode": "remote",
			"file-triggers-enabled": true,
			"locked-reason": "",
			"source": "terraform"
		}
	}
}`)
	})

	mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/actions/lock", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		io.WriteString(w, `{
    "data": {
        "id": "ws-EUht4zmoJaZTZMv8",
        "type": "workspaces",
        "attributes": {
            "locked": true,
            "name": "my-app-test",
            "queue-all-runs": false,
            "speculative-enabled": true,
            "structured-run-output-enabled": true,
            "terraform-version": "1.9.4",
            "source": "terraform",
            "source-name": null,
            "source-url": null,
            "tag-names": []
        }
    }
}`)
	})

	mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/current-state-version", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		io.WriteString(w, `
		{
			"data": {
            "id": "sv-XJmHFY12zJFmwkWN",
            "type": "state-versions",
            "attributes": {
                "created-at": "2025-02-12T14:16:43.541Z",
                "size": 878,
                "hosted-state-download-url": "/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_state",
                "hosted-json-state-download-url": "/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_json_state",
                "serial": 1,
                "state-version": 4,
                "status": "finalized",
                "terraform-version": "1.9.4"
            }
        }
		}
		`)
	})

	mux.HandleFunc("/api/state-versions/sv-XJmHFY12zJFmwkWN/hosted_state", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		io.Copy(w, f)
	})

	mux.HandleFunc("/api/v2/workspaces/ws-EUht4zmoJaZTZMv8/actions/unlock", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(200)
		io.WriteString(w, `{
	"data": {
		"id": "ws-EUht4zmoJaZTZMv8",
		"type": "workspaces",
		"attributes": {
			"locked": false,
			"name": "my-app-test",
			"queue-all-runs": false,
			"speculative-enabled": true,
			"structured-run-output-enabled": true,
			"terraform-version": "1.9.4",
			"source": "terraform",
			"source-name": null,
			"source-url": null,
			"tag-names": []
		}
	}
}`)
	})

	return httptest.NewServer(mux)
}
