package cmd

import (
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
	"testing"
	"time"

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

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

	err := diagnoseCommandHandler(nil, nil, &strings.Builder{})
	if err == nil {
		t.Fatal("diagnoseCommandHandler() error = nil, want missing seed error")
	}
	if got, want := err.Error(), "missing required seed"; got != want {
		t.Fatalf("diagnoseCommandHandler() error = %q, want %q", got, want)
	}
}

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

	err := diagnoseCommandHandler(nil, []string{"   "}, &strings.Builder{})
	if err == nil {
		t.Fatal("diagnoseCommandHandler() error = nil, want missing seed error")
	}
	if got, want := err.Error(), "missing required seed"; got != want {
		t.Fatalf("diagnoseCommandHandler() error = %q, want %q", got, want)
	}
}

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

	err := diagnoseCommandHandler(nil, []string{"seed", "extra"}, &strings.Builder{})
	if err == nil {
		t.Fatal("diagnoseCommandHandler() error = nil, want unexpected args error")
	}
	if got, want := err.Error(), "unexpected arguments: [extra]"; got != want {
		t.Fatalf("diagnoseCommandHandler() error = %q, want %q", got, want)
	}
}

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

	err := diagnoseCommandHandler(nil, []string{"seed", "--output", "bogus"}, &strings.Builder{})
	if err == nil {
		t.Fatal("diagnoseCommandHandler() error = nil, want invalid output error")
	}
	if got, want := err.Error(), "output must be one of: table, json"; got != want {
		t.Fatalf("diagnoseCommandHandler() error = %q, want %q", got, want)
	}
}

func TestDiagnoseBuildsSeedLokiQueryWithDefaultSince(t *testing.T) {
	clearDiagnoseRangeEnv(t)

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

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

	if got, want := len(client.requests), 3; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	seedRequest := client.requests[0]
	if got, want := seedRequest.Method, http.MethodGet; got != want {
		t.Fatalf("method = %q, want %q", got, want)
	}
	if got, want := seedRequest.Path, "/data-proxy/loki/cluster-42/api/v1/query_range"; got != want {
		t.Fatalf("path = %q, want %q", got, want)
	}

	wantQuery := `{cluster_env="staging", namespace="tidbcluster-42"} |= "Request range exceeds bound"`
	assertQueryValue(t, seedRequest.Query, "query", wantQuery)
	assertQueryValue(t, seedRequest.Query, "limit", "100")

	startUnix, err := strconv.ParseInt(seedRequest.Query.Get("start"), 10, 64)
	if err != nil {
		t.Fatalf("start query value = %q, want unix nanoseconds: %v", seedRequest.Query.Get("start"), err)
	}
	endUnix, err := strconv.ParseInt(seedRequest.Query.Get("end"), 10, 64)
	if err != nil {
		t.Fatalf("end query value = %q, want unix nanoseconds: %v", seedRequest.Query.Get("end"), err)
	}
	if got, want := endUnix-startUnix, int64(time.Hour); got != want {
		t.Fatalf("range length = %d, want %d", got, want)
	}
}

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

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := diagnoseCommandHandler(ctx, []string{
		"seed",
		"--cluster-id", "cluster-42",
		"--start", "1712123000",
		"--end", "1712123600",
		"--limit", "17",
	}, &stdout); err != nil {
		t.Fatalf("diagnoseCommandHandler() error = %v", err)
	}

	if got, want := len(client.requests), 3; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	seedRequest := client.requests[0]
	assertQueryValue(t, seedRequest.Query, "query", `{cluster_env="staging", namespace="tidbcluster-42"} |= "seed"`)
	assertQueryValue(t, seedRequest.Query, "start", "1712123000000000000")
	assertQueryValue(t, seedRequest.Query, "end", "1712123600000000000")
	assertQueryValue(t, seedRequest.Query, "limit", "17")
}

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

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	seed := "help \"quote\"\nnext line"
	var stdout strings.Builder
	if err := diagnoseCommandHandler(ctx, []string{
		seed,
		"--cluster-id", "cluster-42",
		"--since", "1h",
	}, &stdout); err != nil {
		t.Fatalf("diagnoseCommandHandler() error = %v", err)
	}

	wantQuery := `{cluster_env="staging", namespace="tidbcluster-42"} |= ` + strconv.Quote(seed)
	assertQueryValue(t, client.requests[0].Query, "query", wantQuery)
}

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

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

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

	assertQueryValue(t, client.requests[0].Query, "query", `{cluster_env="staging", namespace="tidbcluster-42"} |= "help"`)
}

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

	t.Run("defaults to 100", func(t *testing.T) {
		t.Parallel()

		client := &recordingClinicClient{
			responses: []clinicResponse{
				{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			},
		}
		ctx := newDiagnoseTestRuntimeContext(client)

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

		assertQueryValue(t, client.requests[0].Query, "limit", "100")
	})

	t.Run("rejects non-positive limit", func(t *testing.T) {
		t.Parallel()

		ctx := newDiagnoseTestRuntimeContext(&recordingClinicClient{})
		err := diagnoseCommandHandler(ctx, []string{"seed", "--cluster-id", "cluster-42", "--since", "1h", "--limit", "0"}, &strings.Builder{})
		if err == nil {
			t.Fatal("diagnoseCommandHandler() error = nil, want validation error")
		}
		if got, want := err.Error(), "limit must be greater than 0"; got != want {
			t.Fatalf("error = %q, want %q", got, want)
		}
	})

	t.Run("rejects limit above max", func(t *testing.T) {
		t.Parallel()

		ctx := newDiagnoseTestRuntimeContext(&recordingClinicClient{})
		err := diagnoseCommandHandler(ctx, []string{"seed", "--cluster-id", "cluster-42", "--since", "1h", "--limit", "1001"}, &strings.Builder{})
		if err == nil {
			t.Fatal("diagnoseCommandHandler() error = nil, want validation error")
		}
		if got, want := err.Error(), "limit must be less than or equal to 1000"; got != want {
			t.Fatalf("error = %q, want %q", got, want)
		}
	})
}

