package store

import "testing"

func gEntry(src, dst, status string, aliases ...string) GlossaryEntry {
	e := GlossaryEntry{BookID: "book", Src: src, Dst: dst, Status: status, Source: "seed"}
	for _, a := range aliases {
		e.Aliases = append(e.Aliases, GlossaryAlias{Alias: a, AliasType: "прозвище"})
	}
	return e
}

func TestReplaceGlossaryIdempotentAndScoped(t *testing.T) {
	s, _ := openTemp(t)
	entries := []GlossaryEntry{
		gEntry("阿Q", "А-кью", "approved"),
		gEntry("四叔", "Четвёртый дядюшка", "approved", "鲁四老爷"),
	}
	if err := s.ReplaceGlossary("book", entries); err != nil {
		t.Fatal(err)
	}
	// Another book is untouched.
	if err := s.ReplaceGlossary("other", []GlossaryEntry{{BookID: "other", Src: "x", Dst: "икс", Status: "approved"}}); err != nil {
		t.Fatal(err)
	}

	got1, err := s.GlossaryForBook("book")
	if err != nil {
		t.Fatal(err)
	}
	if len(got1) != 2 {
		t.Fatalf("want 2 entries, got %d", len(got1))
	}
	// Re-replacing the identical set yields the identical content (idempotent).
	if err := s.ReplaceGlossary("book", entries); err != nil {
		t.Fatal(err)
	}
	got2, err := s.GlossaryForBook("book")
	if err != nil {
		t.Fatal(err)
	}
	if len(got2) != 2 {
		t.Fatalf("re-replace changed count: %d", len(got2))
	}
	// The alias survived the replace and attached to the right term.
	var sishu *GlossaryEntry
	for i := range got2 {
		if got2[i].Src == "四叔" {
			sishu = &got2[i]
		}
	}
	if sishu == nil || len(sishu.Aliases) != 1 || sishu.Aliases[0].Alias != "鲁四老爷" {
		t.Fatalf("alias not preserved: %+v", sishu)
	}
	// other book still has its one row.
	other, _ := s.GlossaryForBook("other")
	if len(other) != 1 {
		t.Fatalf("scope leak: other book has %d rows", len(other))
	}
}

func TestReplaceGlossaryRemovesDroppedRows(t *testing.T) {
	s, _ := openTemp(t)
	if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("阿Q", "А-кью", "approved"), gEntry("四叔", "дядюшка", "approved")}); err != nil {
		t.Fatal(err)
	}
	// Drop one term: full replace must make it disappear (no phantom).
	if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("阿Q", "А-кью", "approved")}); err != nil {
		t.Fatal(err)
	}
	got, _ := s.GlossaryForBook("book")
	if len(got) != 1 || got[0].Src != "阿Q" {
		t.Fatalf("dropped term still present: %+v", got)
	}
}

func TestGlossaryRevisionLogged(t *testing.T) {
	s, _ := openTemp(t)
	if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("王胡", "Бородатый Ван", "approved")}); err != nil {
		t.Fatal(err)
	}
	// Change the approved rendering: a B1 editorial revision must be journaled.
	if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("王胡", "Ван Ху", "approved")}); err != nil {
		t.Fatal(err)
	}
	revs, err := s.GlossaryRevisionsForBook("book")
	if err != nil {
		t.Fatal(err)
	}
	if len(revs) != 1 || revs[0].OldDst != "Бородатый Ван" || revs[0].NewDst != "Ван Ху" {
		t.Fatalf("revision not logged correctly: %+v", revs)
	}
	// Re-replacing identical does NOT add a second revision.
	if err := s.ReplaceGlossary("book", []GlossaryEntry{gEntry("王胡", "Ван Ху", "approved")}); err != nil {
		t.Fatal(err)
	}
	revs, _ = s.GlossaryRevisionsForBook("book")
	if len(revs) != 1 {
		t.Fatalf("idempotent replace added a spurious revision: %d", len(revs))
	}
}

func TestUniqueSrcSenseWindow(t *testing.T) {
	s, _ := openTemp(t)
	// Same src+sense+window twice → UNIQUE violation (B2).
	dup := []GlossaryEntry{gEntry("道", "путь", "approved"), gEntry("道", "дао", "approved")}
	if err := s.ReplaceGlossary("book", dup); err == nil {
		t.Fatal("expected UNIQUE(src,sense,window) violation for duplicate src+sense+window")
	}
	// Same src, different SENSE → allowed (polysemy, A3).
	ok := []GlossaryEntry{
		{BookID: "book", Src: "道", Dst: "путь", Sense: "way", Status: "approved"},
		{BookID: "book", Src: "道", Dst: "дао", Sense: "dao", Status: "approved"},
	}
	if err := s.ReplaceGlossary("book", ok); err != nil {
		t.Fatalf("distinct senses should be allowed: %v", err)
	}
	got, _ := s.GlossaryForBook("book")
	if len(got) != 2 {
		t.Fatalf("want 2 polysemy rows, got %d", len(got))
	}
}

func TestRetrievalStateUpsert(t *testing.T) {
	s, _ := openTemp(t)
	rs := RetrievalState{BookID: "book", Chapter: 1, ChunkIdx: 0, SnapshotID: "snap",
		NExactHits: 3, NAmbiguousFlagged: 1, NSpoilerBlocked: 1, NPostcheckMiss: 1,
		PostcheckDetail: `[{"src":"阿Q","dst":"А-кью"}]`, InjectedIDs: `["ah_q"]`}
	if err := s.UpsertRetrievalState(rs); err != nil {
		t.Fatal(err)
	}
	got, err := s.GetRetrievalState("book", 1, 0)
	if err != nil || got == nil {
		t.Fatalf("get: %v %v", got, err)
	}
	if got.NExactHits != 3 || got.NSpoilerBlocked != 1 || got.NPostcheckMiss != 1 {
		t.Fatalf("roundtrip mismatch: %+v", got)
	}
	// Overwrite (resolve semantics on resume): new counts win.
	rs.NExactHits = 5
	rs.NPostcheckMiss = 0
	if err := s.UpsertRetrievalState(rs); err != nil {
		t.Fatal(err)
	}
	got, _ = s.GetRetrievalState("book", 1, 0)
	if got.NExactHits != 5 || got.NPostcheckMiss != 0 {
		t.Fatalf("overwrite failed: %+v", got)
	}
	all, _ := s.RetrievalStatesForBook("book")
	if len(all) != 1 {
		t.Fatalf("want 1 retrieval-state row, got %d", len(all))
	}
}
