package pipeline

import (
	"context"
	"testing"
)

// --- dialogue-dash --------------------------------------------------------------

func TestLintDialogueDash(t *testing.T) {
	cases := []struct {
		name string
		text string
		want int
	}{
		{"em-dash correct", "— Привет, — сказал он.\n— И тебе.", 0},
		{"hyphen dialogue", "- Привет, — сказал он.\n- Пока.", 2},
		{"en-dash dialogue", "– Привет, мир.", 1},
		{"straight quote dialogue", "\"Привет\", — сказал он.", 1},
		{"hyphenated word not dialogue", "Он сделал это как-то по-своему.\n-то не считается.", 0},
		{"list-like dash not dialogue", "-1 балл\n-2 очка", 0},
		{"mixing em-dash and chevrons", "— Реплика первая.\n«Реплика вторая», — подумал он.", 1},
		{"chevron only no mixing", "«Цитата из книги».\nОбычный текст.", 0},
		{"chevron term in prose not flagged", "Это была «Война и мир».", 0},
		// D20.4 FP1: a chevron CITATION/definition at line start (« … » — prose, NO attribution comma)
		// mixed with em-dash dialogue must NOT count as style mixing.
		{"chevron citation at line start not mixing", "— Реплика.\n«Мастер Гу» — так его называли.", 0},
		{"chevron title alone at line start not mixing", "— Реплика.\n«Война и мир» стояла на полке.", 0},
		// Self-review: an em-dash + space + digit is a scene break, NOT dialogue — must not count.
		{"em-dash scene break not dialogue", "— 15 минут спустя он вернулся.", 0},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			n, _ := lintDialogueDash(c.text)
			if n != c.want {
				t.Errorf("lintDialogueDash(%q) = %d, want %d", c.text, n, c.want)
			}
		})
	}
}

// --- yofikator ------------------------------------------------------------------

func TestLintYofikation(t *testing.T) {
	cases := []struct {
		name   string
		text   string
		policy string
		want   int
	}{
		{"name both spellings auto", "Пётр вошёл. Потом Петр ушёл.", "auto", 1},
		{"name consistent auto", "Пётр вошёл и Пётр ушёл.", "auto", 0},
		{"homograph vse not flagged", "Все ушли. И всё пропало.", "auto", 0},
		{"no yo at all auto", "Все ели мед.", "auto", 0},
		{"all-e flags any yo", "Пётр ел мёд.", "all-e", 2},
		{"all-e clean", "Петр ел мед.", "all-e", 0},
		{"artem both spellings", "Артём и Артем — один человек.", "auto", 1},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			n, _ := lintYofikation(c.text, c.policy)
			if n != c.want {
				t.Errorf("lintYofikation(%q, %q) = %d, want %d", c.text, c.policy, n, c.want)
			}
		})
	}
}

// --- translit interjections -----------------------------------------------------

func TestLintTranslitInterjections(t *testing.T) {
	empty := map[string]bool{}
	cases := []struct {
		name  string
		text  string
		allow map[string]bool
		want  int
	}{
		{"ara-ara and hmf", "Ара-ара, — протянула она. Хмф!", empty, 2},
		{"nani", "Нани?! — вскрикнул он.", empty, 1},
		{"inside word not flagged", "У него сильный характер и добрая душа.", empty, 0},
		{"allowlisted maa", "Маа, что же делать.", map[string]bool{"маа": true}, 0},
		{"non-allowlisted maa fires", "Маа, что же делать.", empty, 1},
		// Self-review majors: «ара» (macaw noun / name) and «уму» (dative of «ум») were removed from
		// the blocklist — they must NOT flag ordinary prose.
		{"bare ara is a macaw not flagged", "Красный ара сидел на ветке.", empty, 0},
		{"umu dative not flagged", "Это уму непостижимо.", empty, 0},
		{"clean prose", "Он молча кивнул и ушёл.", empty, 0},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			n, _ := lintTranslitInterjections(c.text, c.allow)
			if n != c.want {
				t.Errorf("lintTranslitInterjections(%q) = %d, want %d", c.text, n, c.want)
			}
		})
	}
}

// --- CJK number parser ----------------------------------------------------------

func TestParseCJKNumber(t *testing.T) {
	cases := []struct {
		s    string
		want int64
		ok   bool
	}{
		{"三万", 30000, true},
		{"三千万", 30000000, true},
		{"一億二千万", 120000000, true},
		{"十万", 100000, true},
		{"30万", 300000, true},
		{"五億", 500000000, true},
		{"一兆", 1000000000000, true},
		{"三", 0, false},   // no big marker
		{"三千", 0, false},  // no big marker (千 is a small unit)
		{"", 0, false},
	}
	for _, c := range cases {
		v, ok := parseCJKNumber(c.s)
		if ok != c.ok || (ok && v != c.want) {
			t.Errorf("parseCJKNumber(%q) = (%d,%v), want (%d,%v)", c.s, v, ok, c.want, c.ok)
		}
	}
}

// --- number magnitude gate ------------------------------------------------------

