package cmd

import (
	"context"
	"encoding/json"
	"net/http"
	"net/url"
	"strings"
	"testing"

	"github.com/tidbcloud/clinic-cli/internal/clinic"
	"github.com/tidbcloud/clinic-cli/internal/config"
)

func TestClusterSearchBuildsDashboardClusterQuery(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"items":[{"clusterID":"c1"}]}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"search",
		"--query", "tenant-a",
		"--show-deleted",
		"--limit", "25",
		"--page", "3",
		"--cluster-status", "active",
		"--deploy-type", "premium",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if got, want := client.request.Method, http.MethodGet; got != want {
		t.Fatalf("method = %q, want %q", got, want)
	}
	if got, want := client.request.Path, "/clinic/api/v1/dashboard/clusters"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	assertQueryValue(t, client.request.Query, "query", "tenant-a")
	assertQueryValue(t, client.request.Query, "show_deleted", "true")
	assertQueryValue(t, client.request.Query, "limit", "25")
	assertQueryValue(t, client.request.Query, "page", "3")
	assertQueryValue(t, client.request.Query, "cluster_status", "active")
	assertQueryValue(t, client.request.Query, "deploy_type_v2", "premium")
	assertJSONEqual(t, stdout.String(), `{"items":[{"clusterID":"c1"}]}`)
}

func TestClusterSearchAllowsDeployTypeWithoutQuery(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"items":[{"clusterID":"c1"}],"total":1}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"search",
		"--deploy-type", "premium resource pool",
		"--limit", "100",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if got, want := client.request.Path, "/clinic/api/v1/dashboard/clusters"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	assertQueryValue(t, client.request.Query, "deploy_type_v2", "premium resource pool")
	assertQueryValue(t, client.request.Query, "limit", "100")
	assertQueryValue(t, client.request.Query, "show_deleted", "false")
	if got := client.request.Query.Get("query"); got != "" {
		t.Fatalf("query param = %q, want empty", got)
	}
	assertJSONEqual(t, stdout.String(), `{"items":[{"clusterID":"c1"}],"total":1}`)
}

func TestClusterSearchRequiresQueryOrDeployType(t *testing.T) {
	t.Parallel()

	ctx := &RuntimeContext{Client: &recordingClinicClient{}}
	want := "missing required filter: provide --query or --deploy-type"
	cases := []struct {
		name string
		args []string
	}{
		{name: "no filters", args: []string{"search", "--limit", "10"}},
	}
	for _, tc := range cases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			var stdout strings.Builder
			err := clusterCommandHandler(ctx, tc.args, &stdout)
			if err == nil {
				t.Fatal("clusterCommandHandler() error = nil, want missing filter error")
			}
			if got := err.Error(); got != want {
				t.Fatalf("clusterCommandHandler() error = %q, want %q", got, want)
			}
		})
	}
}

func TestClusterSearchIDUsesClusterIDQueryParam(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"items":[{"clusterID":"1024"}]}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"search-id",
		"--cluster-id", "1024",
		"--show-deleted",
		"--limit", "5",
		"--page", "2",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if got, want := client.request.Path, "/clinic/api/v1/dashboard/clusters"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	assertQueryValue(t, client.request.Query, "cluster_id", "1024")
	assertQueryValue(t, client.request.Query, "show_deleted", "true")
	assertQueryValue(t, client.request.Query, "limit", "5")
	assertQueryValue(t, client.request.Query, "page", "2")
	if got := client.request.Query.Get("query"); got != "" {
		t.Fatalf("query param = %q, want empty", got)
	}
}