func TestDiagnoseSinceOverridesEnvAndProfileRange(t *testing.T) {
	now := time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC)
	t.Setenv(envClinicRangeStart, "1712000000")
	t.Setenv(envClinicRangeEnd, "1712003600")

	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "cluster-42",
			OrgID:               "org-1",
			ClusterDeployTypeV2: "dedicated",
			RangeStart:          "1711990000",
			RangeEnd:            "1711993600",
		},
	}

	start, end, err := resolveDiagnoseTimeScope(
		ctx,
		diagnoseOptions{
			Since: "24h",
		},
		now,
	)
	if err != nil {
		t.Fatalf("resolveDiagnoseTimeScope() error = %v", err)
	}

	if got, want := start, now.Add(-24*time.Hour); !got.Equal(want) {
		t.Fatalf("start = %s, want %s", got, want)
	}
	if got, want := end, now; !got.Equal(want) {
		t.Fatalf("end = %s, want %s", got, want)
	}
}

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

	around := "2026-04-03T10:05:00Z"
	start, end, err := resolveDiagnoseTimeScope(
		newDiagnoseTestRuntimeContext(&recordingClinicClient{}),
		diagnoseOptions{
			Around: around,
			Window: "10m",
		},
		time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC),
	)
	if err != nil {
		t.Fatalf("resolveDiagnoseTimeScope() error = %v", err)
	}

	wantStart := time.Date(2026, time.April, 3, 10, 0, 0, 0, time.UTC)
	wantEnd := time.Date(2026, time.April, 3, 10, 10, 0, 0, time.UTC)
	if got, want := start, wantStart; !got.Equal(want) {
		t.Fatalf("start = %s, want %s", got, want)
	}
	if got, want := end, wantEnd; !got.Equal(want) {
		t.Fatalf("end = %s, want %s", got, want)
	}
}

func TestDiagnoseTimeScopeUsesEnvThenProfileFallback(t *testing.T) {
	t.Setenv(envClinicRangeStart, "1712123000")
	t.Setenv(envClinicRangeEnd, "1712123600")

	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "cluster-42",
			OrgID:               "org-1",
			ClusterDeployTypeV2: "dedicated",
			RangeStart:          "1712000000",
			RangeEnd:            "1712003600",
		},
	}

	start, end, err := resolveDiagnoseTimeScope(ctx, diagnoseOptions{}, time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("resolveDiagnoseTimeScope() error = %v", err)
	}

	if got, want := start.Unix(), int64(1712123000); got != want {
		t.Fatalf("start unix = %d, want %d", got, want)
	}
	if got, want := end.Unix(), int64(1712123600); got != want {
		t.Fatalf("end unix = %d, want %d", got, want)
	}
}

func TestDiagnoseTimeScopeUsesProfileFallbackWhenEnvMissing(t *testing.T) {
	clearDiagnoseRangeEnv(t)

	ctx := &RuntimeContext{
		SelectedEnv: "staging",
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "cluster-42",
			OrgID:               "org-1",
			ClusterDeployTypeV2: "dedicated",
			RangeStart:          "1712000000",
			RangeEnd:            "1712003600",
		},
	}

	start, end, err := resolveDiagnoseTimeScope(ctx, diagnoseOptions{}, time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC))
	if err != nil {
		t.Fatalf("resolveDiagnoseTimeScope() error = %v", err)
	}

	if got, want := start.Unix(), int64(1712000000); got != want {
		t.Fatalf("start unix = %d, want %d", got, want)
	}
	if got, want := end.Unix(), int64(1712003600); got != want {
		t.Fatalf("end unix = %d, want %d", got, want)
	}
}

func TestDiagnoseDefaultsToSinceOneHourWhenNoRangeResolves(t *testing.T) {
	clearDiagnoseRangeEnv(t)

	now := time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC)
	start, end, err := resolveDiagnoseTimeScope(newDiagnoseTestRuntimeContext(&recordingClinicClient{}), diagnoseOptions{}, now)
	if err != nil {
		t.Fatalf("resolveDiagnoseTimeScope() error = %v", err)
	}

	if got, want := start, now.Add(-time.Hour); !got.Equal(want) {
		t.Fatalf("start = %s, want %s", got, want)
	}
	if got, want := end, now; !got.Equal(want) {
		t.Fatalf("end = %s, want %s", got, want)
	}
}

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

	now := time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC)

	testCases := []struct {
		name string
		opts diagnoseOptions
	}{
		{
			name: "since with start",
			opts: diagnoseOptions{
				Start: "1712123000",
				Since: "1h",
			},
		},
		{
			name: "since with around",
			opts: diagnoseOptions{
				Around: "2026-04-03T10:05:00Z",
				Since:  "1h",
			},
		},
		{
			name: "around with start",
			opts: diagnoseOptions{
				Start:  "1712123000",
				Around: "2026-04-03T10:05:00Z",
				Window: "10m",
			},
		},
	}

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

			_, _, err := resolveDiagnoseTimeScope(newDiagnoseTestRuntimeContext(&recordingClinicClient{}), tc.opts, now)
			if err == nil {
				t.Fatal("resolveDiagnoseTimeScope() error = nil, want conflict error")
			}
		})
	}
}

func TestDiagnoseTimeRejectsPartialModes(t *testing.T) {
	clearDiagnoseRangeEnv(t)

	now := time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC)

	testCases := []struct {
		name string
		opts diagnoseOptions
	}{
		{
			name: "around without window",
			opts: diagnoseOptions{
				Around: "2026-04-03T10:05:00Z",
			},
		},
		{
			name: "window without around",
			opts: diagnoseOptions{
				Window: "10m",
			},
		},
		{
			name: "start without end",
			opts: diagnoseOptions{
				Start: "1712123000",
			},
		},
		{
			name: "end without start",
			opts: diagnoseOptions{
				End: "1712123600",
			},
		},
	}

	for _, tc := range testCases {
		tc := tc
		t.Run(tc.name, func(t *testing.T) {
			_, _, err := resolveDiagnoseTimeScope(newDiagnoseTestRuntimeContext(&recordingClinicClient{}), tc.opts, now)
			if err == nil {
				t.Fatal("resolveDiagnoseTimeScope() error = nil, want partial-mode error")
			}
		})
	}
}

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

	testCases := []struct {
		name string
		opts diagnoseOptions
	}{
		{
			name: "zero width",
			opts: diagnoseOptions{
				Start: "1712123000",
				End:   "1712123000",
			},
		},
		{
			name: "inverted",
			opts: diagnoseOptions{
				Start: "1712123600",
				End:   "1712123000",
			},
		},
	}

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

			_, _, err := resolveDiagnoseTimeScope(
				newDiagnoseTestRuntimeContext(&recordingClinicClient{}),
				tc.opts,
				time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC),
			)
			if err == nil {
				t.Fatal("resolveDiagnoseTimeScope() error = nil, want non-increasing range error")
			}
			if got, want := err.Error(), "start must be before end"; got != want {
				t.Fatalf("error = %q, want %q", got, want)
			}
		})
	}
}

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

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{err: &clinic.HTTPError{StatusCode: http.StatusGatewayTimeout, Status: "504 Gateway Timeout"}},
			{err: &clinic.HTTPError{StatusCode: http.StatusGatewayTimeout, Status: "504 Gateway Timeout"}},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

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

	if got, want := len(client.requests), 5; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
}

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

	var stdout strings.Builder
	cmd := newDiagnoseCommand(nil, &stdout)
	cmd.SetArgs([]string{"--help"})
	if err := cmd.Execute(); err != nil {
		t.Fatalf("diagnose help error = %v", err)
	}

	assertContainsAll(t, stdout.String(), []string{
		"Usage:",
		"diagnose <seed>",
		"Correlate evidence",
		"does not infer root cause",
		"diagnose.v2",
		"--cluster-id string",
		"--since string",
		"--around string",
		"--window string",
		"--no-expand",
		"--max-seed-expand",
		"--max-related-logs",
		"-o, --output string",
		"Supported: table, json",
		"json emits the full diagnose.v2 evidence bundle",
		"now-<duration>",
		"Examples:",
	})
}

