package cmd

import (
	"errors"
	"strconv"
	"strings"
	"testing"
	"time"
)

func TestDiagnoseNoExpandRunsNoRelatedLokiQueriesAndWarns(t *testing.T) {
	client := &recordingClinicClient{
		responses: diagnoseExpansionResponsesForTest(
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123000000000000", `[conn=281072972] Request range exceeds bound`},
			}),
		),
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseExpansionOptionsForTest(func(opts *diagnoseOptions) {
		opts.NoExpand = true
	}), diagnoseExpansionNowForTest())

	if got, want := len(client.requests), 3; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	assertDiagnoseWarningForTest(t, bundle, "expansion_skipped", "related_logs")
}

func TestDiagnoseExpandMaxSeedSkipsExpansionAndWarns(t *testing.T) {
	client := &recordingClinicClient{
		responses: diagnoseExpansionResponsesForTest(
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123000000000000", `[conn=281072972] Request range exceeds bound`},
				{"1712123001000000000", `[conn=281072973] Request range exceeds bound`},
			}),
		),
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseExpansionOptionsForTest(func(opts *diagnoseOptions) {
		opts.MaxSeedExpand = 1
	}), diagnoseExpansionNowForTest())

	if got, want := len(client.requests), 3; got != want {
		t.Fatalf("request count = %d, want %d", got, want)
	}
	assertDiagnoseWarningForTest(t, bundle, "expansion_skipped", "seed_matches")
}

func TestDiagnoseRelatedSameConnectionQueryAndRelation(t *testing.T) {
	related := lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-1","namespace":"tidbcluster-42"}`, [][]string{
		{"1712123010000000000", `[conn=281072972] same connection follow-up`},
	})
	client := &recordingClinicClient{
		responses: diagnoseExpansionResponsesForTest(
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123000000000000", `[conn=281072972] Request range exceeds bound`},
			}),
			related,
		),
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseExpansionOptionsForTest(nil), diagnoseExpansionNowForTest())

	record := diagnoseQueryPlanRecordByPurposeForTest(t, bundle, "same_connection")
	if got, want := stringValue(t, record, "id"), "q4"; got != want {
		t.Fatalf("same_connection query id = %q, want %q", got, want)
	}
	query := stringValue(t, record, "query")
	if !strings.Contains(query, `|= "281072972"`) {
		t.Fatalf("same_connection query = %q, want exact quoted conn match", query)
	}

	relatedLogs := sliceValue(t, bundle, "related_logs")
	if got, want := len(relatedLogs), 1; got != want {
		t.Fatalf("related_logs length = %d, want %d\nbundle=%s", got, want, mustMarshalForTest(t, bundle))
	}
	relatedEntry := mapValueFromAny(t, relatedLogs[0], "related_logs[0]")
	relations := sliceValue(t, relatedEntry, "relations")
	relation := mapValueFromAny(t, relations[0], "related_logs[0].relations[0]")
	if got, want := stringValue(t, relation, "type"), "same_connection"; got != want {
		t.Fatalf("relation.type = %q, want %q", got, want)
	}
	if got, want := stringValue(t, relation, "strength"), "strong"; got != want {
		t.Fatalf("relation.strength = %q, want %q", got, want)
	}
}

func TestDiagnoseRelationSameInstanceAndSameRegionStrengths(t *testing.T) {
	client := &recordingClinicClient{
		responses: diagnoseExpansionResponsesForTest(
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123000000000000", `[regionID=6500] Request range exceeds bound`},
			}),
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123001000000000", `[INFO] same instance nearby`},
			}),
			lokiSeedResponseForTest(`{"component":"tikv","instance":"tikv-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123002000000000", `[regionID=6500] same region nearby`},
			}),
		),
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseExpansionOptionsForTest(nil), diagnoseExpansionNowForTest())

	diagnoseQueryPlanRecordByPurposeForTest(t, bundle, "same_instance_nearby")
	diagnoseQueryPlanRecordByPurposeForTest(t, bundle, "same_region")

	assertDiagnoseRelatedRelationForTest(t, bundle, "same_instance_nearby", "weak")
	assertDiagnoseRelatedRelationForTest(t, bundle, "same_region", "medium")
}

func TestDiagnoseRelatedLimitWarningAndHitLimit(t *testing.T) {
	client := &recordingClinicClient{
		responses: diagnoseExpansionResponsesForTest(
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123000000000000", `[conn=281072972] Request range exceeds bound`},
			}),
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-1","namespace":"tidbcluster-42"}`, repeatedDiagnoseExpansionValuesForTest(100, `[conn=281072972] related`)),
		),
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseExpansionOptionsForTest(nil), diagnoseExpansionNowForTest())

	record := diagnoseQueryPlanRecordByPurposeForTest(t, bundle, "same_connection")
	if got, want := boolValue(t, record, "hit_limit"), true; got != want {
		t.Fatalf("hit_limit = %t, want %t", got, want)
	}
	assertDiagnoseWarningForTest(t, bundle, "limit", "same_connection")
}