func TestClusterSearchIDAutoCreatesEnvLocalProfileWithoutChangingActiveProfile(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	cfg.ActiveEnv = "staging"
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "green",
		Profiles: map[string]config.ProfileRecord{
			"green": {
				ClusterID: "cluster-green",
			},
		},
	}
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	responseBody := `{"items":[{"clusterID":"branch-cluster-42","orgID":"org-branch","parentID":"parent-branch","clusterDeployTypeV2":"dedicated","name":"wrong-cluster"},{"clusterID":"cluster-42","orgID":"org-42","parentID":"parent-42","clusterDeployTypeV2":"Serverless (Beta)","name":"Primary Cluster !!"}]}`
	client := &recordingClinicClient{
		responses: []clinicResponse{{body: []byte(responseBody)}},
	}
	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		Config:      cfg,
		Client:      client,
	}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"search-id",
		"--cluster-id", "cluster-42",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	assertJSONEqual(t, stdout.String(), responseBody)

	loaded := mustLoadConfig(t)
	stagingProfiles := loaded.EnvProfiles["staging"]
	if stagingProfiles.ActiveProfile != "green" {
		t.Fatalf("EnvProfiles[staging].ActiveProfile = %q, want %q", stagingProfiles.ActiveProfile, "green")
	}

	profile, ok := stagingProfiles.Profiles["cluster-42__serverless-beta__primary-cluster"]
	if !ok {
		t.Fatal("auto-created staging profile missing")
	}
	if profile.ClusterID != "cluster-42" {
		t.Fatalf("profile.ClusterID = %q, want %q", profile.ClusterID, "cluster-42")
	}
	if profile.ClusterName != "Primary Cluster !!" {
		t.Fatalf("profile.ClusterName = %q, want %q", profile.ClusterName, "Primary Cluster !!")
	}
	if profile.OrgID != "org-42" || profile.ParentID != "parent-42" {
		t.Fatalf("profile org/parent = %+v, want org-42/parent-42", profile)
	}
	if profile.ClusterDeployTypeV2 != "Serverless (Beta)" {
		t.Fatalf("profile.ClusterDeployTypeV2 = %q, want %q", profile.ClusterDeployTypeV2, "Serverless (Beta)")
	}
	if profile.RangeStart != "" || profile.RangeEnd != "" {
		t.Fatalf("profile ranges = %+v, want empty range_start/range_end", profile)
	}
}

func TestClusterSearchIDAutoCreateSkipsWhenClusterAlreadyExistsInSelectedEnv(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	cfg.ActiveEnv = "staging"
	cfg.EnvProfiles["prod"] = config.EnvProfileContainer{
		Profiles: map[string]config.ProfileRecord{
			"cluster-42__dedicated__prod-cluster": {
				ClusterID:   "cluster-42",
				ClusterName: "prod-cluster",
			},
		},
	}
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "green",
		Profiles: map[string]config.ProfileRecord{
			"green": {
				ClusterID: "cluster-green",
			},
			"custom-name": {
				ClusterID:           "cluster-42",
				ClusterName:         "existing-cluster",
				OrgID:               "keep-org",
				ParentID:            "keep-parent",
				ClusterDeployTypeV2: "keep-v2",
				RangeStart:          "10",
				RangeEnd:            "20",
			},
		},
	}
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	responseBody := `{"items":[{"clusterID":"cluster-42","orgID":"org-new","parentID":"parent-new","clusterDeployTypeV2":"serverless","name":"updated-name"}]}`
	client := &recordingClinicClient{
		responses: []clinicResponse{{body: []byte(responseBody)}},
	}
	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		Config:      cfg,
		Client:      client,
	}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"search-id",
		"--cluster-id", "cluster-42",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	assertJSONEqual(t, stdout.String(), responseBody)

	loaded := mustLoadConfig(t)
	stagingProfiles := loaded.EnvProfiles["staging"]
	if got := len(stagingProfiles.Profiles); got != 2 {
		t.Fatalf("len(EnvProfiles[staging].Profiles) = %d, want %d", got, 2)
	}

	profile := stagingProfiles.Profiles["custom-name"]
	if profile.ClusterName != "existing-cluster" || profile.OrgID != "keep-org" || profile.ParentID != "keep-parent" {
		t.Fatalf("custom-name profile mutated = %+v, want original metadata preserved", profile)
	}
	if profile.ClusterDeployTypeV2 != "keep-v2" {
		t.Fatalf("custom-name ClusterDeployTypeV2 mutated = %q, want %q", profile.ClusterDeployTypeV2, "keep-v2")
	}
	if profile.RangeStart != "10" || profile.RangeEnd != "20" {
		t.Fatalf("custom-name ranges mutated = %+v, want 10/20", profile)
	}
	if _, ok := stagingProfiles.Profiles["cluster-42__serverless__updated-name"]; ok {
		t.Fatal("unexpected auto-created profile when cluster_id already existed in selected env")
	}
	if got := loaded.EnvProfiles["prod"].Profiles["cluster-42__dedicated__prod-cluster"].ClusterName; got != "prod-cluster" {
		t.Fatalf("EnvProfiles[prod] mutated, cluster name = %q, want %q", got, "prod-cluster")
	}
}

