package rag

import "testing"

// TestBleve_ChineseSearch 钉死中文全文检索：CJK 分词器让"星云一号的总设计师"能命中含
// "星云一号""总设计师"的中文块。修复前默认标准分词器把整段中文当一个 token → 永远 0 命中。
func TestBleve_ChineseSearch(t *testing.T) {
	t.Setenv("BLEVE_PATH", t.TempDir()+"/bleve") // 隔离落盘索引，测试互不污染
	b := openBleve()
	defer b.close()
	if !b.ready() {
		t.Skip("bleve 不可用")
	}
	const kb = "k1"
	if err := b.index(kb, "d1", []string{
		"星云一号卫星于2023年由长征七号发射，项目总设计师是李明华。",
		"今天天气不错，适合出门散步。",
	}); err != nil {
		t.Fatal(err)
	}
	hits := b.search(kb, "星云一号的总设计师是谁", 5)
	if len(hits) == 0 {
		t.Fatal("中文全文检索应命中（CJK 分词），got 0 —— 分词器回归了？")
	}
	if !contains(hits[0].Text, "总设计师") {
		t.Fatalf("最相关块应含'总设计师'，got %q", hits[0].Text)
	}
}

// TestQueryNgrams 钉死查询 n-gram：含"星云一号"等子串，能与更长实体名"星云一号卫星"双向匹配。
func TestQueryNgrams(t *testing.T) {
	ng := queryNgrams("星云一号的总设计师是谁")
	if !hasStr(ng, "星云一号") {
		t.Fatalf("应含 2..8 长度子串'星云一号'，got %v", ng)
	}
	// 长度边界：最短 2、最长 8。
	for _, s := range ng {
		if r := []rune(s); len(r) < 2 || len(r) > 8 {
			t.Fatalf("n-gram 长度应在 [2,8]，got %q(len=%d)", s, len(r))
		}
	}
}

// TestBleve_PersistsAcrossReopen 钉死落盘：写入→关闭→同路径重开，数据仍在。
// 修复"NewMemOnly 进程重启即丢、三路静默退化两路"的回归——这是 P0 的核心验收。
func TestBleve_PersistsAcrossReopen(t *testing.T) {
	t.Setenv("BLEVE_PATH", t.TempDir()+"/bleve")
	b1 := openBleve()
	if !b1.ready() {
		t.Skip("bleve 不可用")
	}
	if err := b1.index("kp", "d1", []string{"持久化测试：龙渊号探测器由总师陈思远负责。"}); err != nil {
		t.Fatal(err)
	}
	b1.close() // 模拟进程退出（落盘刷盘）

	b2 := openBleve() // 模拟重启：同路径重开
	defer b2.close()
	if hits := b2.search("kp", "龙渊号探测器", 5); len(hits) == 0 {
		t.Fatal("重开后应仍能检索到（落盘持久），got 0 —— 退回内存了？")
	}
}

func contains(s, sub string) bool {
	return len(sub) == 0 || (len(s) >= len(sub) && indexOf(s, sub) >= 0)
}
func indexOf(s, sub string) int {
	for i := 0; i+len(sub) <= len(s); i++ {
		if s[i:i+len(sub)] == sub {
			return i
		}
	}
	return -1
}
func hasStr(ss []string, want string) bool {
	for _, s := range ss {
		if s == want {
			return true
		}
	}
	return false
}