func TestDiagnoseRelatedQueryFailureKeepsBundleOutputAndWarns(t *testing.T) {
	client := &recordingClinicClient{
		responses: diagnoseExpansionResponsesForTest(
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123000000000000", `[conn=281072972] Request range exceeds bound`},
			}),
			[]byte{},
		),
	}
	client.responses[len(client.responses)-1].err = errors.New("loki unavailable")
	ctx := newDiagnoseTestRuntimeContext(client)

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseExpansionOptionsForTest(nil), diagnoseExpansionNowForTest())

	relatedLogs := sliceValue(t, bundle, "related_logs")
	if got, want := len(relatedLogs), 0; got != want {
		t.Fatalf("related_logs length = %d, want %d", got, want)
	}
	record := diagnoseQueryPlanRecordByPurposeForTest(t, bundle, "same_connection")
	if got, want := stringValue(t, record, "status"), "failed"; got != want {
		t.Fatalf("status = %q, want %q", got, want)
	}
	assertDiagnoseWarningForTest(t, bundle, "query_error", "same_connection")
}

func TestDiagnoseMaxRelatedLogsCapsOutputAndWarns(t *testing.T) {
	client := &recordingClinicClient{
		responses: diagnoseExpansionResponsesForTest(
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-0","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123000000000000", `[conn=281072972] Request range exceeds bound`},
			}),
			lokiSeedResponseForTest(`{"component":"tidb","instance":"tidb-1","namespace":"tidbcluster-42"}`, [][]string{
				{"1712123001000000000", `[conn=281072972] related one`},
				{"1712123002000000000", `[conn=281072972] related two`},
			}),
		),
	}
	ctx := newDiagnoseTestRuntimeContext(client)

	bundle := runDiagnoseBundleJSONForTest(t, ctx, diagnoseExpansionOptionsForTest(func(opts *diagnoseOptions) {
		opts.MaxRelatedLogs = 1
		opts.MaxRelatedLogsSet = true
	}), diagnoseExpansionNowForTest())

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

func diagnoseExpansionOptionsForTest(configure func(*diagnoseOptions)) diagnoseOptions {
	opts := diagnoseOptions{
		Seed:      "Request range exceeds bound",
		ClusterID: "cluster-42",
		Start:     "1712123000",
		End:       "1712123600",
		Limit:     100,
		Output:    "json",
	}
	if configure != nil {
		configure(&opts)
	}
	return opts
}

func diagnoseExpansionNowForTest() time.Time {
	return time.Date(2026, time.April, 22, 9, 30, 0, 0, time.UTC)
}

func diagnoseExpansionResponsesForTest(seedBody []byte, relatedBodies ...[]byte) []clinicResponse {
	responses := []clinicResponse{
		{body: seedBody},
		{body: []byte(`{"components":{"tidb":[{"host":"tidb-0"}]}}`)},
		{body: []byte(`{"items":[]}`)},
	}
	for _, body := range relatedBodies {
		responses = append(responses, clinicResponse{body: body})
	}
	return responses
}

func repeatedDiagnoseExpansionValuesForTest(count int, line string) [][]string {
	values := make([][]string, 0, count)
	base := int64(1712123010000000000)
	for i := 0; i < count; i++ {
		values = append(values, []string{
			strconvFormatIntForTest(base + int64(i)),
			line + " " + strconvFormatIntForTest(int64(i)),
		})
	}
	return values
}

func strconvFormatIntForTest(value int64) string {
	return strconv.FormatInt(value, 10)
}

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

	for _, item := range sliceValue(t, bundle, "query_plan") {
		record := mapValueFromAny(t, item, "query_plan[]")
		if stringValue(t, record, "purpose") == purpose {
			return record
		}
	}
	t.Fatalf("query plan purpose %q not found in bundle %s", purpose, mustMarshalForTest(t, bundle))
	return nil
}

func assertDiagnoseWarningForTest(t *testing.T, bundle map[string]any, code, source string) {
	t.Helper()

	for _, item := range sliceValue(t, bundle, "warnings") {
		warning := mapValueFromAny(t, item, "warnings[]")
		if stringValue(t, warning, "code") == code && stringValue(t, warning, "source") == source {
			return
		}
	}
	t.Fatalf("warning code=%q source=%q not found in bundle %s", code, source, mustMarshalForTest(t, bundle))
}

func assertDiagnoseRelatedRelationForTest(t *testing.T, bundle map[string]any, relationType, strength string) {
	t.Helper()

	for _, item := range sliceValue(t, bundle, "related_logs") {
		entry := mapValueFromAny(t, item, "related_logs[]")
		for _, relationItem := range sliceValue(t, entry, "relations") {
			relation := mapValueFromAny(t, relationItem, "relations[]")
			if stringValue(t, relation, "type") == relationType && stringValue(t, relation, "strength") == strength {
				return
			}
		}
	}
	t.Fatalf("relation type=%q strength=%q not found in bundle %s", relationType, strength, mustMarshalForTest(t, bundle))
}