func TestClusterSearchIDAutoCreateSkipsWithoutExactMatch(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	cfg.ActiveEnv = "staging"
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		ActiveProfile: "green",
		Profiles: map[string]config.ProfileRecord{
			"green": {
				ClusterID: "cluster-green",
			},
		},
	}
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	responseBody := `{"items":[{"clusterID":"branch-cluster-42","orgID":"org-branch","parentID":"parent-branch","clusterDeployTypeV2":"dedicated","name":"wrong-cluster"}]}`
	client := &recordingClinicClient{
		responses: []clinicResponse{{body: []byte(responseBody)}},
	}
	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		Config:      cfg,
		Client:      client,
	}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"search-id",
		"--cluster-id", "cluster-42",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	assertJSONEqual(t, stdout.String(), responseBody)

	loaded := mustLoadConfig(t)
	stagingProfiles := loaded.EnvProfiles["staging"]
	if got := len(stagingProfiles.Profiles); got != 1 {
		t.Fatalf("len(EnvProfiles[staging].Profiles) = %d, want %d", got, 1)
	}
	if stagingProfiles.ActiveProfile != "green" {
		t.Fatalf("EnvProfiles[staging].ActiveProfile = %q, want %q", stagingProfiles.ActiveProfile, "green")
	}
}

func TestClusterSearchIDAutoCreateUsesClusterNameFieldWhenNameIsAbsent(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	cfg.ActiveEnv = "staging"
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		Profiles: map[string]config.ProfileRecord{},
	}
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	responseBody := `{"items":[{"clusterID":"cluster-42","orgID":"org-42","parentID":"parent-42","clusterDeployTypeV2":"premium","clusterName":"Primary Cluster From clusterName"}]}`
	client := &recordingClinicClient{
		responses: []clinicResponse{{body: []byte(responseBody)}},
	}
	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		Config:      cfg,
		Client:      client,
	}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"search-id",
		"--cluster-id", "cluster-42",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	assertJSONEqual(t, stdout.String(), responseBody)

	loaded := mustLoadConfig(t)
	stagingProfiles := loaded.EnvProfiles["staging"]
	profile, ok := stagingProfiles.Profiles["cluster-42__premium__primary-cluster-from-clustername"]
	if !ok {
		t.Fatal("auto-created staging profile using clusterName is missing")
	}
	if profile.ClusterName != "Primary Cluster From clusterName" {
		t.Fatalf("profile.ClusterName = %q, want %q", profile.ClusterName, "Primary Cluster From clusterName")
	}
}

func TestClusterSearchIDAutoCreateAppendsNumericSuffixWhenGeneratedNameExists(t *testing.T) {
	setTestConfigHome(t)

	cfg := mustLoadConfig(t)
	cfg.ActiveEnv = "staging"
	cfg.EnvProfiles["staging"] = config.EnvProfileContainer{
		Profiles: map[string]config.ProfileRecord{
			"cluster-42__dedicated__primary-cluster": {
				ClusterID: "another-cluster",
			},
		},
	}
	if err := cfg.Save(); err != nil {
		t.Fatalf("cfg.Save() error = %v", err)
	}

	responseBody := `{"items":[{"clusterID":"cluster-42","orgID":"org-42","parentID":"parent-42","clusterDeployTypeV2":"dedicated","name":"Primary Cluster"}]}`
	client := &recordingClinicClient{
		responses: []clinicResponse{{body: []byte(responseBody)}},
	}
	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		Config:      cfg,
		Client:      client,
	}

	if err := clusterCommandHandler(ctx, []string{
		"search-id",
		"--cluster-id", "cluster-42",
	}, &strings.Builder{}); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	loaded := mustLoadConfig(t)
	stagingProfiles := loaded.EnvProfiles["staging"]
	if _, ok := stagingProfiles.Profiles["cluster-42__dedicated__primary-cluster__2"]; !ok {
		t.Fatal("expected suffixed auto-created profile name to exist")
	}
}

