# -*- coding: utf-8 -*-
"""MemoryStore 单元测试"""
import json
import os
import sys
import tempfile
from pathlib import Path

import pytest

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))

from abyss.memory.store import MemoryEntry, MemoryStore


@pytest.fixture
def tmp_store_path(tmp_path):
    return tmp_path / "memory.jsonl"


@pytest.fixture
def store(tmp_store_path):
    return MemoryStore(tmp_store_path)


class TestMemoryEntry:
    def test_to_dict_roundtrip(self):
        e = MemoryEntry(
            id="mem_1",
            title="测试标题",
            content="测试内容",
            category="preference",
            tags=["python", "code"],
        )
        d = e.to_dict()
        assert d["id"] == "mem_1"
        assert d["title"] == "测试标题"
        assert d["content"] == "测试内容"
        assert d["category"] == "preference"
        assert d["tags"] == ["python", "code"]

        # from_dict 反序列化
        e2 = MemoryEntry.from_dict(d)
        assert e2.id == e.id
        assert e2.title == e.title
        assert e2.content == e.content
        assert e2.category == e.category
        assert e2.tags == e.tags

    def test_from_dict_defaults(self):
        """from_dict 缺失字段应填默认值"""
        e = MemoryEntry.from_dict({"content": "foo"})
        assert e.content == "foo"
        assert e.title == ""
        assert e.category == "session"
        assert e.tags == []
        # id 自动生成
        assert e.id.startswith("mem_")

    def test_to_index_entry(self):
        e = MemoryEntry(id="1", title="t", content="c")
        idx = e.to_index_entry()
        assert idx["id"] == "1"
        assert idx["title"] == "t"
        assert idx["content"] == "c"


class TestMemoryStore:
    def test_add_and_list(self, store):
        e = MemoryEntry(id="", title="测试", content="内容", category="session")
        store.add(e)
        assert store.count == 1
        all_entries = store.list_all()
        assert len(all_entries) == 1
        assert all_entries[0].title == "测试"
        # id 应自动生成
        assert all_entries[0].id.startswith("mem_")
        # created_at 应自动填
        assert all_entries[0].created_at != ""

    def test_persist_to_disk(self, tmp_store_path):
        store = MemoryStore(tmp_store_path)
        store.add(MemoryEntry(id="", title="t1", content="c1"))
        store.add(MemoryEntry(id="", title="t2", content="c2"))
        assert tmp_store_path.exists()

        # 重新加载，应能读取
        store2 = MemoryStore(tmp_store_path)
        assert store2.count == 2
        titles = [e.title for e in store2.list_all()]
        assert "t1" in titles
        assert "t2" in titles

    def test_persist_false(self, tmp_store_path):
        """persist=False 不写盘"""
        store = MemoryStore(tmp_store_path)
        store.add(MemoryEntry(id="", title="t1", content="c1"), persist=False)
        assert store.count == 1
        # 但磁盘上不存在
        assert not tmp_store_path.exists()

    def test_get_by_id(self, store):
        e = MemoryEntry(id="mem_abc", title="t", content="c")
        store.add(e)
        got = store.get("mem_abc")
        assert got is not None
        assert got.title == "t"

        # 不存在的 id
        assert store.get("nonexistent") is None

    def test_remove(self, store):
        store.add(MemoryEntry(id="mem_1", title="t1", content="c1"))
        store.add(MemoryEntry(id="mem_2", title="t2", content="c2"))
        assert store.count == 2

        assert store.remove("mem_1") is True
        assert store.count == 1
        # 删除不存在的应返回 False
        assert store.remove("nonexistent") is False

    def test_clear(self, store):
        store.add(MemoryEntry(id="", title="t1", content="c1"))
        store.add(MemoryEntry(id="", title="t2", content="c2"))
        cleared = store.clear()
        assert cleared == 2
        assert store.count == 0

    def test_max_entries_fifo(self, tmp_store_path):
        """超过 max_entries 时 FIFO 淘汰最旧的"""
        store = MemoryStore(tmp_store_path, max_entries=3)
        store.add(MemoryEntry(id="mem_1", title="t1", content="c1"))
        store.add(MemoryEntry(id="mem_2", title="t2", content="c2"))
        store.add(MemoryEntry(id="mem_3", title="t3", content="c3"))
        store.add(MemoryEntry(id="mem_4", title="t4", content="c4"))
        # 应保留后 3 条
        assert store.count == 3
        ids = [e.id for e in store.list_all()]
        assert "mem_1" not in ids
        assert "mem_2" in ids
        assert "mem_3" in ids
        assert "mem_4" in ids

    def test_search_simple(self, store):
        """简单关键词搜索（向后兼容）"""
        store.add(MemoryEntry(id="m1", title="Python 入门", content="Python 编程基础"))
        store.add(MemoryEntry(id="m2", title="Java 入门", content="Java 编程基础"))
        store.add(MemoryEntry(id="m3", title="Go 入门", content="Go 并发编程"))

        results = store.search("Python", top_k=2)
        assert len(results) >= 1
        # 应命中 Python 相关
        titles = [r.title for r in results]
        assert any("Python" in t for t in titles)

    def test_search_empty_query(self, store):
        store.add(MemoryEntry(id="m1", title="t", content="c"))
        assert store.search("", top_k=5) == []

    def test_save_method(self, tmp_store_path):
        """显式 save() 方法"""
        store = MemoryStore(tmp_store_path)
        store.add(MemoryEntry(id="m1", title="t", content="c"), persist=False)
        # 此时磁盘上没数据
        assert not tmp_store_path.exists()
        store.save()
        assert tmp_store_path.exists()

    def test_load_corrupt_file(self, tmp_store_path):
        """损坏的行应被跳过，不阻断加载"""
        tmp_store_path.parent.mkdir(parents=True, exist_ok=True)
        with tmp_store_path.open("w", encoding="utf-8") as f:
            f.write('{"id":"m1","title":"t1","content":"c1"}\n')
            f.write('corrupt line without json\n')  # 损坏
            f.write('{"id":"m2","title":"t2","content":"c2"}\n')
        store = MemoryStore(tmp_store_path)
        assert store.count == 2  # 跳过损坏行

    def test_path_property(self, tmp_store_path):
        store = MemoryStore(tmp_store_path)
        assert store.path == tmp_store_path

    def test_atomic_write_no_corruption(self, tmp_store_path):
        """原子写入不应产生中间文件"""
        store = MemoryStore(tmp_store_path)
        store.add(MemoryEntry(id="m1", title="t", content="c"))
        # .tmp 文件不应残留
        tmp_file = tmp_store_path.with_suffix(tmp_store_path.suffix + ".tmp")
        assert not tmp_file.exists()
