package dict_test

import (
	"encoding/binary"
	"io"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"testing"

	"chirami/internal/dict"
	"chirami/internal/dict/build"
)

func buildTestDict(t *testing.T, includeNames bool) *dict.Dict {
	t.Helper()

	jmdictFile, err := os.Open("testdata/jmdict.json")
	if err != nil {
		t.Fatalf("open jmdict.json: %v", err)
	}
	defer jmdictFile.Close()

	kanjidicFile, err := os.Open("testdata/kanjidic.json")
	if err != nil {
		t.Fatalf("open kanjidic.json: %v", err)
	}
	defer kanjidicFile.Close()

	accentsFile, err := os.Open("testdata/accents.txt")
	if err != nil {
		t.Fatalf("open accents.txt: %v", err)
	}
	defer accentsFile.Close()

	xmlFile, err := os.Open("testdata/jmdict.xml")
	if err != nil {
		t.Fatalf("open jmdict.xml: %v", err)
	}
	defer xmlFile.Close()

	furiFile, err := os.Open("testdata/furigana.json")
	if err != nil {
		t.Fatalf("open furigana.json: %v", err)
	}
	defer furiFile.Close()

	n5File, err := os.Open("testdata/jlpt_n5.csv")
	if err != nil {
		t.Fatalf("open jlpt_n5: %v", err)
	}
	defer n5File.Close()

	n4File, err := os.Open("testdata/jlpt_n4.csv")
	if err != nil {
		t.Fatalf("open jlpt_n4: %v", err)
	}
	defer n4File.Close()

	n1File, err := os.Open("testdata/jlpt_n1.csv")
	if err != nil {
		t.Fatalf("open jlpt_n1: %v", err)
	}
	defer n1File.Close()

	opts := []build.BuildOption{
		build.WithPriorities(xmlFile),
		build.WithFurigana(furiFile),
		build.WithJLPT(map[string]io.Reader{
			"N5": n5File,
			"N4": n4File,
			"N1": n1File,
		}),
	}

	kradFile, err := os.Open("testdata/kradfile.json")
	if err == nil {
		defer kradFile.Close()
		opts = append(opts, build.WithKradfile(kradFile))
	}
	radkFile, err := os.Open("testdata/radkfile.json")
	if err == nil {
		defer radkFile.Close()
		opts = append(opts, build.WithRadkfile(radkFile))
	}

	if includeNames {
		namesFile, err := os.Open("testdata/jmnedict.json")
		if err != nil {
			t.Fatalf("open jmnedict.json: %v", err)
		}
		defer namesFile.Close()
		opts = append(opts, build.WithNames(namesFile))
	}

	outPath := filepath.Join(t.TempDir(), "testdict.bin")
	err = build.Build(jmdictFile, kanjidicFile, accentsFile, outPath, opts...)
	if err != nil {
		t.Fatalf("build test dict: %v", err)
	}

	d, err := dict.Open(outPath)
	if err != nil {
		t.Fatalf("open test dict: %v", err)
	}
	t.Cleanup(func() {
		d.Close()
	})
	return d
}