func TestClusterGetUsesOrgScopedPath(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{responses: []clinicResponse{{body: []byte(`{"clusterID":"c-99"}`)}}}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"get",
		"--org-id", "org-42",
		"--cluster-id", "c-99",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if got, want := client.request.Method, http.MethodGet; got != want {
		t.Fatalf("method = %q, want %q", got, want)
	}
	if got, want := client.request.Path, "/clinic/api/v1/orgs/org-42/clusters/c-99"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}
	if encoded := client.request.Query.Encode(); encoded != "" {
		t.Fatalf("query = %q, want empty", encoded)
	}
}

func TestClusterGetByIDUsesExactClusterIDMatchForOrgResolution(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"items":[{"clusterID":"bran-1024","orgID":"org-branch"},{"clusterID":"1024","orgID":"org-actual"}]}`)},
			{body: []byte(`{"clusterID":"1024","orgID":"org-actual","name":"prod-cluster"}`)},
		},
	}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"get-by-id",
		"--cluster-id", "1024",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if len(client.requests) != 2 {
		t.Fatalf("request count = %d, want 2", len(client.requests))
	}
	assertQueryValue(t, client.requests[0].Query, "cluster_id", "1024")
	if got, want := client.requests[1].Path, "/clinic/api/v1/orgs/org-actual/clusters/1024"; got != want {
		t.Fatalf("detail path = %q, want %q", got, want)
	}
	assertJSONEqual(t, stdout.String(), `{"clusterID":"1024","orgID":"org-actual","name":"prod-cluster"}`)
}

func TestClusterGetByIDUsesSelectedProfileMetadataWithoutRemoteLookup(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"clusterID":"1024","orgID":"org-actual","name":"prod-cluster"}`)},
		},
	}
	ctx := &RuntimeContext{
		Client:      client,
		SelectedEnv: "staging",
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "1024",
			OrgID:               "org-actual",
			ClusterDeployTypeV2: "dedicated",
		},
	}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"get-by-id",
		"--cluster-id", "1024",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if got, want := len(client.requests), 1; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	if got, want := client.requests[0].Path, "/clinic/api/v1/orgs/org-actual/clusters/1024"; got != want {
		t.Fatalf("detail path = %q, want %q", got, want)
	}
	assertJSONEqual(t, stdout.String(), `{"clusterID":"1024","orgID":"org-actual","name":"prod-cluster"}`)
}

func TestClusterGetByIDFallsBackToRemoteWhenSelectedProfileMetadataIncomplete(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"items":[{"clusterID":"branch-1024","orgID":"org-branch"},{"clusterID":"1024","orgID":"org-actual"}]}`)},
			{body: []byte(`{"clusterID":"1024","orgID":"org-actual","name":"prod-cluster"}`)},
		},
	}
	ctx := &RuntimeContext{
		Client:      client,
		SelectedEnv: "staging",
		SelectedProfile: &config.ProfileRecord{
			ClusterID: "1024",
		},
	}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"get-by-id",
		"--cluster-id", "1024",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if got, want := len(client.requests), 2; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	assertQueryValue(t, client.requests[0].Query, "cluster_id", "1024")
	if got, want := client.requests[1].Path, "/clinic/api/v1/orgs/org-actual/clusters/1024"; got != want {
		t.Fatalf("detail path = %q, want %q", got, want)
	}
	assertJSONEqual(t, stdout.String(), `{"clusterID":"1024","orgID":"org-actual","name":"prod-cluster"}`)
}

func TestClusterGetByIDReturnsClearNotFoundWhenExactMatchMissing(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"items":[{"clusterID":"bran-1024","orgID":"org-branch"}]}`)},
		},
	}
	ctx := &RuntimeContext{Client: client}

	err := clusterCommandHandler(ctx, []string{
		"get-by-id",
		"--cluster-id", "1024",
	}, &strings.Builder{})
	if err == nil {
		t.Fatal("clusterCommandHandler() error = nil, want not-found error")
	}
	if got := err.Error(); !strings.Contains(got, "cluster not found: 1024") {
		t.Fatalf("error = %q, want cluster not found message", got)
	}
	if len(client.requests) != 1 {
		t.Fatalf("request count = %d, want 1", len(client.requests))
	}
}

