package cmd

import (
	"errors"
	"net/http"
	"strings"
	"testing"
	"time"

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

func TestDiagnoseSourceDedicatedTopologyRequestAndObject(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"clusterID":"ded-1","components":{"tidb":[{"host":"tidb-0"}],"tikv":[{"host":"tikv-0"}]},"topology":{"ignored":true}}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)
	ctx.SelectedProfile = &config.ProfileRecord{
		ClusterID:           "ded-1",
		OrgID:               "org-42",
		ClusterDeployTypeV2: "dedicated",
	}

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseOptions{
		Seed:      "seed",
		ClusterID: "ded-1",
		Start:     "1712123000",
		End:       "1712123600",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))

	if got, want := len(client.requests), 3; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	if got, want := client.requests[1].Method, http.MethodGet; got != want {
		t.Fatalf("topology method = %q, want %q", got, want)
	}
	if got, want := client.requests[1].Path, "/clinic/api/v1/orgs/org-42/clusters/ded-1"; got != want {
		t.Fatalf("topology path = %q, want %q", got, want)
	}

	topology := mapValue(t, bundle, "topology")
	queryIDs := stringSliceValue(t, topology, "query_ids")
	if got, want := strings.Join(queryIDs, ","), "q2"; got != want {
		t.Fatalf("topology.query_ids = %q, want %q", got, want)
	}
	payload := mapValue(t, topology, "payload")
	if _, ok := payload["tidb"]; !ok {
		t.Fatalf("topology payload = %s, want dedicated components object", mustMarshalForTest(t, payload))
	}
	if _, ok := payload["ignored"]; ok {
		t.Fatalf("topology payload used topology fallback despite components: %s", mustMarshalForTest(t, payload))
	}

	record := queryPlanRecordByPurposeForTest(t, bundle, "topology")
	if got, want := stringValue(t, record, "id"), "q2"; got != want {
		t.Fatalf("topology query id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, record, "status"), "success"; got != want {
		t.Fatalf("topology query status = %q, want %q", got, want)
	}
}

func TestDiagnoseSourcePremiumTopologyUsesResourcePoolRequest(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"components":[{"name":"tidb"}]}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)
	ctx.SelectedProfile = &config.ProfileRecord{
		ClusterID:           "tenant-1",
		OrgID:               "org-42",
		ParentID:            "pool-9",
		ClusterDeployTypeV2: "premium",
	}

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseOptions{
		Seed:      "seed",
		ClusterID: "tenant-1",
		Start:     "1712123000",
		End:       "1712123600",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))

	if got, want := len(client.requests), 3; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	if got, want := client.requests[1].Path, "/clinic/api/v1/orgs/org-42/clusters/pool-9/resource-pool/components"; got != want {
		t.Fatalf("topology path = %q, want %q", got, want)
	}
	topology := mapValue(t, bundle, "topology")
	payload := mapValue(t, topology, "payload")
	components := sliceValue(t, payload, "components")
	if got, want := len(components), 1; got != want {
		t.Fatalf("components length = %d, want %d", got, want)
	}
}

func TestDiagnoseSourceTopologyFailureReturnsWarningAndFailedQueryPlan(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{err: errors.New("topology unavailable")},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)
	ctx.SelectedProfile = &config.ProfileRecord{
		ClusterID:           "ded-1",
		OrgID:               "org-42",
		ClusterDeployTypeV2: "dedicated",
	}

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseOptions{
		Seed:      "seed",
		ClusterID: "ded-1",
		Start:     "1712123000",
		End:       "1712123600",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))

	record := queryPlanRecordByPurposeForTest(t, bundle, "topology")
	if got, want := stringValue(t, record, "status"), "failed"; got != want {
		t.Fatalf("topology query status = %q, want %q", got, want)
	}
	warnings := sliceValue(t, bundle, "warnings")
	if got, want := len(warnings), 1; got != want {
		t.Fatalf("warnings length = %d, want %d", got, want)
	}
	warning := mapValueFromAny(t, warnings[0], "warnings[0]")
	if got, want := stringValue(t, warning, "code"), "missing_source"; got != want {
		t.Fatalf("warning code = %q, want %q", got, want)
	}
	if got, want := stringValue(t, warning, "source"), "topology"; got != want {
		t.Fatalf("warning source = %q, want %q", got, want)
	}
}

