package cmd

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

func TestDiagnoseGroupKindsActiveOtherSharedAndUnknown(t *testing.T) {
	scope := DiagnoseBundleScope{
		ClusterID:    "cluster-42",
		Namespace:    "tidbcluster-42",
		SharedpoolID: "pool-9",
		Timezone:     "Asia/Shanghai",
	}
	seedMatches := []DiagnoseLogEntry{
		{
			ID:        "log-001",
			TimeUTC:   "2026-04-22T09:00:00Z",
			Component: "tidb",
			Labels:    map[string]string{"namespace": "tidbcluster-42"},
			Fields:    map[string]string{"keyspaceName": "cluster-42"},
			Anchors:   map[string]any{"keyspace": "cluster-42"},
		},
		{
			ID:        "log-002",
			TimeUTC:   "2026-04-22T09:00:01Z",
			Component: "tikv",
			Labels:    map[string]string{"namespace": "tidbcluster-42"},
			Fields:    map[string]string{"keyspaceName": "tenant-b"},
			Anchors:   map[string]any{"keyspace": "tenant-b"},
		},
		{
			ID:        "log-003",
			TimeUTC:   "2026-04-22T09:00:02Z",
			Component: "pd",
			Labels: map[string]string{
				"cluster_id": "unknown",
				"namespace":  "tidbcluster-42",
			},
			Anchors: map[string]any{},
		},
		{
			ID:        "log-004",
			TimeUTC:   "2026-04-22T09:00:03Z",
			Component: "ticdc",
			Labels: map[string]string{
				"cluster_id": "unknown",
				"namespace":  "tidbother",
			},
			Anchors: map[string]any{},
		},
	}

	groups, timelines, _, _ := buildDiagnoseGroupsTimelinesAndSummaries(
		scope,
		nil,
		seedMatches,
		nil,
		nil,
		time.UTC,
	)

	if got, want := len(groups), 4; got != want {
		t.Fatalf("group count = %d, want %d", got, want)
	}
	if got, want := len(timelines), 4; got != want {
		t.Fatalf("timeline count = %d, want %d", got, want)
	}

	groupByKind := make(map[string]DiagnoseGroup, len(groups))
	for _, group := range groups {
		groupByKind[group.Kind] = group
	}
	for _, kind := range []string{
		"active_profile_keyspace",
		"other_keyspace",
		"shared_component",
		"unknown_scope",
	} {
		if _, ok := groupByKind[kind]; !ok {
			t.Fatalf("group kind %q not found in %#v", kind, groups)
		}
	}

	if got, want := groupByKind["active_profile_keyspace"].Keyspace, "cluster-42"; got != want {
		t.Fatalf("active keyspace = %q, want %q", got, want)
	}
	if got, want := groupByKind["other_keyspace"].Keyspace, "tenant-b"; got != want {
		t.Fatalf("other keyspace = %q, want %q", got, want)
	}
}

func TestDiagnoseMultiKeyspaceWarning(t *testing.T) {
	scope := DiagnoseBundleScope{ClusterID: "cluster-42", Namespace: "tidbcluster-42", Timezone: "UTC"}
	seedMatches := []DiagnoseLogEntry{
		{
			ID:      "log-001",
			TimeUTC: "2026-04-22T09:00:00Z",
			Fields:  map[string]string{"keyspaceName": "cluster-42"},
			Anchors: map[string]any{"keyspace": "cluster-42"},
			Labels:  map[string]string{"namespace": "tidbcluster-42"},
		},
		{
			ID:      "log-002",
			TimeUTC: "2026-04-22T09:00:01Z",
			Fields:  map[string]string{"keyspaceName": "tenant-b"},
			Anchors: map[string]any{"keyspace": "tenant-b"},
			Labels:  map[string]string{"namespace": "tidbcluster-42"},
		},
	}

	_, _, _, warnings := buildDiagnoseGroupsTimelinesAndSummaries(scope, nil, seedMatches, nil, nil, time.UTC)
	if !diagnoseWarningsContainCodeForTest(warnings, "multi_keyspace") {
		t.Fatalf("warnings do not contain multi_keyspace: %#v", warnings)
	}
}