func TestClusterTopologyReturnsDedicatedComponentsPayload(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"items":[{"clusterID":"ded-1","orgID":"org-42","clusterDeployTypeV2":"dedicated"}]}`)},
			{body: []byte(`{"clusterID":"ded-1","components":{"tidb":[{"host":"tidb-0"}],"tikv":[{"host":"tikv-0"}]}}`)},
		},
	}
	ctx := &RuntimeContext{Client: client}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"topology",
		"--cluster-id", "ded-1",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if len(client.requests) != 2 {
		t.Fatalf("request count = %d, want 2", len(client.requests))
	}
	if got, want := client.requests[1].Path, "/clinic/api/v1/orgs/org-42/clusters/ded-1"; got != want {
		t.Fatalf("detail path = %q, want %q", got, want)
	}
	assertJSONEqual(t, stdout.String(), `{"tidb":[{"host":"tidb-0"}],"tikv":[{"host":"tikv-0"}]}`)
}

func TestClusterTopologyRequiresClusterDeployTypeV2(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"items":[{"clusterID":"ded-1","orgID":"org-42"}]}`)},
		},
	}
	ctx := &RuntimeContext{Client: client}

	err := clusterCommandHandler(ctx, []string{
		"topology",
		"--cluster-id", "ded-1",
	}, &strings.Builder{})
	if err == nil {
		t.Fatal("clusterCommandHandler() error = nil, want missing deploy type error")
	}
	if got := err.Error(); !strings.Contains(got, "cluster ded-1 has no deploy type") {
		t.Fatalf("error = %q, want missing deploy type message", got)
	}
	if got, want := len(client.requests), 1; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
}

func TestClusterTopologyUsesParentResourcePoolForPremiumAndBYOCClusters(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name       string
		deployType string
	}{
		{name: "premium", deployType: "premium"},
		{name: "byoc", deployType: "byoc"},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			client := &recordingClinicClient{
				responses: []clinicResponse{
					{body: []byte(`{"items":[{"clusterID":"tenant-1","orgID":"org-42","clusterDeployTypeV2":"` + tc.deployType + `","parentID":"pool-9"}]}`)},
					{body: []byte(`{"components":[{"name":"tidb"}]}`)},
				},
			}
			ctx := &RuntimeContext{Client: client}

			var stdout strings.Builder
			if err := clusterCommandHandler(ctx, []string{
				"topology",
				"--cluster-id", "tenant-1",
			}, &stdout); err != nil {
				t.Fatalf("clusterCommandHandler() error = %v", err)
			}

			if len(client.requests) != 2 {
				t.Fatalf("request count = %d, want 2", len(client.requests))
			}
			if got, want := client.requests[1].Path, "/clinic/api/v1/orgs/org-42/clusters/pool-9/resource-pool/components"; got != want {
				t.Fatalf("resource pool path = %q, want %q", got, want)
			}
			assertJSONEqual(t, stdout.String(), `{"components":[{"name":"tidb"}]}`)
		})
	}
}

func TestClusterTopologyUsesOwnClusterIDForResourcePoolDeployTypes(t *testing.T) {
	t.Parallel()

	testCases := []struct {
		name       string
		deployType string
	}{
		{name: "premium resource pool", deployType: "premium resource pool"},
		{name: "essential v2 resource pool", deployType: "essential v2 resource pool"},
		{name: "resource_pool", deployType: "resource_pool"},
		{name: "byoc resource pool", deployType: "byoc resource pool"},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			client := &recordingClinicClient{
				responses: []clinicResponse{
					{body: []byte(`{"items":[{"clusterID":"pool-1","orgID":"org-42","clusterDeployTypeV2":"` + tc.deployType + `"}]}`)},
					{body: []byte(`[{"component_type":"TIFLASH_COMPUTE","node_count":1}]`)},
				},
			}
			ctx := &RuntimeContext{Client: client}

			var stdout strings.Builder
			if err := clusterCommandHandler(ctx, []string{
				"topology",
				"--cluster-id", "pool-1",
			}, &stdout); err != nil {
				t.Fatalf("clusterCommandHandler() error = %v", err)
			}

			if len(client.requests) != 2 {
				t.Fatalf("request count = %d, want 2", len(client.requests))
			}
			if got, want := client.requests[1].Path, "/clinic/api/v1/orgs/org-42/clusters/pool-1/resource-pool/components"; got != want {
				t.Fatalf("resource pool path = %q, want %q", got, want)
			}
			assertJSONEqual(t, stdout.String(), `[{"component_type":"TIFLASH_COMPUTE","node_count":1}]`)
		})
	}
}