func TestDiagnoseSourceEventsRequestParamsAndProvenance(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[{"id":"evt-1","name":"Scale Out","type":"scale","severity":2,"createdAt":"2024-04-03T05:45:00Z","message":"added tidb"}]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)
	ctx.SelectedProfile = &config.ProfileRecord{
		ClusterID:           "ded-1",
		OrgID:               "org-42",
		ClusterDeployTypeV2: "dedicated",
	}

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseOptions{
		Seed:      "seed",
		ClusterID: "ded-1",
		Start:     "1712123000",
		End:       "1712123600",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))

	if got, want := client.requests[2].Path, "/clinic/api/v1/activityhub/applications/org-42/targets/ded-1/activities"; got != want {
		t.Fatalf("events path = %q, want %q", got, want)
	}
	assertQueryValue(t, client.requests[2].Query, "begin_ts", "1712123000")
	assertQueryValue(t, client.requests[2].Query, "end_ts", "1712123600")
	assertQueryValue(t, client.requests[2].Query, "limit", "200")

	events := sliceValue(t, bundle, "events")
	if got, want := len(events), 1; got != want {
		t.Fatalf("events length = %d, want %d", got, want)
	}
	event := mapValueFromAny(t, events[0], "events[0]")
	if got, want := stringValue(t, event, "id"), "evt-1"; got != want {
		t.Fatalf("event id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, event, "name"), "Scale Out"; got != want {
		t.Fatalf("event name = %q, want %q", got, want)
	}
	if got, want := intValue(t, event, "severity"), 2; got != want {
		t.Fatalf("event severity = %d, want %d", got, want)
	}
	if got, want := strings.Join(stringSliceValue(t, event, "query_ids"), ","), "q3"; got != want {
		t.Fatalf("event query_ids = %q, want %q", got, want)
	}
	if got, want := boolValue(t, event, "payload_truncated"), false; got != want {
		t.Fatalf("event payload_truncated = %t, want %t", got, want)
	}
}

func TestDiagnoseSourceEventPayloadTruncation(t *testing.T) {
	large := strings.Repeat("x", 21000)
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[{"id":"evt-big","message":"` + large + `"}]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)
	ctx.SelectedProfile = &config.ProfileRecord{
		ClusterID:           "ded-1",
		OrgID:               "org-42",
		ClusterDeployTypeV2: "dedicated",
	}

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseOptions{
		Seed:      "seed",
		ClusterID: "ded-1",
		Start:     "1712123000",
		End:       "1712123600",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))

	event := mapValueFromAny(t, sliceValue(t, bundle, "events")[0], "events[0]")
	if got, want := boolValue(t, event, "payload_truncated"), true; got != want {
		t.Fatalf("event payload_truncated = %t, want %t", got, want)
	}
	rawPayload := stringValue(t, event, "raw_payload")
	if got, wantMax := len(rawPayload), 20000; got > wantMax {
		t.Fatalf("raw_payload length = %d, want <= %d", got, wantMax)
	}
}

func queryPlanRecordByPurposeForTest(t *testing.T, bundle map[string]any, purpose string) map[string]any {
	t.Helper()

	for i, value := range sliceValue(t, bundle, "query_plan") {
		record := mapValueFromAny(t, value, "query_plan record")
		if stringValue(t, record, "purpose") == purpose {
			return record
		}
		_ = i
	}
	t.Fatalf("query_plan has no record with purpose %q: %s", purpose, mustMarshalForTest(t, bundle["query_plan"]))
	return nil
}

func stringSliceValue(t *testing.T, values map[string]any, key string) []string {
	t.Helper()

	raw := sliceValue(t, values, key)
	out := make([]string, 0, len(raw))
	for i, value := range raw {
		text, ok := value.(string)
		if !ok {
			t.Fatalf("%s[%d] = %T, want string", key, i, value)
		}
		out = append(out, text)
	}
	return out
}