func TestLookup(t *testing.T) {
	d := buildTestDict(t, false)

	// 1. Kanji lookup
	taberuKanji := d.Lookup("食べる")
	if len(taberuKanji) == 0 {
		t.Fatalf("Lookup('食べる') returned 0 entries")
	}
	if taberuKanji[0].Kanji[0].Text != "食べる" {
		t.Errorf("got %q, want '食べる'", taberuKanji[0].Kanji[0].Text)
	}

	// 2. Hiragana lookup matching kanji form's reading
	taberuHira := d.Lookup("たべる")
	if len(taberuHira) == 0 {
		t.Fatalf("Lookup('たべる') returned 0 entries")
	}
	if taberuHira[0].ID != taberuKanji[0].ID {
		t.Errorf("Lookup('たべる') ID %d != Lookup('食べる') ID %d", taberuHira[0].ID, taberuKanji[0].ID)
	}

	// 3. Katakana lookup matching hiragana reading
	taberuKata := d.Lookup("タベル")
	if len(taberuKata) == 0 {
		t.Fatalf("Lookup('タベル') returned 0 entries")
	}
	if taberuKata[0].ID != taberuKanji[0].ID {
		t.Errorf("Lookup('タベル') ID %d != Lookup('食べる') ID %d", taberuKata[0].ID, taberuKanji[0].ID)
	}

	// 4. Katakana-only entry
	ramen := d.Lookup("ラーメン")
	if len(ramen) == 0 {
		t.Fatalf("Lookup('ラーメン') returned 0 entries")
	}
	if ramen[0].Kana[0].Text != "ラーメン" {
		t.Errorf("got %q, want 'ラーメン'", ramen[0].Kana[0].Text)
	}

	// Hiragana matching katakana entry
	ramenHira := d.Lookup("らーめん")
	if len(ramenHira) == 0 {
		t.Fatalf("Lookup('らーめん') returned 0 entries")
	}
	if ramenHira[0].ID != ramen[0].ID {
		t.Errorf("Lookup('らーめん') ID %d != Lookup('ラーメン') ID %d", ramenHira[0].ID, ramen[0].ID)
	}

	// 5. Kana-only entry
	kasakasa := d.Lookup("かさかさ")
	if len(kasakasa) == 0 {
		t.Fatalf("Lookup('かさかさ') returned 0 entries")
	}

	// 6. Particle は
	ha := d.Lookup("は")
	if len(ha) == 0 {
		t.Fatalf("Lookup('は') returned 0 entries")
	}

	// 7. Non-existent entry
	nonExistent := d.Lookup("存在しない単語xyz")
	if len(nonExistent) != 0 {
		t.Errorf("expected 0 entries for non-existent word, got %d", len(nonExistent))
	}
}

func TestLookupPriorityRanking(t *testing.T) {
	d := buildTestDict(t, false)

	// "きょう": 今日 (ID 1579110, nf02) must beat 京 (ID 1233320, nf03)
	entries := d.Lookup("きょう")
	if len(entries) < 2 {
		t.Fatalf("expected at least 2 entries for 'きょう', got %d", len(entries))
	}
	if entries[0].Kanji[0].Text != "今日" {
		t.Fatalf("first entry for 'きょう' is %q (ID %d), want '今日' (ID 1579110)", entries[0].Kanji[0].Text, entries[0].ID)
	}
	if entries[1].Kanji[0].Text != "京" {
		t.Errorf("second entry for 'きょう' is %q, want '京'", entries[1].Kanji[0].Text)
	}
}

func TestJLPT(t *testing.T) {
	d := buildTestDict(t, false)

	// 今日 listed in both N5 and N4 -> N5 wins (easiest level)
	kyo := d.Lookup("今日")
	if len(kyo) == 0 {
		t.Fatalf("Lookup('今日') empty")
	}
	if kyo[0].JLPT != "N5" {
		t.Errorf("今日 JLPT = %q, want 'N5'", kyo[0].JLPT)
	}

	// 食べる in N5
	taberu := d.Lookup("食べる")
	if len(taberu) == 0 {
		t.Fatalf("Lookup('食べる') empty")
	}
	if taberu[0].JLPT != "N5" {
		t.Errorf("食べる JLPT = %q, want 'N5'", taberu[0].JLPT)
	}

	// 勉強 in N4
	benkyo := d.Lookup("勉強")
	if len(benkyo) == 0 {
		t.Fatalf("Lookup('勉強') empty")
	}
	if benkyo[0].JLPT != "N4" {
		t.Errorf("勉強 JLPT = %q, want 'N4'", benkyo[0].JLPT)
	}

	// 日本語 in N1
	nihongo := d.Lookup("日本語")
	if len(nihongo) == 0 {
		t.Fatalf("Lookup('日本語') empty")
	}
	if nihongo[0].JLPT != "N1" {
		t.Errorf("日本語 JLPT = %q, want 'N1'", nihongo[0].JLPT)
	}
}