func TestDiagnoseGroupActiveByTidbClusterIDLabel(t *testing.T) {
	scope := DiagnoseBundleScope{ClusterID: "cluster-42", Namespace: "tidbcluster-42", Timezone: "UTC"}
	seedMatches := []DiagnoseLogEntry{
		{
			ID:      "log-001",
			TimeUTC: "2026-04-22T09:00:00Z",
			Labels: map[string]string{
				"cluster_id":      "unknown",
				"tidb_cluster_id": "cluster-42",
				"namespace":       "tidbcluster-42",
			},
			Anchors: map[string]any{},
		},
	}

	groups, _, _, _ := buildDiagnoseGroupsTimelinesAndSummaries(scope, nil, seedMatches, nil, nil, time.UTC)
	if got, want := len(groups), 1; got != want {
		t.Fatalf("group count = %d, want %d", got, want)
	}
	if got, want := groups[0].Kind, "active_profile_keyspace"; got != want {
		t.Fatalf("group kind = %q, want %q", got, want)
	}
}

func TestDiagnoseTimelineSortOrderAndRelativeTiming(t *testing.T) {
	scope := DiagnoseBundleScope{ClusterID: "cluster-42", Namespace: "tidbcluster-42", Timezone: "Asia/Shanghai"}
	seedMatches := []DiagnoseLogEntry{
		{
			ID:        "log-001",
			TimeUTC:   "2026-04-22T10:00:00Z",
			TimeLocal: "2026-04-22T18:00:00+08:00",
			Component: "tidb",
			Instance:  "tidb-0",
			Labels:    map[string]string{"namespace": "tidbcluster-42"},
			Fields:    map[string]string{"keyspaceName": "cluster-42"},
			Anchors:   map[string]any{"keyspace": "cluster-42"},
		},
		{
			ID:        "log-002",
			TimeUTC:   "2026-04-22T10:02:00Z",
			TimeLocal: "2026-04-22T18:02:00+08:00",
			Component: "tikv",
			Instance:  "tikv-0",
			Labels:    map[string]string{"namespace": "tidbcluster-42"},
			Fields:    map[string]string{"keyspaceName": "cluster-42"},
			Anchors:   map[string]any{"keyspace": "cluster-42"},
		},
	}
	relatedLogs := []DiagnoseLogEntry{
		{
			ID:        "log-003",
			TimeUTC:   "2026-04-22T09:59:00Z",
			TimeLocal: "2026-04-22T17:59:00+08:00",
			Component: "tidb",
			Instance:  "tidb-1",
			Relations: []DiagnoseRelation{{SeedID: "log-001", Type: "same_connection"}},
		},
		{
			ID:        "log-004",
			TimeUTC:   "2026-04-22T10:03:00Z",
			TimeLocal: "2026-04-22T18:03:00+08:00",
			Component: "tikv",
			Instance:  "tikv-1",
			Relations: []DiagnoseRelation{{SeedID: "log-001", Type: "same_connection"}},
		},
	}
	events := []DiagnoseEvent{
		{ID: "event-001", Timestamp: "2026-04-22T09:58:00Z", Message: "scale out"},
		{ID: "event-002", Timestamp: "2026-04-22T10:01:00Z", Message: "ddl start"},
		{ID: "event-003", Timestamp: "2026-04-22T10:04:00Z", Message: "ddl done"},
	}

	groups, timelines, _, warnings := buildDiagnoseGroupsTimelinesAndSummaries(scope, nil, seedMatches, relatedLogs, events, time.FixedZone("UTC+8", 8*3600))
	if len(warnings) != 0 {
		t.Fatalf("warnings = %#v, want none", warnings)
	}
	if got, want := len(groups), 1; got != want {
		t.Fatalf("group count = %d, want %d", got, want)
	}
	if got, want := len(timelines), 1; got != want {
		t.Fatalf("timeline count = %d, want %d", got, want)
	}

	timeline := timelines[0]
	if got, want := timeline.GroupID, groups[0].ID; got != want {
		t.Fatalf("timeline.group_id = %q, want %q", got, want)
	}
	if got, want := len(timeline.Entries), 7; got != want {
		t.Fatalf("entry count = %d, want %d", got, want)
	}

	wantOrder := []string{"event-001", "log-003", "log-001", "event-002", "log-002", "log-004", "event-003"}
	wantRelative := []string{"before", "before", "during", "during", "during", "after", "after"}
	for i, entry := range timeline.Entries {
		if got, want := entry.SourceID, wantOrder[i]; got != want {
			t.Fatalf("entries[%d].source_id = %q, want %q", i, got, want)
		}
		if got, want := entry.RelativeToSeedWindow, wantRelative[i]; got != want {
			t.Fatalf("entries[%d].relative_to_seed_window = %q, want %q", i, got, want)
		}
	}
}