func TestDiagnoseNormalizesLokiStreamsIntoEvidence(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123000000000000","[conn=281072972] request range exceeds bound\r\n"],
						["1712123000000000001"]
					]
				}
			]
		}
		}
		`)

	evidence, err := normalizeDiagnoseEvidence(body)
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidence() error = %v", err)
	}
	if got, want := len(evidence), 1; got != want {
		t.Fatalf("evidence count = %d, want %d", got, want)
	}

	got := evidence[0]
	if got.ID != "E1" {
		t.Fatalf("evidence ID = %q, want %q", got.ID, "E1")
	}
	if want := time.Unix(0, 1712123000000000000).UTC(); !got.Time.Equal(want) {
		t.Fatalf("time = %s, want %s", got.Time, want)
	}
	if got, want := got.Line, "[conn=281072972] request range exceeds bound"; got != want {
		t.Fatalf("line = %q, want %q", got, want)
	}
	if got, want := got.Component, "tidb"; got != want {
		t.Fatalf("component = %q, want %q", got, want)
	}
	if got, want := got.Instance, "10.0.0.1:4000"; got != want {
		t.Fatalf("instance = %q, want %q", got, want)
	}
	if got, want := got.Source.Stream["namespace"], "tidbcluster-42"; got != want {
		t.Fatalf("stream namespace = %q, want %q", got, want)
	}
}

func TestDiagnoseRejectsMalformedLokiJSON(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte("{")},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	err := diagnoseCommandHandler(ctx, []string{"seed", "--cluster-id", "cluster-42", "--since", "1h"}, &strings.Builder{})
	if err == nil {
		t.Fatal("diagnoseCommandHandler() error = nil, want decode error")
	}
	if got := err.Error(); !strings.Contains(got, "decode loki response") {
		t.Fatalf("error = %q, want decode loki response", got)
	}
}

func TestDiagnoseTableRenderIncludesBundleScopeAndGroupCounts(t *testing.T) {
	setDiagnoseTestLocalTimeZone(t, time.FixedZone("Asia/Shanghai", 8*60*60))

	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"cluster_id":"unknown","component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123099000000000","Request range exceeds bound"]
					]
				}
			]
		}
		}
		`)

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: body},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		NoExpand:  true,
		Output:    "table",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	got := stdout.String()
	assertContainsAll(t, got, []string{
		`diagnose.v2 bundle for "Request range exceeds bound"`,
		"searched cluster=cluster-42 namespace=tidbcluster-42",
		"Profile timezone: Asia/Shanghai",
		"Query window local: 2026-04-20T22:44:59+08:00 -> 2026-04-21T00:44:59+08:00",
		"Query window UTC:   2026-04-20T14:44:59Z -> 2026-04-20T16:44:59Z",
		"seed matches=1 related logs=0",
		"warnings=2 types=expansion_skipped,parse_failure",
		"groups=1",
		"shared_component",
		"keyspace=-",
		"cluster=-",
		"components=tidb",
		"seed=1",
		"related=0",
		"events=0",
		"first=2024-04-03T05:44:59Z",
		"last=2024-04-03T05:44:59Z",
		"rerun with -o json to inspect the full diagnose.v2 evidence bundle",
	})
}

func TestDiagnoseCommandWritesTableOutput(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":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := diagnoseCommandHandler(ctx, []string{
		"Request range exceeds bound",
		"--cluster-id", "cluster-42",
		"--since", "2h",
	}, &stdout); err != nil {
		t.Fatalf("diagnoseCommandHandler() error = %v", err)
	}

	assertContainsAll(t, stdout.String(), []string{
		`diagnose.v2 bundle for "Request range exceeds bound"`,
		"seed matches=0 related logs=0",
		"groups=0",
	})
}

func TestDiagnoseTableOutputIncludesProfileTimezoneAndQueryWindows(t *testing.T) {
	setDiagnoseTestLocalTimeZone(t, time.FixedZone("Asia/Shanghai", 8*60*60))

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "table",
	}, time.Date(2026, time.April, 20, 16, 30, 0, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	assertContainsAll(t, stdout.String(), []string{
		"searched cluster=cluster-42 namespace=tidbcluster-42",
		"Profile timezone: Asia/Shanghai",
		"Query window local: 2026-04-20T22:30:00+08:00 -> 2026-04-21T00:30:00+08:00",
		"Query window UTC:   2026-04-20T14:30:00Z -> 2026-04-20T16:30:00Z",
	})
}

func TestDiagnoseTableDoesNotPrintRawLogLines(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123099000000000","[conn=281072972] [sql=select * from test.t where id=1] Request range exceeds bound"]
					]
				}
			]
		}
		}
		`)

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: body},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "table",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	if strings.Contains(stdout.String(), "[conn=281072972] [sql=select * from test.t where id=1] Request range exceeds bound") {
		t.Fatalf("table output contains raw log line:\n%s", stdout.String())
	}
}

func TestDiagnoseRenderJSONIncludesFullBundle(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"cluster_id":"unknown","component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123099000000000","Request range exceeds bound"]
					]
				}
			]
		}
		}
		`)

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: body},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		NoExpand:  true,
		Output:    "json",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	var result map[string]any
	decoder := json.NewDecoder(strings.NewReader(stdout.String()))
	decoder.UseNumber()
	if err := decoder.Decode(&result); err != nil {
		t.Fatalf("json.Unmarshal() error = %v\njson=%s", err, stdout.String())
	}

	if got, want := stringValue(t, result, "schema_version"), "diagnose.v2"; got != want {
		t.Fatalf("schema_version = %q, want %q", got, want)
	}
	for _, key := range []string{
		"scope",
		"query_plan",
		"seed_matches",
		"related_logs",
		"events",
		"topology",
		"groups",
		"timelines",
		"summaries",
		"warnings",
	} {
		if _, ok := result[key]; !ok {
			t.Fatalf("missing key %q in diagnose.v2 bundle: %s", key, stdout.String())
		}
	}
}