func TestNames(t *testing.T) {
	d := buildTestDict(t, true)

	// "きょう" has JMdict entries for 今日 and 京, and JMnedict name entry for surname "今日" (Kyou)
	entries := d.Lookup("きょう")
	if len(entries) < 3 {
		t.Fatalf("expected at least 3 entries, got %d", len(entries))
	}

	// The first 2 entries must be JMdict entries (ID < 100000000)
	if entries[0].ID >= 100000000 || entries[1].ID >= 100000000 {
		t.Errorf("first entries should be JMdict, got ID %d and %d", entries[0].ID, entries[1].ID)
	}

	// The name entry must be ranked after all JMdict entries
	var nameEntry *dict.Entry
	for i := range entries {
		if entries[i].ID >= 100000000 {
			nameEntry = &entries[i]
			break
		}
	}
	if nameEntry == nil {
		t.Fatalf("no name entry found in Lookup('きょう')")
	}
	if !slices.Contains(nameEntry.Senses[0].Misc, "surname") {
		t.Errorf("name sense misc = %v, want 'surname'", nameEntry.Senses[0].Misc)
	}
	if !slices.Contains(nameEntry.Senses[0].Gloss, "Kyou") {
		t.Errorf("name sense gloss = %v, want 'Kyou'", nameEntry.Senses[0].Gloss)
	}
}

func TestLookupReading(t *testing.T) {
	d := buildTestDict(t, false)

	kyoEntries := d.LookupReading("今日", "きょう")
	if len(kyoEntries) < 2 {
		t.Fatalf("expected at least 2 entries for '今日', got %d", len(kyoEntries))
	}
	if kyoEntries[0].Kana[0].Text != "きょう" {
		t.Errorf("first entry reading = %q, want 'きょう'", kyoEntries[0].Kana[0].Text)
	}

	konnichiEntries := d.LookupReading("今日", "こんにち")
	if len(konnichiEntries) < 2 {
		t.Fatalf("expected at least 2 entries for '今日', got %d", len(konnichiEntries))
	}
	if konnichiEntries[0].Kana[0].Text != "こんにち" {
		t.Errorf("first entry reading = %q, want 'こんにち'", konnichiEntries[0].Kana[0].Text)
	}

	// Kana-only lookup ranks the particle は ahead of kanji homophone 葉 (nf03).
	haEntries := d.LookupReading("は", "は")
	if len(haEntries) < 2 {
		t.Fatalf("expected at least 2 entries for 'は', got %d", len(haEntries))
	}
	if haEntries[0].ID != 2028920 {
		t.Errorf("first entry for 'は' = ID %d (%v), want 2028920 (particle は)", haEntries[0].ID, haEntries[0].Kanji)
	}
}

func TestLongestPrefix(t *testing.T) {
	d := buildTestDict(t, false)

	n, entries := d.LongestPrefix("日本語を勉強", 8)
	expectedLen := len("日本語")
	if n != expectedLen {
		t.Errorf("LongestPrefix byte length = %d, want %d", n, expectedLen)
	}
	if len(entries) == 0 {
		t.Fatalf("LongestPrefix returned 0 entries")
	}
	if entries[0].Kanji[0].Text != "日本語" {
		t.Errorf("got %q, want '日本語'", entries[0].Kanji[0].Text)
	}

	n, entries = d.LongestPrefix("あいうえお", 3)
	if n != 0 || entries != nil {
		t.Errorf("expected 0, nil for no match, got %d, %v", n, entries)
	}
}

