package main

import "testing"

func TestDSNWithDatabaseReplacesDatabaseName(t *testing.T) {
	got, err := dsnWithDatabase("root@tcp(127.0.0.1:4000)/?parseTime=true&loc=UTC&multiStatements=true", "nexuscrm_pressure_test")
	if err != nil {
		t.Fatalf("dsnWithDatabase: %v", err)
	}
	want := "root@tcp(127.0.0.1:4000)/nexuscrm_pressure_test?parseTime=true&loc=UTC&multiStatements=true"
	if got != want {
		t.Fatalf("dsnWithDatabase = %q, want %q", got, want)
	}
}

func TestScenarioSeedGenerationNormalEnqueue(t *testing.T) {
	scenario, ok := findScenario("workflow_action_worker_path_800_events_120_lanes")
	if !ok {
		t.Fatal("workflow action scenario missing")
	}
	if !isSeedableScenario(scenario) {
		t.Fatal("workflow action scenario should be seedable")
	}
	if got := scenarioSeedGeneration(scenario); got != normalEnqueueGeneration {
		t.Fatalf("scenarioSeedGeneration = %d, want %d", got, normalEnqueueGeneration)
	}
}

func TestEvaluateLaneOrderSamplesViolations(t *testing.T) {
	records := []laneOrderRecord{
		{Lane: "lane-a", WorkID: "work-001", EventID: "event-001", EnqueueOrdinal: 1},
		{Lane: "lane-a", WorkID: "work-003", EventID: "event-003", EnqueueOrdinal: 3},
		{Lane: "lane-a", WorkID: "work-002", EventID: "event-002", EnqueueOrdinal: 2},
		{Lane: "lane-b", WorkID: "work-004", EventID: "event-004", EnqueueOrdinal: 1},
	}

	ok, violations, samples := evaluateLaneOrder(records)
	if ok {
		t.Fatal("evaluateLaneOrder ok = true, want false")
	}
	if violations != 1 {
		t.Fatalf("violations = %d, want 1", violations)
	}
	if len(samples) != 1 {
		t.Fatalf("samples len = %d, want 1", len(samples))
	}
	if samples[0] != "lane-a event event-002 enqueue_ordinal 2 after 3 (work-002)" {
		t.Fatalf("sample = %q", samples[0])
	}
}

func TestEvaluateLaneOrderSkipsMissingOrdinals(t *testing.T) {
	records := []laneOrderRecord{
		{Lane: "lane-a", WorkID: "work-001", EventID: "event-001", EnqueueOrdinal: 0},
		{Lane: "lane-a", WorkID: "work-002", EventID: "event-002", EnqueueOrdinal: 1},
	}

	ok, violations, samples := evaluateLaneOrder(records)
	if !ok {
		t.Fatalf("evaluateLaneOrder ok = false, violations=%d samples=%v", violations, samples)
	}
	if violations != 0 || len(samples) != 0 {
		t.Fatalf("violations/samples = %d/%v, want none", violations, samples)
	}
}

func TestMergeProgressBucketsKeepsMaxLeaseWidth(t *testing.T) {
	result := gateResult{
		CurrentLeasedWork:  1,
		CurrentLeasedLanes: 1,
		CurrentLeaseTokens: 1,
		MaxLeasedWork:      1,
		MaxLeasedLanes:     1,
		MaxLeasedTokens:    1,
	}

	mergeProgressBuckets(&result, []progressBucket{
		{CurrentLeasedWork: 3, CurrentLeasedLanes: 2, CurrentLeaseTokens: 4},
		{CurrentLeasedWork: 2, CurrentLeasedLanes: 5, CurrentLeaseTokens: 3},
	})

	if result.MaxLeasedWork != 3 {
		t.Fatalf("MaxLeasedWork = %d, want 3", result.MaxLeasedWork)
	}
	if result.MaxLeasedLanes != 5 {
		t.Fatalf("MaxLeasedLanes = %d, want 5", result.MaxLeasedLanes)
	}
	if result.MaxLeasedTokens != 4 {
		t.Fatalf("MaxLeasedTokens = %d, want 4", result.MaxLeasedTokens)
	}
	if len(result.Buckets) != 2 {
		t.Fatalf("Buckets len = %d, want 2", len(result.Buckets))
	}
}