func TestDiagnoseJSONIncludesV2Scope(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123099000000000","[conn=281072972] [sql=select * from test.t where id=1] Request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tikv","instance":"10.0.0.2:20160","namespace":"tidbcluster-42"},
					"values":[
						["1712123102000000000","[regionID=6500] Request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4001","namespace":"tidbcluster-42"},
					"values":[
						["1712123103000000000","[conn=281072972] [regionID=6500] Request range exceeds bound"]
					]
				}
			]
		}
		}
		`)

	client := &recordingClinicClient{
		responses: []clinicResponse{{body: body}},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		NoExpand:  true,
		Output:    "json",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	var result map[string]any
	decoder := json.NewDecoder(strings.NewReader(stdout.String()))
	decoder.UseNumber()
	if err := decoder.Decode(&result); err != nil {
		t.Fatalf("json.Unmarshal() error = %v\njson=%s", err, stdout.String())
	}
	scope := mapValue(t, result, "scope")
	if got, want := stringValue(t, scope, "cluster_id"), "cluster-42"; got != want {
		t.Fatalf("scope.cluster_id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, scope, "seed"), "Request range exceeds bound"; got != want {
		t.Fatalf("scope.seed = %q, want %q", got, want)
	}
	if got, want := stringValue(t, scope, "start_utc"), "2026-04-20T14:44:59Z"; got != want {
		t.Fatalf("scope.start_utc = %q, want %q", got, want)
	}
	if got, want := stringValue(t, scope, "end_utc"), "2026-04-20T16:44:59Z"; got != want {
		t.Fatalf("scope.end_utc = %q, want %q", got, want)
	}
	plan := sliceValue(t, result, "query_plan")
	if got, want := len(plan), 3; got != want {
		t.Fatalf("query_plan length = %d, want %d", got, want)
	}
	record := mapValueFromAny(t, plan[0], "query_plan[0]")
	if got, want := intValue(t, record, "limit"), 100; got != want {
		t.Fatalf("query_plan[0].limit = %d, want %d", got, want)
	}
	if got, want := stringValue(t, record, "query"), `{cluster_env="staging", namespace="tidbcluster-42"} |= "Request range exceeds bound"`; got != want {
		t.Fatalf("query_plan[0].query = %q, want %q", got, want)
	}
}

func TestDiagnoseJSONIncludesV2TimezoneAndLocalWindows(t *testing.T) {
	setDiagnoseTestLocalTimeZone(t, time.FixedZone("Asia/Shanghai", 8*60*60))

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 20, 16, 30, 0, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	var result map[string]any
	decoder := json.NewDecoder(strings.NewReader(stdout.String()))
	decoder.UseNumber()
	if err := decoder.Decode(&result); err != nil {
		t.Fatalf("json.Unmarshal() error = %v\njson=%s", err, stdout.String())
	}
	scope := mapValue(t, result, "scope")
	if got, want := stringValue(t, scope, "timezone"), "Asia/Shanghai"; got != want {
		t.Fatalf("scope.timezone = %q, want %q", got, want)
	}
	if got, want := stringValue(t, scope, "start_local"), "2026-04-20T22:30:00+08:00"; got != want {
		t.Fatalf("scope.start_local = %q, want %q", got, want)
	}
	if got, want := stringValue(t, scope, "end_local"), "2026-04-21T00:30:00+08:00"; got != want {
		t.Fatalf("scope.end_local = %q, want %q", got, want)
	}
}

func TestDiagnoseJSONNoResultsUsesEmptyArrays(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":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	var raw map[string]json.RawMessage
	if err := json.Unmarshal([]byte(stdout.String()), &raw); err != nil {
		t.Fatalf("json.Unmarshal() error = %v\njson=%s", err, stdout.String())
	}
	if _, ok := raw["incidents"]; ok {
		t.Fatalf("incidents key exists in V2 JSON\njson=%s", stdout.String())
	}
	for _, key := range []string{"seed_matches", "related_logs", "events", "groups", "timelines", "warnings"} {
		if got, want := strings.TrimSpace(string(raw[key])), "[]"; got != want {
			t.Fatalf("%s json = %s, want %s\njson=%s", key, got, want, stdout.String())
		}
	}
}

func TestDiagnoseJSONV2SkeletonCountsSeedResultsInQueryPlan(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123099000000000","[conn=281072972] [sql=select * from test.t where id=1] Request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tikv","instance":"10.0.0.2:20160","namespace":"tidbcluster-42"},
					"values":[
						["1712123102000000000","[regionID=6500] Request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4001","namespace":"tidbcluster-42"},
					"values":[
						["1712123103000000000","[conn=281072972] [regionID=6500] Request range exceeds bound"]
					]
				}
			]
		}
		}
		`)

	client := &recordingClinicClient{
		responses: []clinicResponse{{body: body}},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	var result map[string]any
	decoder := json.NewDecoder(strings.NewReader(stdout.String()))
	decoder.UseNumber()
	if err := decoder.Decode(&result); err != nil {
		t.Fatalf("json.Unmarshal() error = %v\njson=%s", err, stdout.String())
	}
	if _, ok := result["incidents"]; ok {
		t.Fatalf("incidents key exists in V2 JSON\njson=%s", stdout.String())
	}
	plan := sliceValue(t, result, "query_plan")
	record := mapValueFromAny(t, plan[0], "query_plan[0]")
	if got, want := intValue(t, record, "result_count"), 3; got != want {
		t.Fatalf("query_plan[0].result_count = %d, want %d", got, want)
	}
}