func TestDiagnoseTimelineTruncationWarning(t *testing.T) {
	scope := DiagnoseBundleScope{ClusterID: "cluster-42", Namespace: "tidbcluster-42", Timezone: "UTC"}
	seedMatches := make([]DiagnoseLogEntry, 0, 501)
	base := time.Date(2026, time.April, 22, 9, 0, 0, 0, time.UTC)
	for i := 0; i < 501; i++ {
		seedMatches = append(seedMatches, DiagnoseLogEntry{
			ID:        fmt.Sprintf("log-%03d", i+1),
			TimeUTC:   base.Add(time.Duration(i) * time.Second).Format(time.RFC3339),
			Component: "tidb",
			Labels:    map[string]string{"namespace": "tidbcluster-42"},
			Fields:    map[string]string{"keyspaceName": "cluster-42"},
			Anchors:   map[string]any{"keyspace": "cluster-42"},
		})
	}

	_, timelines, _, warnings := buildDiagnoseGroupsTimelinesAndSummaries(scope, nil, seedMatches, nil, nil, time.UTC)
	if got, want := len(timelines), 1; got != want {
		t.Fatalf("timeline count = %d, want %d", got, want)
	}
	if got, want := len(timelines[0].Entries), 500; got != want {
		t.Fatalf("entries after truncation = %d, want %d", got, want)
	}
	if !diagnoseWarningsContainCodeForTest(warnings, "timeline_truncated") {
		t.Fatalf("warnings do not contain timeline_truncated: %#v", warnings)
	}
}