func TestKanji(t *testing.T) {
	d := buildTestDict(t, false)

	k, ok := d.Kanji('食')
	if !ok {
		t.Fatalf("Kanji('食') not found")
	}
	if k.Literal != "食" {
		t.Errorf("Literal = %q, want '食'", k.Literal)
	}
	if k.Strokes != 9 {
		t.Errorf("Strokes = %d, want 9", k.Strokes)
	}
	if k.Grade != 2 {
		t.Errorf("Grade = %d, want 2", k.Grade)
	}
	if !slices.Contains(k.Meanings, "eat") {
		t.Errorf("Meanings %v does not contain 'eat'", k.Meanings)
	}
	if !slices.Contains(k.On, "ショク") {
		t.Errorf("On %v does not contain 'ショク'", k.On)
	}
	if !slices.Contains(k.Kun, "た.べる") {
		t.Errorf("Kun %v does not contain 'た.べる'", k.Kun)
	}

	k2, ok := d.Kanji('語')
	if !ok {
		t.Fatalf("Kanji('語') not found")
	}
	if k2.Strokes != 14 {
		t.Errorf("Strokes = %d, want 14", k2.Strokes)
	}

	_, ok = d.Kanji('?')
	if ok {
		t.Errorf("expected Kanji('?') to return false")
	}
}

func TestPitchAttachment(t *testing.T) {
	d := buildTestDict(t, false)

	// 1. 食べる -> downstep 2
	entries := d.Lookup("食べる")
	if len(entries) == 0 {
		t.Fatalf("no entry for 食べる")
	}
	e := entries[0]
	if len(e.Pitch) == 0 {
		t.Fatalf("no pitch attached to 食べる")
	}
	if e.Pitch[0].Reading != "たべる" {
		t.Errorf("pitch reading = %q, want 'たべる'", e.Pitch[0].Reading)
	}
	if !slices.Equal(e.Pitch[0].Downsteps, []int{2}) {
		t.Errorf("pitch downsteps = %v, want [2]", e.Pitch[0].Downsteps)
	}

	// 2. かさかさ -> downsteps 1, 0
	entries = d.Lookup("かさかさ")
	if len(entries) == 0 {
		t.Fatalf("no entry for かさかさ")
	}
	e = entries[0]
	if len(e.Pitch) == 0 {
		t.Fatalf("no pitch attached to かさかさ")
	}
	if !slices.Equal(e.Pitch[0].Downsteps, []int{1, 0}) {
		t.Errorf("pitch downsteps = %v, want [1, 0]", e.Pitch[0].Downsteps)
	}

	// 3. ラーメン -> downstep 1
	entries = d.Lookup("ラーメン")
	if len(entries) == 0 {
		t.Fatalf("no entry for ラーメン")
	}
	e = entries[0]
	if len(e.Pitch) == 0 {
		t.Fatalf("no pitch attached to ラーメン")
	}
	if !slices.Equal(e.Pitch[0].Downsteps, []int{1}) {
		t.Errorf("pitch downsteps = %v, want [1]", e.Pitch[0].Downsteps)
	}
}

func TestTagLabel(t *testing.T) {
	d := buildTestDict(t, false)

	if label := d.TagLabel("v1"); label != "Ichidan verb" {
		t.Errorf("d.TagLabel('v1') = %q, want 'Ichidan verb'", label)
	}
	if label := d.TagLabel("vt"); label != "transitive verb" {
		t.Errorf("d.TagLabel('vt') = %q, want 'transitive verb'", label)
	}
	if label := d.TagLabel("n"); label != "noun (common) (futsuumeishi)" {
		t.Errorf("d.TagLabel('n') = %q, want 'noun (common) (futsuumeishi)'", label)
	}
	if label := d.TagLabel("unknown_tag_xyz"); label != "unknown_tag_xyz" {
		t.Errorf("d.TagLabel('unknown_tag_xyz') = %q, want 'unknown_tag_xyz'", label)
	}
}