func TestDiagnoseJSONV2SeedMatchesPreserveRawSeedLines(t *testing.T) {
	rawLine := "[conn=281072972] [sql=select * from test.t where id=1] Request range exceeds bound"
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123099000000000","` + rawLine + `"]
					]
				},
				{
					"stream":{"component":"tikv","instance":"10.0.0.2:20160","namespace":"tidbcluster-42"},
					"values":[
						["1712123102000000000","[regionID=6500] Request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4001","namespace":"tidbcluster-42"},
					"values":[
						["1712123103000000000","[conn=281072972] [regionID=6500] Request range exceeds bound"]
					]
				}
			]
		}
		}
		`)

	client := &recordingClinicClient{
		responses: []clinicResponse{{body: body}},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	var result map[string]any
	decoder := json.NewDecoder(strings.NewReader(stdout.String()))
	decoder.UseNumber()
	if err := decoder.Decode(&result); err != nil {
		t.Fatalf("json.Unmarshal() error = %v\njson=%s", err, stdout.String())
	}
	matches := sliceValue(t, result, "seed_matches")
	if got, want := len(matches), 3; got != want {
		t.Fatalf("seed_matches length = %d, want %d\njson=%s", got, want, stdout.String())
	}
	entry := mapValueFromAny(t, matches[0], "seed_matches[0]")
	if got, want := stringValue(t, entry, "raw"), rawLine; got != want {
		t.Fatalf("seed_matches[0].raw = %q, want %q", got, want)
	}
}

func TestDiagnoseNoResultsExitSuccess(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":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "table",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	assertContainsAll(t, stdout.String(), []string{
		`diagnose.v2 bundle for "Request range exceeds bound"`,
		`searched cluster=cluster-42 namespace=tidbcluster-42`,
		`seed matches=0 related logs=0`,
		`groups=0`,
	})
}

func TestDiagnoseLowConfidenceOutputExitSuccess(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.9:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123099000000000","component-only noisy line"]
					]
				}
			]
		}
		}
		`)

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: body},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	if err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		NoExpand:  true,
		Output:    "table",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout); err != nil {
		t.Fatalf("runDiagnoseSearch() error = %v", err)
	}

	assertContainsAll(t, stdout.String(), []string{
		`diagnose.v2 bundle for "Request range exceeds bound"`,
		`searched cluster=cluster-42 namespace=tidbcluster-42`,
		`seed matches=1 related logs=0`,
		`groups=1`,
		`unknown_scope`,
	})
}

func TestDiagnoseMalformedLokiResponseReturnsError(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{{body: []byte("{")}},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	err := runDiagnoseSearch(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC))
	if err == nil {
		t.Fatal("runDiagnoseSearch() error = nil, want decode error")
	}
	if got := err.Error(); !strings.Contains(got, "decode loki response") {
		t.Fatalf("error = %q, want decode loki response", got)
	}
}

func TestDiagnoseSeedQueryAPIErrorCollectsPartialBundleBeforeReturningError(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{err: &clinic.HTTPError{StatusCode: http.StatusInternalServerError, Status: "500 Internal Server Error"}},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[{"id":"evt-1","createdAt":"2024-04-03T05:45:00Z","message":"scale out"}]}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	var stdout strings.Builder
	err := runDiagnoseSearchWithOutput(ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Since:     "2h",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 20, 16, 44, 59, 0, time.UTC), &stdout)
	if err == nil {
		t.Fatal("runDiagnoseSearch() error = nil, want API error")
	}
	if got := err.Error(); !strings.Contains(got, "500 Internal Server Error") {
		t.Fatalf("runDiagnoseSearch() error = %q, want 500 Internal Server Error", got)
	}
	if got, want := len(client.requests), 3; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}

	var result map[string]any
	decoder := json.NewDecoder(strings.NewReader(stdout.String()))
	decoder.UseNumber()
	if err := decoder.Decode(&result); err != nil {
		t.Fatalf("json.Decode() error = %v\njson=%s", err, stdout.String())
	}

	plan := sliceValue(t, result, "query_plan")
	if got, want := len(plan), 3; got != want {
		t.Fatalf("query_plan length = %d, want %d\njson=%s", got, want, stdout.String())
	}
	seedRecord := mapValueFromAny(t, plan[0], "query_plan[0]")
	if got, want := stringValue(t, seedRecord, "purpose"), "seed_exact_match"; got != want {
		t.Fatalf("query_plan[0].purpose = %q, want %q", got, want)
	}
	if got, want := stringValue(t, seedRecord, "status"), "failed"; got != want {
		t.Fatalf("query_plan[0].status = %q, want %q", got, want)
	}
	topologyRecord := mapValueFromAny(t, plan[1], "query_plan[1]")
	if got, want := stringValue(t, topologyRecord, "purpose"), "topology"; got != want {
		t.Fatalf("query_plan[1].purpose = %q, want %q", got, want)
	}
	if got, want := stringValue(t, topologyRecord, "status"), "success"; got != want {
		t.Fatalf("query_plan[1].status = %q, want %q", got, want)
	}
	events := sliceValue(t, result, "events")
	if got, want := len(events), 1; got != want {
		t.Fatalf("events length = %d, want %d\njson=%s", got, want, stdout.String())
	}

	warnings := sliceValue(t, result, "warnings")
	if got, want := len(warnings), 1; got != want {
		t.Fatalf("warnings length = %d, want %d\njson=%s", got, want, stdout.String())
	}
	warning := mapValueFromAny(t, warnings[0], "warnings[0]")
	if got, want := stringValue(t, warning, "code"), "query_error"; got != want {
		t.Fatalf("warning.code = %q, want %q", got, want)
	}
	if got, want := stringValue(t, warning, "source"), "seed_exact_match"; got != want {
		t.Fatalf("warning.source = %q, want %q", got, want)
	}
	if got, want := stringValue(t, warning, "query_id"), "q1"; got != want {
		t.Fatalf("warning.query_id = %q, want %q", got, want)
	}
	if got := stringValue(t, warning, "message"); !strings.Contains(got, "500 Internal Server Error") {
		t.Fatalf("warning.message = %q, want 500 Internal Server Error", got)
	}
}