func TestLintNumberMagnitude(t *testing.T) {
	cases := []struct {
		name   string
		source string
		final  string
		want   int
	}{
		{"30k mistranslated as millions", "他有三万石粮食。", "У него было три миллиона мешков зерна.", 1},
		{"30k correct as tens of thousands", "他有三万石粮食。", "У него было тридцать тысяч мешков зерна.", 0},
		{"300 million correct", "损失三億元。", "Убытки составили триста миллионов юаней.", 0},
		{"hundred million as thousand wrong", "军队三億人。", "Армия в три тысячи человек.", 1},
		{"wan as myriad not a number", "世间万物皆有灵。", "Все вещи в мире имеют душу.", 0},
		// Self-review major: CJK idioms carrying 万/億 but no digit coefficient are NOT magnitudes.
		{"wan-yi idiom", "万一出事了怎么办。", "Что если что-то случится?", 0},
		{"qianwan idiom", "你千万要小心。", "Ты обязательно будь осторожен.", 0},
		{"wanfen idiom", "他万分感激。", "Он был крайне благодарен.", 0},
		{"no magnitude at all", "他走进房间。", "Он вошёл в комнату.", 0},
		{"arabic output matches", "三万里。", "30000 ли.", 0},
		{"no output number silent", "三万大军。", "Огромная армия.", 0},
		// D20.4 FP3: a magnitude rephrased as prose + a STRAY unrelated output integer (a year) must
		// NOT flag — the year is not evidence the 万 was mistranslated (bare integers only confirm).
		{"rephrased magnitude plus stray year not flagged", "三万大军压境，时值1990年。", "Несметное войско подступило; шёл 1990 год.", 0},
		// D20.4 FP2: 万 inside a NAME (digit-before-万 makes it parse as a magnitude) + an unrelated
		// output number must NOT flag.
		{"wan in a name plus stray year not flagged", "他叫赵三万，生于1985年。", "Его звали Чжао Саньвань, родился в 1985 году.", 0},
	}
	for _, c := range cases {
		t.Run(c.name, func(t *testing.T) {
			n, _ := lintNumberMagnitude(c.source, c.final)
			if n != c.want {
				t.Errorf("lintNumberMagnitude(%q → %q) = %d, want %d", c.source, c.final, n, c.want)
			}
		})
	}
}

// --- combined ------------------------------------------------------------------

func TestRunCheapGatesCombined(t *testing.T) {
	src := "他有三万石粮食。"
	final := "- Ара-ара, — у Пётр было три миллиона мешков. Потом Петр ушёл."
	cfg := cheapGateConfig{yoPolicy: "auto", allowlist: map[string]bool{}}
	r := runCheapGates(src, final, cfg)
	if r.DialogueDash == 0 {
		t.Error("expected a dialogue-dash flag (hyphen-led line)")
	}
	if r.TranslitInterj == 0 {
		t.Error("expected a translit-interjection flag (ара-ара)")
	}
	if r.YoInconsistent == 0 {
		t.Error("expected a yofikation flag (Пётр/Петр)")
	}
	if r.NumberMagnitude == 0 {
		t.Error("expected a number-magnitude flag (三万 → миллиона)")
	}
	if r.total() < 4 {
		t.Errorf("expected all four gates to fire, total=%d detail=%v", r.total(), r.Detail)
	}
}

// TestCheapGatesSurfaceInStatus is the end-to-end wiring test: a chunk whose FINAL text has a style
// issue (a hyphen-led dialogue line) records a style flag in retrieval_state and surfaces it in the
// status projection — WITHOUT flagging the chunk (observability, never a disposition).
func TestCheapGatesSurfaceInStatus(t *testing.T) {
	rec := &reqRec{}
	srv := newJSONProvider(rec, func(body string) (string, string) {
		if isEditBody(body) {
			return "- Привет, — сказал он.", "stop" // hyphen dialogue → dialogue-dash flag
		}
		return "ЧЕРНОВИК ПЕРЕВОДА", "stop"
	})
	defer srv.Close()
	bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: "テキスト。", regenerate: 0})
	ctx := context.Background()

	r := newRunner(t, bookPath)
	defer r.Close()
	res, err := r.TranslateBook(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if res.Flagged != 0 {
		t.Fatalf("a style flag must not flag the chunk (observability only), got flagged=%d", res.Flagged)
	}
	rs, err := r.Store.GetRetrievalState("test-book", 1, 0)
	if err != nil || rs == nil {
		t.Fatalf("no retrieval state persisted: %v", err)
	}
	if rs.NStyleFlags == 0 {
		t.Fatalf("style flag not persisted in retrieval_state: %+v", rs)
	}
	rep, err := r.Status(ctx)
	if err != nil {
		t.Fatal(err)
	}
	if rep.StyleFlags == 0 {
		t.Fatal("status must surface the book-wide style-flag count")
	}
	if rep.Done != 1 || rep.Flagged != 0 {
		t.Fatalf("the chunk must be done, not flagged: done=%d flagged=%d", rep.Done, rep.Flagged)
	}
}
