package pipeline

import (
	"context"
	"testing"
)

// quality_test.go pins the per-run quality-report (D39 слой 5, H5): the claim-1 structural KPI and
// the aggregation of the already-stored deterministic signals. Pure read-only projection.

// TestNarrativeStructure pins the claim-1 KPI: narrative paragraphs (non-dialogue lines) and their
// sentence count. A choppy text (one sentence per line) reads ≈1.0; merged prose reads higher;
// dialogue turns are excluded.
func TestNarrativeStructure(t *testing.T) {
	cases := []struct {
		name     string
		text     string
		wantSent int
		wantPara int
	}{
		// Choppy: 3 one-sentence narrative lines → 3 sentences / 3 paragraphs = 1.0 (претензия-1).
		{"choppy", "Он вышел.\nНебо серело.\nОн вздохнул.", 3, 3},
		// Merged: one paragraph, 3 sentences → 3 / 1 = 3.0 (the reflow fix).
		{"merged", "Он вышел за дверь. Небо серело. Он вздохнул, глядя вдаль.", 3, 1},
		// Dialogue turns excluded; only the narrative line counts.
		{"dialogue-excluded", "Дядя нахмурился.\n— Время летит. Садись, — сказал он.\n— Спасибо.", 1, 1},
		// Blank lines skipped; a two-sentence narrative paragraph plus a one-sentence one.
		{"mixed", "Первый абзац. Ещё предложение.\n\nВторой абзац.", 3, 2},
	}
	for _, c := range cases {
		sent, para := narrativeStructure(c.text)
		if sent != c.wantSent || para != c.wantPara {
			t.Errorf("%s: narrativeStructure = (%d sent, %d para), want (%d, %d)", c.name, sent, para, c.wantSent, c.wantPara)
		}
	}
}

// TestQualityReportAggregates runs a real 1-chunk book and asserts the read-only quality projection
// aggregates the structural KPI from the exported final text plus the stored signals.
func TestQualityReportAggregates(t *testing.T) {
	rec := &reqRec{}
	// The editor (final stage) emits a KNOWN 3-line final: 2 narrative paragraphs (3 sentences total)
	// + 1 dialogue turn (excluded) → mean 1.5 sentences/narrative-paragraph.
	editorFinal := "Судзуки шёл по коридору. Он думал о вчерашней лекции.\nБиблиотека была тихой.\n— Доброе утро, — сказал он."
	srv := newJSONProvider(rec, func(body string) (string, string) {
		if isEditBody(body) {
			return editorFinal, "stop"
		}
		return "Судзуки шёл по коридору.", "stop"
	})
	defer srv.Close()
	bookPath := setupProjectOpts(t, srv.URL, projectOpts{})
	ctx := context.Background()

	r := newRunner(t, bookPath)
	defer r.Close()
	if _, err := r.TranslateBook(ctx); err != nil {
		t.Fatal(err)
	}

	q, err := r.QualityReport()
	if err != nil {
		t.Fatalf("QualityReport: %v", err)
	}
	if q.TotalChunks != 1 || q.TextChunks != 1 || q.ProcessedChunks != 1 {
		t.Fatalf("chunk counts: total=%d processed=%d text=%d, want 1/1/1", q.TotalChunks, q.ProcessedChunks, q.TextChunks)
	}
	// Rates are bounded [0,1] over ProcessedChunks (the disjoint-set bug fix).
	if q.EchoRate < 0 || q.EchoRate > 1 || q.CosmeticStripRate < 0 || q.CosmeticStripRate > 1 {
		t.Errorf("rates must be within [0,1], got echo=%.3f cosmetic=%.3f", q.EchoRate, q.CosmeticStripRate)
	}
	if q.NarrativeSentences != 3 || q.NarrativeParagraphs != 2 {
		t.Errorf("structural KPI = %d sent / %d para, want 3/2", q.NarrativeSentences, q.NarrativeParagraphs)
	}
	if q.MeanSentPerNarrPara != 1.5 {
		t.Errorf("mean sentences/narrative-paragraph = %.3f, want 1.5", q.MeanSentPerNarrPara)
	}
	// A clean run: no glossary misses, no echo, no CJK strip, no trust-gated, no dialogue-dash issue.
	if q.GlossaryMisses != 0 || q.EchoChunks != 0 || q.CosmeticStripChunks != 0 || q.TrustGated != 0 || q.DialogueDashFlags != 0 {
		t.Errorf("clean run must have zero defect signals, got %+v", q)
	}
	if len(q.Chunks) != 1 || q.Chunks[0].NarrativeParagraphs != 2 {
		t.Errorf("per-chunk row missing/wrong: %+v", q.Chunks)
	}
}

// TestQualityReportExcludesGateWithheld pins F5 (D39.4): a chunk the glossary post-check GATE withheld
// (flagged glossary_miss, empty export) must NOT be counted in TextChunks / the structural KPI — its
// text never ships, so counting it would diverge from `tmctl export`/`translate`.
func TestQualityReportExcludesGateWithheld(t *testing.T) {
	srv := newJSONProvider(&reqRec{}, func(body string) (string, string) {
		if isEditBody(body) {
			return "ОТРЕДАКТИРОВАННЫЙ.", "stop" // final drops the approved name → confirmed miss
		}
		return "Некто пошёл в библиотеку.", "stop"
	})
	defer srv.Close()
	bookPath := setupProjectOpts(t, srv.URL, projectOpts{regenerate: 1, source: suzukiSource, glossarySeed: suzukiSeed, postcheckGate: true})

	r := newRunner(t, bookPath)
	defer r.Close()
	if _, err := r.TranslateBook(context.Background()); err != nil {
		t.Fatal(err)
	}
	q, err := r.QualityReport()
	if err != nil {
		t.Fatalf("QualityReport: %v", err)
	}
	// The single chunk is gate-withheld → processed but NOT text-bearing.
	if q.ProcessedChunks != 1 {
		t.Fatalf("the withheld chunk still reached the final stage: processed=%d", q.ProcessedChunks)
	}
	if q.TextChunks != 0 || q.NarrativeSentences != 0 || q.NarrativeParagraphs != 0 {
		t.Errorf("gate-withheld chunk must be excluded from the KPI, got text=%d sent=%d para=%d",
			q.TextChunks, q.NarrativeSentences, q.NarrativeParagraphs)
	}
	// It IS visible as a glossary miss (the observability signal is kept).
	if q.GlossaryMisses == 0 {
		t.Errorf("the confirmed miss must still be reported: %+v", q)
	}
}