func TestDiagnoseRejectsUnexpectedLokiResultType(t *testing.T) {
	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: []byte(`{"status":"success","data":{"resultType":"vector","result":[]}}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	err := diagnoseCommandHandler(ctx, []string{"seed", "--cluster-id", "cluster-42", "--since", "1h"}, &strings.Builder{})
	if err == nil {
		t.Fatal("diagnoseCommandHandler() error = nil, want result type error")
	}
	if got, want := err.Error(), "unexpected loki result type: vector"; got != want {
		t.Fatalf("error = %q, want %q", got, want)
	}
}

func TestDiagnoseDeduplicatesAndAssignsStableEvidenceIDs(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"beta","instance":"10.0.0.2:4000"},
					"values":[
						["1712123000000000000","beta line"],
						["1712123000000000000","beta line"]
					]
				},
				{
					"stream":{"component":"alpha","instance":"10.0.0.1:4000"},
					"values":[
						["1712123000000000000","alpha line"],
						["1712123000000000000","alpha line"]
					]
				}
			]
		}
		}
		`)

	evidence, err := normalizeDiagnoseEvidence(body)
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidence() error = %v", err)
	}
	if got, want := len(evidence), 2; got != want {
		t.Fatalf("evidence count = %d, want %d", got, want)
	}
	if got, want := evidence[0].ID, "E1"; got != want {
		t.Fatalf("first evidence ID = %q, want %q", got, want)
	}
	if got, want := evidence[0].Component, "alpha"; got != want {
		t.Fatalf("first evidence component = %q, want %q", got, want)
	}
	if got, want := evidence[1].ID, "E2"; got != want {
		t.Fatalf("second evidence ID = %q, want %q", got, want)
	}
	if got, want := evidence[1].Component, "beta"; got != want {
		t.Fatalf("second evidence component = %q, want %q", got, want)
	}
}

func TestDiagnoseExtractsBracketFields(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{},
					"values":[
						["1712123000000000000","[conn=281072972] [regionID=6500] [sql=select * from test.t where id=1] [unknown=keep]"]
					]
				}
			]
		}
	}`)

	evidence, err := normalizeDiagnoseEvidence(body)
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidence() error = %v", err)
	}
	fields := evidence[0].Fields
	if got, want := fields["conn"], "281072972"; got != want {
		t.Fatalf("conn field = %q, want %q", got, want)
	}
	if got, want := fields["regionID"], "6500"; got != want {
		t.Fatalf("regionID field = %q, want %q", got, want)
	}
	if got, want := fields["sql"], "select * from test.t where id=1"; got != want {
		t.Fatalf("sql field = %q, want %q", got, want)
	}
	if got, want := fields["unknown"], "keep"; got != want {
		t.Fatalf("unknown field = %q, want %q", got, want)
	}
}

func TestDiagnoseExtractsKnownKVAnchors(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{},
					"values":[
						["1712123000000000000","conn=281072972 txnStartTS=450000 regionID=6500 storeAddr=10.0.0.1:20160 keyspaceName=tenant-a"]
					]
				}
			]
		}
	}`)

	evidence, err := normalizeDiagnoseEvidence(body)
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidence() error = %v", err)
	}
	anchors := evidence[0].Anchors
	if got, want := anchors.ConnID, "281072972"; got != want {
		t.Fatalf("conn id = %q, want %q", got, want)
	}
	if got, want := anchors.TxnStartTS, "450000"; got != want {
		t.Fatalf("txn start ts = %q, want %q", got, want)
	}
	if got, want := anchors.RegionID, "6500"; got != want {
		t.Fatalf("region id = %q, want %q", got, want)
	}
	if got, want := anchors.StoreAddr, "10.0.0.1:20160"; got != want {
		t.Fatalf("store addr = %q, want %q", got, want)
	}
	if got, want := anchors.Keyspace, "tenant-a"; got != want {
		t.Fatalf("keyspace = %q, want %q", got, want)
	}
}