func TestDiagnoseSummaryCountsAndAnchorAggregation(t *testing.T) {
	scope := DiagnoseBundleScope{ClusterID: "cluster-42", Namespace: "tidbcluster-42", Timezone: "UTC"}
	queryPlan := []DiagnoseQueryRecord{
		{ID: "q1", HitLimit: false},
		{ID: "q2", HitLimit: true},
		{ID: "q3", HitLimit: true},
	}
	seedMatches := []DiagnoseLogEntry{
		{
			ID:        "log-001",
			TimeUTC:   "2026-04-22T09:00:00Z",
			Component: "tidb",
			Instance:  "tidb-0",
			Labels:    map[string]string{"namespace": "tidbcluster-42"},
			Fields:    map[string]string{"keyspaceName": "cluster-42"},
			Anchors: map[string]any{
				"keyspace":  "cluster-42",
				"conn":      "281072972",
				"region_id": "6500",
			},
		},
		{
			ID:        "log-002",
			TimeUTC:   "2026-04-22T09:01:00Z",
			Component: "tikv",
			Instance:  "tikv-0",
			Labels:    map[string]string{"namespace": "tidbcluster-42"},
			Fields:    map[string]string{"keyspaceName": "tenant-a"},
			Anchors: map[string]any{
				"keyspace": "tenant-a",
				"conn":     "281072973",
				"table":    []any{"db1.t1"},
			},
		},
		{
			ID:        "log-003",
			TimeUTC:   "2026-04-22T09:02:00Z",
			Component: "pd",
			Instance:  "pd-0",
			Labels: map[string]string{
				"cluster_id": "unknown",
				"namespace":  "tidbcluster-42",
			},
			Anchors: map[string]any{
				"trace_id": "trace-1",
			},
		},
	}
	relatedLogs := []DiagnoseLogEntry{
		{
			ID:        "log-004",
			TimeUTC:   "2026-04-22T09:03:00Z",
			Component: "tikv",
			Instance:  "tikv-1",
			Anchors: map[string]any{
				"conn":  "281072972",
				"table": []string{"db1.t2"},
			},
			Relations: []DiagnoseRelation{
				{SeedID: "log-001", Type: "same_connection"},
			},
		},
	}

	_, _, summaries, _ := buildDiagnoseGroupsTimelinesAndSummaries(scope, queryPlan, seedMatches, relatedLogs, nil, time.UTC)
	if got, want := summaries.MatchCounts.TotalSeedMatches, 3; got != want {
		t.Fatalf("total_seed_matches = %d, want %d", got, want)
	}
	if got, want := summaries.MatchCounts.ByComponent["tidb"], 1; got != want {
		t.Fatalf("by_component[tidb] = %d, want %d", got, want)
	}
	if got, want := summaries.MatchCounts.ByComponent["tikv"], 1; got != want {
		t.Fatalf("by_component[tikv] = %d, want %d", got, want)
	}
	if got, want := summaries.MatchCounts.ByKeyspace["cluster-42"], 1; got != want {
		t.Fatalf("by_keyspace[cluster-42] = %d, want %d", got, want)
	}
	if got, want := summaries.MatchCounts.ByKeyspace["tenant-a"], 1; got != want {
		t.Fatalf("by_keyspace[tenant-a] = %d, want %d", got, want)
	}
	if got, want := summaries.FirstSeedMatchUTC, "2026-04-22T09:00:00Z"; got != want {
		t.Fatalf("first_seed_match_utc = %q, want %q", got, want)
	}
	if got, want := summaries.LastSeedMatchUTC, "2026-04-22T09:02:00Z"; got != want {
		t.Fatalf("last_seed_match_utc = %q, want %q", got, want)
	}
	if got, want := summaries.ActiveProfileSeedMatches, 1; got != want {
		t.Fatalf("active_profile_seed_matches = %d, want %d", got, want)
	}
	if got, want := summaries.OtherKeyspaceSeedMatches, 1; got != want {
		t.Fatalf("other_keyspace_seed_matches = %d, want %d", got, want)
	}
	if got, want := summaries.SharedComponentSeedMatches, 1; got != want {
		t.Fatalf("shared_component_seed_matches = %d, want %d", got, want)
	}

	if got, want := strings.Join(summaries.AffectedComponents, ","), "pd,tidb,tikv"; got != want {
		t.Fatalf("affected_components = %q, want %q", got, want)
	}
	if got, want := strings.Join(summaries.AffectedInstances, ","), "pd-0,tidb-0,tikv-0,tikv-1"; got != want {
		t.Fatalf("affected_instances = %q, want %q", got, want)
	}

	if got, want := strings.Join(summaries.ExtractedAnchors["conn"], ","), "281072972,281072973"; got != want {
		t.Fatalf("extracted_anchors[conn] = %q, want %q", got, want)
	}
	if got, want := strings.Join(summaries.ExtractedAnchors["table"], ","), "db1.t1,db1.t2"; got != want {
		t.Fatalf("extracted_anchors[table] = %q, want %q", got, want)
	}
	if got, want := summaries.QueryLimits.AnyQueryHitLimit, true; got != want {
		t.Fatalf("any_query_hit_limit = %t, want %t", got, want)
	}
	if got, want := strings.Join(summaries.QueryLimits.HitLimitQueryIDs, ","), "q2,q3"; got != want {
		t.Fatalf("hit_limit_query_ids = %q, want %q", got, want)
	}
}

func TestDiagnoseSummaryJSONOmitsForbiddenRootCauseFields(t *testing.T) {
	scope := DiagnoseBundleScope{ClusterID: "cluster-42", Namespace: "tidbcluster-42", Timezone: "UTC"}
	seedMatches := []DiagnoseLogEntry{
		{
			ID:      "log-001",
			TimeUTC: "2026-04-22T09:00:00Z",
			Fields:  map[string]string{"keyspaceName": "cluster-42"},
			Anchors: map[string]any{"keyspace": "cluster-42"},
			Labels:  map[string]string{"namespace": "tidbcluster-42"},
		},
	}

	_, _, summaries, _ := buildDiagnoseGroupsTimelinesAndSummaries(scope, nil, seedMatches, nil, nil, time.UTC)
	raw, err := json.Marshal(summaries)
	if err != nil {
		t.Fatalf("json.Marshal(summaries) error = %v", err)
	}
	text := string(raw)
	for _, forbidden := range []string{"root_cause", "likely_cause", "culprit", "benign"} {
		if strings.Contains(text, forbidden) {
			t.Fatalf("summary json unexpectedly includes %q: %s", forbidden, text)
		}
	}
}

func diagnoseWarningsContainCodeForTest(warnings []DiagnoseWarning, code string) bool {
	for _, warning := range warnings {
		if warning.Code == code {
			return true
		}
	}
	return false
}