func TestFurigana(t *testing.T) {
	d := buildTestDict(t, false)

	rubies, ok := d.Furigana("日本語", "にほんご")
	if !ok {
		t.Fatalf("Furigana('日本語', 'にほんご') not found")
	}
	want := []dict.Ruby{
		{Text: "日", Ruby: "に"},
		{Text: "本", Ruby: "ほん"},
		{Text: "語", Ruby: "ご"},
	}
	if !slices.Equal(rubies, want) {
		t.Errorf("Furigana('日本語', 'にほんご') = %+v, want %+v", rubies, want)
	}

	rubies, ok = d.Furigana("今日", "きょう")
	if !ok {
		t.Fatalf("Furigana('今日', 'きょう') not found")
	}
	wantKyo := []dict.Ruby{
		{Text: "今日", Ruby: "きょう"},
	}
	if !slices.Equal(rubies, wantKyo) {
		t.Errorf("Furigana('今日', 'きょう') = %+v, want %+v", rubies, wantKyo)
	}

	rubies, ok = d.Furigana("食べる", "たべる")
	if !ok {
		t.Fatalf("Furigana('食べる', 'たべる') not found")
	}
	wantTaberu := []dict.Ruby{
		{Text: "食", Ruby: "た"},
		{Text: "べる", Ruby: ""},
	}
	if !slices.Equal(rubies, wantTaberu) {
		t.Errorf("Furigana('食べる', 'たべる') = %+v, want %+v", rubies, wantTaberu)
	}

	rubies, ok = d.Furigana("大人", "おとな")
	if !ok {
		t.Fatalf("Furigana('大人', 'おとな') not found")
	}
	wantOtona := []dict.Ruby{
		{Text: "大人", Ruby: "おとな"},
	}
	if !slices.Equal(rubies, wantOtona) {
		t.Errorf("Furigana('大人', 'おとな') = %+v, want %+v", rubies, wantOtona)
	}

	rubies, ok = d.Furigana("食べる", "タベル")
	if !ok {
		t.Fatalf("Furigana('食べる', 'タベル') not found")
	}
	if !slices.Equal(rubies, wantTaberu) {
		t.Errorf("Furigana('食べる', 'タベル') = %+v, want %+v", rubies, wantTaberu)
	}

	_, ok = d.Furigana("存在しない", "そんざいしない")
	if ok {
		t.Errorf("expected false for non-existent furigana")
	}
}

func TestComponents(t *testing.T) {
	d := buildTestDict(t, false)

	comps := d.Components('食')
	if !slices.Equal(comps, []rune{'食'}) {
		t.Errorf("Components('食') = %v, want ['食']", comps)
	}

	comps = d.Components('語')
	want := []rune{'言', '口', '五'}
	if !slices.Equal(comps, want) {
		t.Errorf("Components('語') = %v, want %v", comps, want)
	}

	comps = d.Components('良')
	if !slices.Equal(comps, []rune{'艮'}) {
		t.Errorf("Components('良') = %v, want ['艮']", comps)
	}

	// Verify display component forms: 化->⺅, 艾->艹, 汁->氵, 込->辶, ｜->丨, ノ->丿
	if comps = d.Components('休'); !slices.Equal(comps, []rune{'⺅', '木'}) {
		t.Errorf("Components('休') = %v (%c), want [⺅, 木]", comps, comps)
	}
	if comps = d.Components('花'); !slices.Equal(comps, []rune{'⺅', '匕', '艹'}) {
		t.Errorf("Components('花') = %v (%c), want [⺅, 匕, 艹]", comps, comps)
	}
	if comps = d.Components('働'); !slices.Equal(comps, []rune{'丨', '一', '⺅', '力', '日', '丿'}) {
		t.Errorf("Components('働') = %v (%c), want [丨, 一, ⺅, 力, 日, 丿]", comps, comps)
	}

	if comps = d.Components('？'); comps != nil {
		t.Errorf("Components('？') = %v, want nil", comps)
	}
}