func TestDiagnoseExtractsScalarAnchorBeforeTrailingMessage(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{},
					"values":[
						["1712123000000000000","regionID=6500 Request range exceeds bound"]
					]
				}
			]
		}
	}`)

	evidence, err := normalizeDiagnoseEvidence(body)
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidence() error = %v", err)
	}
	if got, want := evidence[0].Anchors.RegionID, "6500"; got != want {
		t.Fatalf("region id = %q, want %q", got, want)
	}
}

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

	result := mustDiagnoseCorrelationResult(t, diagnoseCorrelationFixtureEvidence(t))

	if got, want := len(result.Incidents), 1; got != want {
		t.Fatalf("incident count = %d, want %d", got, want)
	}

	incident := result.Incidents[0]
	if got, want := incident.Status, "correlated"; got != want {
		t.Fatalf("incident status = %q, want %q", got, want)
	}
	if got, want := incident.ID, "I1"; got != want {
		t.Fatalf("incident id = %q, want %q", got, want)
	}
	if got, want := incident.EvidenceCount, 3; got != want {
		t.Fatalf("incident evidence count = %d, want %d", got, want)
	}
	if got, want := incident.WeakEvidenceCount, 0; got != want {
		t.Fatalf("incident weak evidence count = %d, want %d", got, want)
	}
	if got, want := incident.Anchors.ConnID, "281072972"; got != want {
		t.Fatalf("incident conn_id = %q, want %q", got, want)
	}
	if got, want := incident.Anchors.RegionID, "6500"; got != want {
		t.Fatalf("incident region_id = %q, want %q", got, want)
	}
	if got, want := incident.Anchors.SQL, "select * from test.t where id=1"; got != want {
		t.Fatalf("incident sql = %q, want %q", got, want)
	}
	if strings.Contains(strings.ToLower(incident.Summary), "caused by") {
		t.Fatalf("incident summary contains root-cause wording: %q", incident.Summary)
	}
	assertContainsAll(t, incident.Summary, []string{"conn_id", "region_id"})
}

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

	evidence := mustDiagnoseEvidenceFromBody(t, []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000"},
					"values":[
						["1712123000000000000","component-only one"]
					]
				},
				{
					"stream":{"component":"tidb","instance":"10.0.0.2:4000"},
					"values":[
						["1712123003000000000","component-only two"]
					]
				}
			]
		}
		}
		`))

	result := correlateDiagnoseEvidence(QueryScope{Seed: "seed"}, evidence)
	if got, want := len(result.Incidents), 0; got != want {
		t.Fatalf("incident count = %d, want %d", got, want)
	}
	if got, want := len(result.Warnings), 1; got != want {
		t.Fatalf("warning count = %d, want %d", got, want)
	}
}

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

	evidence := mustDiagnoseEvidenceFromBody(t, []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000"},
					"values":[
						["1712123000000000000","conn=281072972 sql=select * from test.t where id=1"]
					]
				},
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4001"},
					"values":[
						["1712123001000000000","conn=281072972"]
					]
				},
				{
					"stream":{"component":"tikv","instance":"10.0.0.2:20160"},
					"values":[
						["1712123002000000000","regionID=6500 request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tikv","instance":"10.0.0.2:20161"},
					"values":[
						["1712123003000000000","regionID=6500"]
					]
				},
				{
					"stream":{"component":"tidb","instance":"10.0.0.9:4000"},
					"values":[
						["1712123004000000000","component-only bridge"]
					]
				}
			]
		}
		}
		`))

	result := correlateDiagnoseEvidence(QueryScope{Seed: "seed"}, evidence)
	if got, want := len(result.Incidents), 2; got != want {
		t.Fatalf("incident count = %d, want %d", got, want)
	}
	if got, want := len(result.Warnings), 1; got != want {
		t.Fatalf("warning count = %d, want %d", got, want)
	}
}

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

	base := time.Date(2026, time.April, 4, 12, 0, 0, 0, time.UTC)
	evidence := []Evidence{
		{ID: "E1", Time: base, Line: "conn side one", Anchors: Anchors{ConnID: "281072972"}},
		{ID: "E2", Time: base.Add(120 * time.Second), Line: "conn side two", Anchors: Anchors{ConnID: "281072972"}},
		{
			ID:      "E3",
			Time:    base.Add(181 * time.Second),
			Line:    "bridge has conn and region but sub-threshold region edge",
			Anchors: Anchors{ConnID: "281072972", RegionID: "6500"},
		},
		{ID: "E4", Time: base.Add(242 * time.Second), Line: "region side one", Anchors: Anchors{RegionID: "6500"}},
		{ID: "E5", Time: base.Add(362 * time.Second), Line: "region side two", Anchors: Anchors{RegionID: "6500"}},
	}

	result := correlateDiagnoseEvidence(QueryScope{Seed: "seed"}, evidence)
	if got, want := len(result.Incidents), 2; got != want {
		t.Fatalf("incident count = %d, want %d", got, want)
	}
	if got, want := result.Incidents[0].EvidenceCount, 3; got != want {
		t.Fatalf("first incident evidence count = %d, want %d", got, want)
	}
	if got, want := result.Incidents[0].Anchors.ConnID, "281072972"; got != want {
		t.Fatalf("first incident conn_id = %q, want %q", got, want)
	}
	if got, want := result.Incidents[1].EvidenceCount, 2; got != want {
		t.Fatalf("second incident evidence count = %d, want %d", got, want)
	}
	if got, want := result.Incidents[1].Anchors.RegionID, "6500"; got != want {
		t.Fatalf("second incident region_id = %q, want %q", got, want)
	}
}

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

	result := mustDiagnoseCorrelationResult(t, diagnoseCorrelationFixtureEvidence(t))
	if got, want := len(result.Incidents), 1; got != want {
		t.Fatalf("incident count = %d, want %d", got, want)
	}
	if got, want := result.Incidents[0].EvidenceCount, 3; got != want {
		t.Fatalf("incident evidence count = %d, want %d", got, want)
	}
	if got, want := len(result.Warnings), 1; got != want {
		t.Fatalf("warning count = %d, want %d", got, want)
	}
}

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

	result := mustDiagnoseCorrelationResult(t, diagnoseCorrelationFixtureEvidence(t))
	reasons := result.Incidents[0].ScoreReasons
	if len(reasons) == 0 {
		t.Fatal("score reasons are empty, want grouping evidence")
	}

	wantAnchors := map[string]bool{
		"conn_id":    false,
		"region_id":  false,
		"error_text": false,
		"timestamp":  false,
	}
	for _, reason := range reasons {
		if len(reason.EvidenceIDs) == 0 {
			t.Fatalf("score reason %+v has no evidence ids", reason)
		}
		if reason.Anchor == "" {
			t.Fatalf("score reason %+v has empty anchor", reason)
		}
		if reason.Description == "" {
			t.Fatalf("score reason %+v has empty description", reason)
		}
		if reason.Points <= 0 {
			t.Fatalf("score reason %+v has non-positive points", reason)
		}
		if _, ok := wantAnchors[reason.Anchor]; ok {
			wantAnchors[reason.Anchor] = true
		}
	}
	for anchor, seen := range wantAnchors {
		if !seen {
			t.Fatalf("expected score reason for anchor %q", anchor)
		}
	}
}

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

	incidents := []Incident{
		{
			Score:         10,
			EvidenceCount: 2,
			StartTime:     time.Unix(30, 0).UTC(),
			Anchors:       Anchors{ConnID: "b"},
		},
		{
			Score:         10,
			EvidenceCount: 3,
			StartTime:     time.Unix(20, 0).UTC(),
			Anchors:       Anchors{ConnID: "a"},
		},
		{
			Score:         12,
			EvidenceCount: 1,
			StartTime:     time.Unix(10, 0).UTC(),
			Anchors:       Anchors{ConnID: "c"},
		},
	}

	sortDiagnoseIncidents(incidents)

	if got, want := incidents[0].Anchors.ConnID, "c"; got != want {
		t.Fatalf("first incident conn_id = %q, want %q", got, want)
	}
	if got, want := incidents[1].Anchors.ConnID, "a"; got != want {
		t.Fatalf("second incident conn_id = %q, want %q", got, want)
	}
	if got, want := incidents[2].Anchors.ConnID, "b"; got != want {
		t.Fatalf("third incident conn_id = %q, want %q", got, want)
	}
}

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

	incidents := []Incident{
		{Score: 7, EvidenceCount: 1, StartTime: time.Unix(30, 0).UTC(), Anchors: Anchors{ConnID: "b"}},
		{Score: 8, EvidenceCount: 1, StartTime: time.Unix(20, 0).UTC(), Anchors: Anchors{ConnID: "a"}},
	}

	sortDiagnoseIncidents(incidents)
	assignDiagnoseIncidentIDs(incidents)

	if got, want := incidents[0].ID, "I1"; got != want {
		t.Fatalf("first incident id = %q, want %q", got, want)
	}
	if got, want := incidents[1].ID, "I2"; got != want {
		t.Fatalf("second incident id = %q, want %q", got, want)
	}
}

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

	evidence := mustDiagnoseEvidenceFromBody(t, []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.9:4000"},
					"values":[
						["1712123000000000000","component-only noisy line"]
					]
				}
			]
		}
		}
		`))

	result := correlateDiagnoseEvidence(QueryScope{Seed: "seed"}, evidence)
	if got, want := len(result.Incidents), 0; got != want {
		t.Fatalf("incident count = %d, want %d", got, want)
	}
	if got, want := len(result.Warnings), 1; got != want {
		t.Fatalf("warning count = %d, want %d", got, want)
	}
	if !strings.Contains(strings.ToLower(result.Warnings[0]), "correlation threshold") {
		t.Fatalf("warning = %q, want correlation threshold wording", result.Warnings[0])
	}
}

