package cmd

import (
	"encoding/json"
	"testing"
	"time"
)

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

	rawLine := `2026/04/03 05:43:20.123 +00:00 [WARN] [session.go:123] [conn=281072972] [txnStartTS=449191516849864704] [regionID=6500] [regionVersion=12] [storeAddr=10.0.0.2:20160] [peerAddr=10.0.0.3:20160] [keyspaceName=ks1] [sql=select * from test.t where id=1] [sqlDigest=digest-1] [database=test] [table=t] [requestID=req-1] [traceID=trace-1] [unknown=keep] Request range exceeds bound`
	body := lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42","node":"node-a","clusterID":"cluster-42","tenantID":"tenant-1"}`, [][]string{
		{"1712123000123456789", rawLine},
	})
	bundle := runDiagnoseLogEntryBundleForTest(t, body)

	matches := sliceValue(t, bundle, "seed_matches")
	if got, want := len(matches), 1; got != want {
		t.Fatalf("seed_matches length = %d, want %d\nbundle=%s", got, want, mustMarshalForTest(t, bundle))
	}

	entry := mapValueFromAny(t, matches[0], "seed_matches[0]")
	if got, want := stringValue(t, entry, "id"), "log-001"; got != want {
		t.Fatalf("id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "kind"), "log"; got != want {
		t.Fatalf("kind = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "time_utc"), "2024-04-03T05:43:20.123456789Z"; got != want {
		t.Fatalf("time_utc = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "time_local"), "2024-04-03T13:43:20.123456789+08:00"; got != want {
		t.Fatalf("time_local = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "component"), "tidb"; got != want {
		t.Fatalf("component = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "instance"), "tidb-0"; got != want {
		t.Fatalf("instance = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "node"), "node-a"; got != want {
		t.Fatalf("node = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "stream"), `clusterID=cluster-42component=tidbinstance=tidb-0namespace=tidbcluster-42node=node-atenantID=tenant-1`; got != want {
		t.Fatalf("stream = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "level"), "WARN"; got != want {
		t.Fatalf("level = %q, want %q", got, want)
	}
	if got, want := stringValue(t, entry, "file"), "session.go"; got != want {
		t.Fatalf("file = %q, want %q", got, want)
	}
	if got, want := intValue(t, entry, "line_no"), 123; got != want {
		t.Fatalf("line_no = %d, want %d", got, want)
	}
	if got, want := stringValue(t, entry, "raw"), rawLine; got != want {
		t.Fatalf("raw = %q, want %q", got, want)
	}
	if got := stringValue(t, entry, "message"); got == "" || got == rawLine {
		t.Fatalf("message = %q, want parsed message distinct from raw", got)
	}

	labels := mapValue(t, entry, "labels")
	if got, want := stringValue(t, labels, "tenantID"), "tenant-1"; got != want {
		t.Fatalf("labels.tenantID = %q, want %q", got, want)
	}
	fields := mapValue(t, entry, "fields")
	if got, want := stringValue(t, fields, "unknown"), "keep"; got != want {
		t.Fatalf("fields.unknown = %q, want %q", got, want)
	}
	if got, want := stringValue(t, fields, "sql"), "select * from test.t where id=1"; got != want {
		t.Fatalf("fields.sql = %q, want %q", got, want)
	}

	anchors := mapValue(t, entry, "anchors")
	if got, want := stringValue(t, anchors, "conn"), "281072972"; got != want {
		t.Fatalf("anchors.conn = %q, want %q", got, want)
	}
	if got, want := stringValue(t, anchors, "txn_start_ts"), "449191516849864704"; got != want {
		t.Fatalf("anchors.txn_start_ts = %q, want %q", got, want)
	}
	if got, want := stringValue(t, anchors, "region_id"), "6500"; got != want {
		t.Fatalf("anchors.region_id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, anchors, "region_version"), "12"; got != want {
		t.Fatalf("anchors.region_version = %q, want %q", got, want)
	}
	if got, want := stringValue(t, anchors, "cluster_id"), "cluster-42"; got != want {
		t.Fatalf("anchors.cluster_id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, anchors, "tenant_id"), "tenant-1"; got != want {
		t.Fatalf("anchors.tenant_id = %q, want %q", got, want)
	}
	if got, want := stringValue(t, anchors, "sql"), "select * from test.t where id=1"; got != want {
		t.Fatalf("anchors.sql = %q, want %q", got, want)
	}
	if got, want := stringValue(t, anchors, "sql_digest"), "digest-1"; got != want {
		t.Fatalf("anchors.sql_digest = %q, want %q", got, want)
	}
	if got, want := stringValue(t, anchors, "error_text"), "Request range exceeds bound"; got != want {
		t.Fatalf("anchors.error_text = %q, want %q", got, want)
	}
	tables := sliceValue(t, anchors, "table")
	if !sliceContainsString(tables, "t") || !sliceContainsString(tables, "test.t") {
		t.Fatalf("anchors.table = %#v, want exact table and SQL-derived table", tables)
	}
	queryIDs := sliceValue(t, entry, "query_ids")
	if got, want := queryIDs[0].(string), "q1"; got != want {
		t.Fatalf("query_ids[0] = %q, want %q", got, want)
	}
}

func TestDiagnoseLogEntryParseFailureKeepsRawAndWarns(t *testing.T) {
	body := lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0"}`, [][]string{
		{"1712123000000000000", "plain unstructured seed match"},
	})
	bundle := runDiagnoseLogEntryBundleForTest(t, body)

	entry := mapValueFromAny(t, sliceValue(t, bundle, "seed_matches")[0], "seed_matches[0]")
	if got, want := stringValue(t, entry, "raw"), "plain unstructured seed match"; got != want {
		t.Fatalf("raw = %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\nbundle=%s", got, want, mustMarshalForTest(t, bundle))
	}
	warning := mapValueFromAny(t, warnings[0], "warnings[0]")
	if got, want := stringValue(t, warning, "code"), "parse_failure"; got != want {
		t.Fatalf("warning.code = %q, want %q", got, want)
	}
	if got, want := stringValue(t, warning, "source"), "log-001"; 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)
	}
}

func TestDiagnoseLogEntryRedactionsArePreservedAndWarned(t *testing.T) {
	body := lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0"}`, [][]string{
		{"1712123000000000000", `[regionID=6500] ranges=[?, ?) values="[?,?]" Request range exceeds bound ?`},
	})
	bundle := runDiagnoseLogEntryBundleForTest(t, body)

	entry := mapValueFromAny(t, sliceValue(t, bundle, "seed_matches")[0], "seed_matches[0]")
	redactions := sliceValue(t, entry, "redactions")
	if got, want := len(redactions), 4; got != want {
		t.Fatalf("redactions length = %d, want %d: %#v", got, want, redactions)
	}

	warnings := sliceValue(t, bundle, "warnings")
	if got, want := len(warnings), 1; got != want {
		t.Fatalf("warnings length = %d, want %d\nbundle=%s", got, want, mustMarshalForTest(t, bundle))
	}
	warning := mapValueFromAny(t, warnings[0], "warnings[0]")
	if got, want := stringValue(t, warning, "code"), "redaction"; got != want {
		t.Fatalf("warning.code = %q, want %q", got, want)
	}
	if got, want := stringValue(t, warning, "source"), "log-001"; got != want {
		t.Fatalf("warning.source = %q, want %q", got, want)
	}
}

func TestDiagnoseLogEntryDeduplicatesAndMergesQueryIDs(t *testing.T) {
	body := lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0"}`, [][]string{
		{"1712123000000000000", "[conn=281072972] Request range exceeds bound"},
		{"1712123000000000000", "[conn=281072972] Request range exceeds bound"},
	})

	entries, warnings, err := normalizeDiagnoseLogEntries(body, "Request range exceeds bound", []string{"q1", "q4"}, time.UTC)
	if err != nil {
		t.Fatalf("normalizeDiagnoseLogEntries() error = %v", err)
	}
	if len(warnings) != 0 {
		t.Fatalf("warnings = %#v, want none", warnings)
	}
	if got, want := len(entries), 1; got != want {
		t.Fatalf("entries length = %d, want %d", got, want)
	}
	if got, want := entries[0].ID, "log-001"; got != want {
		t.Fatalf("entry id = %q, want %q", got, want)
	}
	if got, want := entries[0].QueryIDs, []string{"q1", "q4"}; !equalStringSlices(got, want) {
		t.Fatalf("query IDs = %#v, want %#v", got, want)
	}
}

func runDiagnoseLogEntryBundleForTest(t *testing.T, seedBody []byte) map[string]any {
	t.Helper()

	client := &recordingClinicClient{
		responses: []clinicResponse{
			{body: seedBody},
			{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
			{body: []byte(`{"items":[]}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
			{body: []byte(`{"status":"success","data":{"resultType":"streams","result":[]}}`)},
		},
	}
	ctx := newDiagnoseTestRuntimeContext(client)
	return runDiagnoseBundleJSONForTest(t, ctx, diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Start:     "1712123000",
		End:       "1712123600",
		Limit:     100,
		Output:    "json",
	}, time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC))
}

func lokiSeedResponseForTest(labels string, values [][]string) []byte {
	raw := `{"status":"success","data":{"resultType":"streams","result":[{"stream":` + labels + `,"values":[`
	for i, value := range values {
		if i > 0 {
			raw += ","
		}
		raw += `[` + quoteJSONStringForTest(value[0]) + `,` + quoteJSONStringForTest(value[1]) + `]`
	}
	raw += `]}]}}`
	return []byte(raw)
}

func quoteJSONStringForTest(value string) string {
	raw, err := json.Marshal(value)
	if err != nil {
		panic(err)
	}
	return string(raw)
}

func equalStringSlices(left, right []string) bool {
	if len(left) != len(right) {
		return false
	}
	for i := range left {
		if left[i] != right[i] {
			return false
		}
	}
	return true
}

func sliceContainsString(values []any, want string) bool {
	for _, value := range values {
		if got, ok := value.(string); ok && got == want {
			return true
		}
	}
	return false
}