func TestKanjiWith(t *testing.T) {
	d := buildTestDict(t, false)

	// In testdata/radkfile.json, 艮 has ["良", "銀", "限"]
	// In testdata/kanjidic.json:
	// 銀: freq 395
	// 限: freq 405
	// 良: freq 501
	// Order must be: 銀, 限, 良
	res := d.KanjiWith('艮', 10)
	want := []rune{'銀', '限', '良'}
	if !slices.Equal(res, want) {
		t.Errorf("KanjiWith('艮', 10) = %v (%c %c %c), want %v (%c %c %c)",
			res, res[0], res[1], res[2], want, want[0], want[1], want[2])
	}

	// KanjiWith('良', 8) resolves 良 -> single component 艮 -> returns [銀, 限, 良]
	res = d.KanjiWith('良', 8)
	if !slices.Equal(res, want) {
		t.Errorf("KanjiWith('良', 8) = %v, want %v", res, want)
	}

	// Both display form and placeholder form must work:
	// ⺅ and 化
	k1 := d.KanjiWith('⺅', 10)
	k2 := d.KanjiWith('化', 10)
	if len(k1) == 0 || !slices.Equal(k1, k2) {
		t.Errorf("KanjiWith('⺅') != KanjiWith('化'): %v vs %v", k1, k2)
	}

	// 艹 and 艾
	if k1, k2 := d.KanjiWith('艹', 10), d.KanjiWith('艾', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
		t.Errorf("KanjiWith('艹') != KanjiWith('艾'): %v vs %v", k1, k2)
	}

	// 氵 and 汁
	if k1, k2 := d.KanjiWith('氵', 10), d.KanjiWith('汁', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
		t.Errorf("KanjiWith('氵') != KanjiWith('汁'): %v vs %v", k1, k2)
	}

	// 辶 and 込
	if k1, k2 := d.KanjiWith('辶', 10), d.KanjiWith('込', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
		t.Errorf("KanjiWith('辶') != KanjiWith('込'): %v vs %v", k1, k2)
	}

	// 丨 and ｜
	if k1, k2 := d.KanjiWith('丨', 10), d.KanjiWith('｜', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
		t.Errorf("KanjiWith('丨') != KanjiWith('｜'): %v vs %v", k1, k2)
	}

	// 丿 and ノ
	if k1, k2 := d.KanjiWith('丿', 10), d.KanjiWith('ノ', 10); len(k1) == 0 || !slices.Equal(k1, k2) {
		t.Errorf("KanjiWith('丿') != KanjiWith('ノ'): %v vs %v", k1, k2)
	}

	// Max limit
	res = d.KanjiWith('艮', 2)
	if !slices.Equal(res, []rune{'銀', '限'}) {
		t.Errorf("KanjiWith('艮', 2) = %v, want ['銀', '限']", res)
	}

	// Non-positive max returns nil
	if res = d.KanjiWith('艮', 0); res != nil {
		t.Errorf("KanjiWith('艮', 0) = %v, want nil", res)
	}
	if res = d.KanjiWith('艮', -1); res != nil {
		t.Errorf("KanjiWith('艮', -1) = %v, want nil", res)
	}
}

func TestComponentName(t *testing.T) {
	d := buildTestDict(t, false)

	cases := []struct {
		r    rune
		want string
	}{
		{'宀', "roof"},
		{'亠', "lid"},
		{'冖', "cover"},
		{'儿', "legs"},
		{'丨', "line"},
		{'丿', "slash"},
		{'匕', "spoon"},
		{'艹', "grass"},
		{'⺅', "person"},
		{'忄', "heart"},
		{'氵', "water"},
		{'辶', "walk"},
		{'食', "eat"},
		{'⺡', "water"},
		{'⺉', "knife"},
	}

	for _, tc := range cases {
		got := d.ComponentName(tc.r)
		if got != tc.want {
			t.Errorf("ComponentName(%c / U+%04X) = %q, want %q", tc.r, tc.r, got, tc.want)
		}
	}
}