func TestNormalizeDeployType(t *testing.T) {
	t.Parallel()

	cases := []struct {
		in   string
		want string
	}{
		{in: "premium resource pool", want: "premium_resource_pool"},
		{in: "Essential V2 Resource Pool", want: "essential_v2_resource_pool"},
		{in: "byoc-resource-pool", want: "byoc_resource_pool"},
		{in: "  premium  ", want: "premium"},
		{in: "", want: ""},
	}
	for _, tc := range cases {
		if got := normalizeDeployType(tc.in); got != tc.want {
			t.Fatalf("normalizeDeployType(%q) = %q, want %q", tc.in, got, tc.want)
		}
	}
}

func TestProfileRecordHasMetadataAllowsResourcePoolWithoutParentID(t *testing.T) {
	t.Parallel()

	if !profileRecordHasMetadata(config.ProfileRecord{
		ClusterID:           "pool-1",
		OrgID:               "org-42",
		ClusterDeployTypeV2: "premium resource pool",
	}) {
		t.Fatal("premium resource pool without parentID should have metadata")
	}
	if profileRecordHasMetadata(config.ProfileRecord{
		ClusterID:           "tenant-1",
		OrgID:               "org-42",
		ClusterDeployTypeV2: "premium",
	}) {
		t.Fatal("premium tenant without parentID should not have metadata")
	}
}

func TestClusterTopologyUsesSelectedProfileMetadataWithoutRemoteLookup(t *testing.T) {
	t.Parallel()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
		},
	}
	ctx := &RuntimeContext{
		Client:      client,
		SelectedEnv: "staging",
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "ded-1",
			OrgID:               "org-42",
			ClusterDeployTypeV2: "dedicated",
		},
	}

	var stdout strings.Builder
	if err := clusterCommandHandler(ctx, []string{
		"topology",
		"--cluster-id", "ded-1",
	}, &stdout); err != nil {
		t.Fatalf("clusterCommandHandler() error = %v", err)
	}

	if got, want := len(client.requests), 1; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	if got, want := client.requests[0].Path, "/clinic/api/v1/orgs/org-42/clusters/ded-1"; got != want {
		t.Fatalf("detail path = %q, want %q", got, want)
	}
	assertJSONEqual(t, stdout.String(), `{"tidb":[{"host":"tidb-0"}]}`)
}

type recordingClinicClient struct {
	request   clinic.Request
	requests  []clinic.Request
	responses []clinicResponse
	err       error
}

type clinicResponse struct {
	body []byte
	err  error
}

func (c *recordingClinicClient) Do(_ context.Context, req clinic.Request, out any) error {
	c.request = req
	c.requests = append(c.requests, req)
	if c.err != nil {
		return c.err
	}

	var response clinicResponse
	if len(c.responses) > 0 {
		response = c.responses[0]
		c.responses = c.responses[1:]
	}
	if response.err != nil {
		return response.err
	}

	raw, ok := out.(*[]byte)
	if ok {
		*raw = append((*raw)[:0], response.body...)
		return nil
	}

	if len(response.body) == 0 {
		return nil
	}

	if err := json.Unmarshal(response.body, out); err != nil {
		return err
	}
	return nil
}

func assertQueryValue(t *testing.T, values url.Values, key, want string) {
	t.Helper()
	if got := values.Get(key); got != want {
		t.Fatalf("query[%q] = %q, want %q", key, got, want)
	}
}

func assertJSONEqual(t *testing.T, got, want string) {
	t.Helper()

	var gotValue any
	if err := json.Unmarshal([]byte(got), &gotValue); err != nil {
		t.Fatalf("json.Unmarshal(got) error = %v\njson=%s", err, got)
	}

	var wantValue any
	if err := json.Unmarshal([]byte(want), &wantValue); err != nil {
		t.Fatalf("json.Unmarshal(want) error = %v\njson=%s", err, want)
	}

	gotJSON, err := json.Marshal(gotValue)
	if err != nil {
		t.Fatalf("json.Marshal(got) error = %v", err)
	}
	wantJSON, err := json.Marshal(wantValue)
	if err != nil {
		t.Fatalf("json.Marshal(want) error = %v", err)
	}

	if string(gotJSON) != string(wantJSON) {
		t.Fatalf("json = %s, want %s", gotJSON, wantJSON)
	}
}