func diagnoseCorrelationFixtureEvidence(t *testing.T) []Evidence {
	t.Helper()

	return mustDiagnoseEvidenceFromBody(t, []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123099000000000","[conn=281072972] [sql=select * from test.t where id=1] Request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tikv","instance":"10.0.0.2:20160","namespace":"tidbcluster-42"},
					"values":[
						["1712123102000000000","[regionID=6500] Request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tidb","instance":"10.0.0.1:4001","namespace":"tidbcluster-42"},
					"values":[
						["1712123103000000000","[conn=281072972] [regionID=6500] Request range exceeds bound"]
					]
				},
				{
					"stream":{"component":"tidb","instance":"10.0.0.9:4000","namespace":"tidbcluster-42"},
					"values":[
						["1712123104000000000","[component=tidb] unrelated TiDB warning"]
					]
				}
			]
		}
		}
		`))
}

func mustDiagnoseEvidenceFromBody(t *testing.T, body []byte) []Evidence {
	t.Helper()

	evidence, err := normalizeDiagnoseEvidenceWithSeed(body, "Request range exceeds bound")
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidenceWithSeed() error = %v", err)
	}
	return evidence
}

func mustDiagnoseCorrelationResult(t *testing.T, evidence []Evidence) DiagnoseResult {
	t.Helper()

	result := correlateDiagnoseEvidence(QueryScope{
		ClusterID: "cluster-42",
		Seed:      "Request range exceeds bound",
		StartTime: time.Unix(1712123000, 0).UTC(),
		EndTime:   time.Unix(1712123600, 0).UTC(),
		Limit:     100,
	}, evidence)

	return result
}

func TestDiagnoseExtractsSeedErrorText(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{},
					"values":[
						["1712123000000000000","regionID=6500 Request range exceeds bound"]
					]
				}
			]
		}
		}
		`)

	evidence, err := normalizeDiagnoseEvidenceWithSeed(body, "Request range exceeds bound")
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidenceWithSeed() error = %v", err)
	}
	if got, want := evidence[0].Anchors.ErrorText, "Request range exceeds bound"; got != want {
		t.Fatalf("error text = %q, want %q", got, want)
	}
}

func TestDiagnosePreservesUnknownFields(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{},
					"values":[
						["1712123000000000000","foo=bar [traceID=abc123] baz=qux"]
					]
				}
			]
		}
		}
		`)

	evidence, err := normalizeDiagnoseEvidence(body)
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidence() error = %v", err)
	}
	fields := evidence[0].Fields
	if got, want := fields["foo"], "bar"; got != want {
		t.Fatalf("foo field = %q, want %q", got, want)
	}
	if got, want := fields["traceID"], "abc123"; got != want {
		t.Fatalf("traceID field = %q, want %q", got, want)
	}
	if got, want := fields["baz"], "qux"; got != want {
		t.Fatalf("baz field = %q, want %q", got, want)
	}
}

func TestDiagnoseNormalizesSQLAndExtractsTables(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{},
					"values":[
						["1712123000000000000","sql=SELECT * FROM db.orders JOIN db.lineitem ON orders.id = lineitem.order_id INTO db.archive"]
					]
				}
			]
		}
		}
		`)

	evidence, err := normalizeDiagnoseEvidence(body)
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidence() error = %v", err)
	}
	anchors := evidence[0].Anchors
	if got, want := anchors.SQL, "select * from db.orders join db.lineitem on orders.id = lineitem.order_id into db.archive"; got != want {
		t.Fatalf("sql anchor = %q, want %q", got, want)
	}
	if got, want := len(anchors.TableNames), 3; got != want {
		t.Fatalf("table name count = %d, want %d", got, want)
	}
	if got, want := anchors.TableNames[0], "db.orders"; got != want {
		t.Fatalf("first table name = %q, want %q", got, want)
	}
	if got, want := anchors.TableNames[1], "db.lineitem"; got != want {
		t.Fatalf("second table name = %q, want %q", got, want)
	}
	if got, want := anchors.TableNames[2], "db.archive"; got != want {
		t.Fatalf("third table name = %q, want %q", got, want)
	}
}

func TestDiagnoseExtractsStackFramesBestEffort(t *testing.T) {
	body := []byte(`{
		"status":"success",
		"data":{
			"resultType":"streams",
			"result":[
				{
					"stream":{},
					"values":[
						["1712123000000000000","panic: boom\n  github.com/tidbcloud/clinic-cli/internal/cmd.doThing\n  /Users/me/work/foo.go:123\n  main.main"]
					]
				}
			]
		}
		}
		`)

	evidence, err := normalizeDiagnoseEvidence(body)
	if err != nil {
		t.Fatalf("normalizeDiagnoseEvidence() error = %v", err)
	}
	frames := evidence[0].Anchors.StackFrames
	if got, want := len(frames), 3; got != want {
		t.Fatalf("frame count = %d, want %d", got, want)
	}
	if got, want := frames[0], "github.com/tidbcloud/clinic-cli/internal/cmd.doThing"; got != want {
		t.Fatalf("first frame = %q, want %q", got, want)
	}
	if got, want := frames[1], "/Users/me/work/foo.go:123"; got != want {
		t.Fatalf("second frame = %q, want %q", got, want)
	}
	if got, want := frames[2], "main.main"; got != want {
		t.Fatalf("third frame = %q, want %q", got, want)
	}
}

func clearDiagnoseRangeEnv(t *testing.T) {
	t.Helper()

	t.Setenv(envClinicRangeStart, "")
	t.Setenv(envClinicRangeEnd, "")
}

func setDiagnoseTestLocalTimeZone(t *testing.T, location *time.Location) {
	t.Helper()

	previous := time.Local
	time.Local = location
	t.Cleanup(func() {
		time.Local = previous
	})
}

func newDiagnoseTestRuntimeContext(client *recordingClinicClient) *RuntimeContext {
	return &RuntimeContext{
		SelectedEnv: "staging",
		SelectedProfile: &config.ProfileRecord{
			ClusterID:           "cluster-42",
			OrgID:               "org-1",
			ClusterDeployTypeV2: "dedicated",
		},
		Client: client,
	}
}