func TestKanjiComponentsAndNamesTable(t *testing.T) {
	d := buildTestDict(t, false)

	table := []struct {
		kanji rune
		comps []rune
		names []string
	}{
		{'休', []rune{'⺅', '木'}, []string{"person", "tree"}},
		{'花', []rune{'⺅', '匕', '艹'}, []string{"person", "spoon", "grass"}},
		{'働', []rune{'丨', '一', '⺅', '力', '日', '丿'}, []string{"line", "one", "person", "power", "sun", "slash"}},
		{'語', []rune{'言', '口', '五'}, []string{"words", "mouth", "five"}},
		{'読', []rune{'言', '士', '儿', '冖'}, []string{"words", "scholar", "legs", "cover"}},
		{'海', []rune{'氵', '母', '毋', '乞'}, []string{"water", "mother", "do not", "beg"}},
		{'道', []rune{'辶', '自', '丷', '首'}, []string{"walk", "oneself", "horns", "head"}},
		{'家', []rune{'宀', '豕'}, []string{"roof", "pig"}},
		{'想', []rune{'心', '木', '目'}, []string{"heart", "tree", "eye"}},
		{'校', []rune{'父', '木', '亠'}, []string{"father", "tree", "lid"}},
	}

	for _, tc := range table {
		gotComps := d.Components(tc.kanji)
		if !slices.Equal(gotComps, tc.comps) {
			t.Errorf("Components(%c) = %v (%c), want %v (%c)", tc.kanji, gotComps, gotComps, tc.comps, tc.comps)
		}
		gotNames := make([]string, len(gotComps))
		for i, c := range gotComps {
			gotNames[i] = d.ComponentName(c)
		}
		if !slices.Equal(gotNames, tc.names) {
			t.Errorf("ComponentNames for %c = %v, want %v", tc.kanji, gotNames, tc.names)
		}
	}
}

func TestWordsWith(t *testing.T) {
	d := buildTestDict(t, false)

	words := d.WordsWith('食', 10)
	if len(words) < 2 {
		t.Fatalf("WordsWith('食', 10) returned %d entries, want at least 2", len(words))
	}

	// First should be 食べる (common, priority nf02)
	if words[0].Kanji[0].Text != "食べる" {
		t.Errorf("words[0] = %q, want '食べる'", words[0].Kanji[0].Text)
	}

	// Second should be 食事
	if words[1].Kanji[0].Text != "食事" {
		t.Errorf("words[1] = %q, want '食事'", words[1].Kanji[0].Text)
	}

	// Verify that 食偽 (search-only sK form) was excluded
	for _, w := range words {
		for _, k := range w.Kanji {
			if k.Text == "食偽" {
				t.Errorf("WordsWith('食') included search-only word '食偽'")
			}
		}
	}

	// Max limit
	wordsLimited := d.WordsWith('食', 1)
	if len(wordsLimited) != 1 {
		t.Errorf("WordsWith('食', 1) length = %d, want 1", len(wordsLimited))
	}

	// Non-positive max returns nil
	if w := d.WordsWith('食', 0); w != nil {
		t.Errorf("WordsWith('食', 0) = %v, want nil", w)
	}

	// Non-existent kanji
	if w := d.WordsWith('？', 10); w != nil {
		t.Errorf("WordsWith('？', 10) = %v, want nil", w)
	}
}

func TestOpenRejectsOldVersion(t *testing.T) {
	// A file with version 1
	var buf [160]byte
	copy(buf[0:8], dict.MagicString)
	binary.LittleEndian.PutUint32(buf[8:12], 1) // version 1
	tmpPath := filepath.Join(t.TempDir(), "olddict.bin")
	if err := os.WriteFile(tmpPath, buf[:], 0644); err != nil {
		t.Fatal(err)
	}
	_, err := dict.Open(tmpPath)
	if err == nil {
		t.Fatal("expected error opening old version file, got nil")
	}
	if !strings.Contains(err.Error(), "run chirami setup") {
		t.Errorf("error %q does not contain 'run chirami setup'", err.Error())
	}
}