func TestSummarizeReportAggregatesGateReadback(t *testing.T) {
	summary := summarizeReport([]gateResult{
		{
			Gate:               gateConfig{Name: "seed_scenario", Database: "nexuscrm_pressure_a", Scenario: "clean_800_events_120_lanes"},
			Accepted:           800,
			Materialized:       800,
			Processed:          800,
			ClaimEligibleHeads: 0,
			MaxLeasedWork:      16,
			MaxLeasedLanes:     16,
			MaxLeasedTokens:    16,
			LaneOrderingOK:     true,
		},
		{
			Gate:               gateConfig{Name: "readback", Database: "nexuscrm_pressure_b", Scenario: "tail_shape"},
			Accepted:           800,
			Materialized:       800,
			Skipped:            61,
			Nonterminal:        739,
			ClaimEligibleHeads: 120,
			MaxLeasedWork:      3,
			MaxLeasedLanes:     3,
			MaxLeasedTokens:    3,
			LaneOrderingOK:     true,
			Buckets: []progressBucket{
				{Skipped: 10, Nonterminal: 790},
				{Skipped: 61, Nonterminal: 739},
			},
		},
	})

	if summary.Status != "nonterminal" {
		t.Fatalf("summary status = %q, want nonterminal", summary.Status)
	}
	if !summary.HarnessOnly || summary.AcceptanceSurface != "local_only_no_test1" {
		t.Fatalf("summary surface = harness_only:%t acceptance:%q", summary.HarnessOnly, summary.AcceptanceSurface)
	}
	if summary.ResultCount != 2 {
		t.Fatalf("ResultCount = %d, want 2", summary.ResultCount)
	}
	if summary.CompletedCount != 1 || summary.NonterminalCount != 1 {
		t.Fatalf("completed/nonterminal = %d/%d, want 1/1", summary.CompletedCount, summary.NonterminalCount)
	}
	if summary.TotalAccepted != 1600 || summary.TotalMaterialized != 1600 || summary.TotalProcessed != 800 || summary.TotalSkipped != 61 || summary.TotalNonterminal != 739 {
		t.Fatalf("totals = accepted %d materialized %d processed %d skipped %d nonterminal %d",
			summary.TotalAccepted, summary.TotalMaterialized, summary.TotalProcessed, summary.TotalSkipped, summary.TotalNonterminal)
	}
	if summary.MaxClaimEligibleHeads != 120 || summary.MaxLeasedWork != 16 || summary.MaxLeasedLanes != 16 || summary.MaxLeasedTokens != 16 || summary.MaxBucketCount != 2 {
		t.Fatalf("maxes = claim %d work %d lanes %d tokens %d buckets %d",
			summary.MaxClaimEligibleHeads, summary.MaxLeasedWork, summary.MaxLeasedLanes, summary.MaxLeasedTokens, summary.MaxBucketCount)
	}
	if len(summary.Gates) != 2 {
		t.Fatalf("gate summaries = %d, want 2", len(summary.Gates))
	}
	if summary.Gates[1].Status != "nonterminal" || summary.Gates[1].LastBucketSkipped != 61 || summary.Gates[1].LastBucketPending != 739 {
		t.Fatalf("second gate summary = %+v", summary.Gates[1])
	}
	if len(summary.Notes) != 1 {
		t.Fatalf("summary notes = %v, want no-test1 note", summary.Notes)
	}
}

func TestSummarizeReportErrorPrecedence(t *testing.T) {
	summary := summarizeReport([]gateResult{
		{
			Gate:                gateConfig{Name: "broad"},
			LaneOrderingOK:      true,
			ExplainHasBroadScan: true,
		},
		{
			Gate:           gateConfig{Name: "failed"},
			LaneOrderingOK: true,
			Error:          "readback failed",
		},
	})

	if summary.Status != "error" {
		t.Fatalf("summary status = %q, want error", summary.Status)
	}
	if summary.ErrorCount != 1 || summary.BroadScanCount != 1 {
		t.Fatalf("error/broad counts = %d/%d, want 1/1", summary.ErrorCount, summary.BroadScanCount)
	}
	if summary.Gates[1].Status != "error" || summary.Gates[1].Error != "readback failed" {
		t.Fatalf("error gate summary = %+v", summary.Gates[1])
	}
}

func TestSummarizeReportCatalogOnly(t *testing.T) {
	summary := summarizeReport(nil)

	if summary.Status != "catalog_only" {
		t.Fatalf("summary status = %q, want catalog_only", summary.Status)
	}
	if !summary.HarnessOnly || summary.AcceptanceSurface != "local_only_no_test1" {
		t.Fatalf("summary surface = harness_only:%t acceptance:%q", summary.HarnessOnly, summary.AcceptanceSurface)
	}
	if summary.ResultCount != 0 || len(summary.Gates) != 0 {
		t.Fatalf("summary result/gate count = %d/%d, want 0/0", summary.ResultCount, len(summary.Gates))
	}
	if len(summary.Notes) != 1 {
		t.Fatalf("summary notes = %v, want no-test1 note", summary.Notes)
	}
}

