# -*- coding: utf-8 -*-
"""CG FileHistoryService 文件历史事务测试（Phase 8 Tier 2）

借鉴 zhikuncode FileHistoryService.java：
- begin_transaction / record_file_change / commit_transaction / rewind
- 按 session_id 隔离事务
- rewind 创建 .bak 备份
- 新建文件回退时删除
"""
import os
import time
import threading
from pathlib import Path

import pytest

from abyss.history.file_history import (
    FileHistoryService,
    TransactionRecord,
    RewindResult,
)


class TestBeginTransaction:
    """begin_transaction 测试"""

    def test_returns_unique_id(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        tid1 = svc.begin_transaction("sess1", "msg1")
        tid2 = svc.begin_transaction("sess1", "msg2")
        assert tid1 != tid2
        assert len(tid1) > 0

    def test_transaction_not_committed_initially(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        tid = svc.begin_transaction("sess1", "msg1")
        txns = svc.list_recent_transactions("sess1")
        # 未 commit 的不应在 list 中
        assert all(t.transaction_id != tid for t in txns)

    def test_records_session_and_message(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        tid = svc.begin_transaction("sess-xyz", "msg-abc")
        svc.commit_transaction(tid)
        txns = svc.list_recent_transactions("sess-xyz")
        assert len(txns) == 1
        assert txns[0].session_id == "sess-xyz"
        assert txns[0].message_id == "msg-abc"


class TestRecordFileChange:
    """record_file_change 测试"""

    def test_stores_old_content(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        tid = svc.begin_transaction("s1", "m1")
        svc.record_file_change(tid, "/path/a.py", "old content")
        svc.commit_transaction(tid)
        txns = svc.list_recent_transactions("s1")
        assert "/path/a.py" in txns[0].snapshots
        assert txns[0].snapshots["/path/a.py"] == "old content"

    def test_stores_none_for_new_file(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        tid = svc.begin_transaction("s1", "m1")
        svc.record_file_change(tid, "/path/new.py", None)
        svc.commit_transaction(tid)
        txns = svc.list_recent_transactions("s1")
        assert txns[0].snapshots["/path/new.py"] is None

    def test_records_multiple_files(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        tid = svc.begin_transaction("s1", "m1")
        svc.record_file_change(tid, "/a.py", "old-a")
        svc.record_file_change(tid, "/b.py", "old-b")
        svc.record_file_change(tid, "/c.py", None)
        svc.commit_transaction(tid)
        txns = svc.list_recent_transactions("s1")
        assert len(txns[0].snapshots) == 3

    def test_unknown_transaction_id_raises(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        with pytest.raises(KeyError):
            svc.record_file_change("nonexistent", "/a.py", "old")


class TestCommitTransaction:
    """commit_transaction 测试"""

    def test_sets_completed_at(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        tid = svc.begin_transaction("s1", "m1")
        before = time.time()
        svc.commit_transaction(tid)
        after = time.time()
        txns = svc.list_recent_transactions("s1")
        assert txns[0].completed_at is not None
        assert before <= txns[0].completed_at <= after

    def test_commit_unknown_id_raises(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        with pytest.raises(KeyError):
            svc.commit_transaction("nonexistent")


class TestRewind:
    """rewind 测试"""

    def test_restores_original_content(self, tmp_path):
        # 真实文件场景
        target = tmp_path / "file.py"
        target.write_text("current content", encoding="utf-8")

        svc = FileHistoryService(storage_dir=str(tmp_path / "hist"))
        tid = svc.begin_transaction("s1", "m1")
        svc.record_file_change(tid, str(target), "old content")
        svc.commit_transaction(tid)

        result = svc.rewind(tid)
        assert result.success
        # 文件应回退到 old content
        assert target.read_text(encoding="utf-8") == "old content"

    def test_deletes_newly_created_files(self, tmp_path):
        # 创建后回退，文件应被删除
        target = tmp_path / "new_file.py"
        target.write_text("newly created", encoding="utf-8")

        svc = FileHistoryService(storage_dir=str(tmp_path / "hist"))
        tid = svc.begin_transaction("s1", "m1")
        # 新建文件快照为 None
        svc.record_file_change(tid, str(target), None)
        svc.commit_transaction(tid)

        svc.rewind(tid)
        assert not target.exists()

    def test_creates_bak_for_current(self, tmp_path):
        target = tmp_path / "file.py"
        target.write_text("current", encoding="utf-8")

        svc = FileHistoryService(storage_dir=str(tmp_path / "hist"))
        tid = svc.begin_transaction("s1", "m1")
        svc.record_file_change(tid, str(target), "old")
        svc.commit_transaction(tid)

        svc.rewind(tid)
        bak_path = Path(str(target) + ".bak")
        assert bak_path.exists()
        assert bak_path.read_text(encoding="utf-8") == "current"

    def test_rewind_only_affects_transaction_files(self, tmp_path):
        # 文件 a 在事务中，文件 b 不在
        a = tmp_path / "a.py"
        b = tmp_path / "b.py"
        a.write_text("a-current", encoding="utf-8")
        b.write_text("b-current", encoding="utf-8")

        svc = FileHistoryService(storage_dir=str(tmp_path / "hist"))
        tid = svc.begin_transaction("s1", "m1")
        svc.record_file_change(tid, str(a), "a-old")
        # 不记录 b
        svc.commit_transaction(tid)

        svc.rewind(tid)
        assert a.read_text(encoding="utf-8") == "a-old"
        # b 不受影响
        assert b.read_text(encoding="utf-8") == "b-current"

    def test_rewind_unknown_id_returns_failure(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        result = svc.rewind("nonexistent")
        assert not result.success

    def test_rewind_returns_affected_files(self, tmp_path):
        a = tmp_path / "a.py"
        b = tmp_path / "b.py"
        a.write_text("a-cur", encoding="utf-8")
        b.write_text("b-cur", encoding="utf-8")

        svc = FileHistoryService(storage_dir=str(tmp_path / "hist"))
        tid = svc.begin_transaction("s1", "m1")
        svc.record_file_change(tid, str(a), "a-old")
        svc.record_file_change(tid, str(b), "b-old")
        svc.commit_transaction(tid)

        result = svc.rewind(tid)
        assert set(result.affected_files) == {str(a), str(b)}


class TestListRecentTransactions:
    """list_recent_transactions 测试"""

    def test_returns_latest_first(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        tid1 = svc.begin_transaction("s1", "m1")
        svc.commit_transaction(tid1)
        time.sleep(0.01)
        tid2 = svc.begin_transaction("s1", "m2")
        svc.commit_transaction(tid2)

        txns = svc.list_recent_transactions("s1", limit=10)
        assert len(txns) == 2
        # 最新的在前
        assert txns[0].transaction_id == tid2
        assert txns[1].transaction_id == tid1

    def test_scoped_to_session(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        t1 = svc.begin_transaction("sess-a", "m1")
        svc.commit_transaction(t1)
        t2 = svc.begin_transaction("sess-b", "m2")
        svc.commit_transaction(t2)

        a_txns = svc.list_recent_transactions("sess-a")
        b_txns = svc.list_recent_transactions("sess-b")
        assert len(a_txns) == 1
        assert a_txns[0].session_id == "sess-a"
        assert len(b_txns) == 1
        assert b_txns[0].session_id == "sess-b"

    def test_limit_respected(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        for i in range(5):
            tid = svc.begin_transaction("s1", f"m{i}")
            svc.commit_transaction(tid)
            time.sleep(0.01)

        txns = svc.list_recent_transactions("s1", limit=3)
        assert len(txns) == 3

    def test_empty_when_no_transactions(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))
        assert svc.list_recent_transactions("no-such") == []


class TestConcurrency:
    """并发测试"""

    def test_concurrent_begin_commit_thread_safe(self, tmp_path):
        svc = FileHistoryService(storage_dir=str(tmp_path))

        def create_one():
            tid = svc.begin_transaction("s1", "m")
            svc.record_file_change(tid, f"/p/{threading.get_ident()}.py", "old")
            svc.commit_transaction(tid)

        threads = [threading.Thread(target=create_one) for _ in range(10)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        assert len(svc.list_recent_transactions("s1")) == 10


class TestPersistence:
    """持久化测试"""

    def test_transactions_persist_across_instances(self, tmp_path):
        storage = str(tmp_path / "hist")
        svc1 = FileHistoryService(storage_dir=storage)
        tid = svc1.begin_transaction("s1", "m1")
        svc1.record_file_change(tid, "/a.py", "old")
        svc1.commit_transaction(tid)

        # 新实例加载已有数据
        svc2 = FileHistoryService(storage_dir=storage)
        txns = svc2.list_recent_transactions("s1")
        assert len(txns) == 1
        assert txns[0].snapshots["/a.py"] == "old"