func TestSummarizeCatalogCountsSeedModesRuntimeAndFlags(t *testing.T) {
	summary := summarizeCatalog([]scenarioDefinition{
		{
			Name:     "direct-a",
			SeedMode: "direct_work_items",
			Flags:    []string{"zero_lane_ordinal", "same_created_date"},
		},
		{
			Name:            "normal-a",
			SeedMode:        "normal_enqueue",
			RuntimeRequired: true,
			Flags:           []string{"worker_path", "normal_enqueue"},
		},
		{
			Name:            "normal-b",
			SeedMode:        "normal_enqueue",
			RuntimeRequired: true,
			Flags:           []string{"worker_path", "workflow_action"},
		},
		{
			Name:            "runtime-only",
			SeedMode:        "worker_path",
			RuntimeRequired: true,
			Flags:           []string{"worker_path"},
		},
	})

	if summary.ScenarioCount != 4 {
		t.Fatalf("ScenarioCount = %d, want 4", summary.ScenarioCount)
	}
	if summary.SeedableCount != 3 {
		t.Fatalf("SeedableCount = %d, want 3", summary.SeedableCount)
	}
	if summary.RuntimeRequiredCount != 3 {
		t.Fatalf("RuntimeRequiredCount = %d, want 3", summary.RuntimeRequiredCount)
	}
	wantModes := []namedCount{
		{Name: "direct_work_items", Count: 1},
		{Name: "normal_enqueue", Count: 2},
		{Name: "worker_path", Count: 1},
	}
	assertNamedCounts(t, "seed modes", summary.SeedModes, wantModes)
	wantFlags := []namedCount{
		{Name: "normal_enqueue", Count: 1},
		{Name: "same_created_date", Count: 1},
		{Name: "worker_path", Count: 3},
		{Name: "workflow_action", Count: 1},
		{Name: "zero_lane_ordinal", Count: 1},
	}
	assertNamedCounts(t, "flags", summary.Flags, wantFlags)
}

func TestCompactSummaryReportDropsHeavyRowsButKeepsAccounting(t *testing.T) {
	report := runReport{
		Repo: "repo",
		ScenarioCatalog: []scenarioDefinition{
			{Name: "clean_800_events_120_lanes", SeedMode: "direct_work_items"},
		},
		ScenarioSummary: summarizeCatalog([]scenarioDefinition{
			{Name: "clean_800_events_120_lanes", SeedMode: "direct_work_items"},
		}),
		Summary: summarizeReport([]gateResult{
			{
				Gate:           gateConfig{Name: "seed_scenario", Database: "nexuscrm_pressure_a"},
				Accepted:       800,
				Materialized:   800,
				Processed:      800,
				LaneOrderingOK: true,
			},
		}),
		Results: []gateResult{
			{
				Gate:           gateConfig{Name: "seed_scenario", Database: "nexuscrm_pressure_a"},
				Accepted:       800,
				Materialized:   800,
				Processed:      800,
				LaneOrderingOK: true,
			},
		},
	}

	compact := compactSummaryReport(report)
	if compact.Repo != "repo" {
		t.Fatalf("Repo = %q, want original metadata", compact.Repo)
	}
	if len(compact.Results) != 0 {
		t.Fatalf("compact results len = %d, want 0", len(compact.Results))
	}
	if len(compact.ScenarioCatalog) != 0 {
		t.Fatalf("compact scenario catalog len = %d, want 0", len(compact.ScenarioCatalog))
	}
	if compact.Summary.ResultCount != 1 || compact.Summary.TotalAccepted != 800 {
		t.Fatalf("compact summary = %+v, want preserved accounting", compact.Summary)
	}
	if compact.ScenarioSummary.ScenarioCount != 1 || compact.ScenarioSummary.SeedableCount != 1 {
		t.Fatalf("compact scenario summary = %+v, want preserved catalog accounting", compact.ScenarioSummary)
	}
}

func TestSortedNamedCountsEmptyAndSorted(t *testing.T) {
	if got := sortedNamedCounts(nil); got != nil {
		t.Fatalf("sortedNamedCounts(nil) = %v, want nil", got)
	}
	got := sortedNamedCounts(map[string]int{"b": 2, "a": 1})
	want := []namedCount{{Name: "a", Count: 1}, {Name: "b", Count: 2}}
	assertNamedCounts(t, "sorted", got, want)
}

func assertNamedCounts(t *testing.T, label string, got, want []namedCount) {
	t.Helper()
	if len(got) != len(want) {
		t.Fatalf("%s len = %d, want %d: got=%v", label, len(got), len(want), got)
	}
	for i := range want {
		if got[i] != want[i] {
			t.Fatalf("%s[%d] = %+v, want %+v", label, i, got[i], want[i])
		}
	}
}
