"""
全面测试 SessionManager —— 覆盖所有公开方法、边界条件和恢复场景
"""
import sys
import tempfile
import asyncio
import json
import time
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent.parent))

import pytest
from aacode.utils.session_manager import SessionManager, SessionMessage


@pytest.fixture
def sm():
    with tempfile.TemporaryDirectory() as td:
        yield SessionManager(Path(td))


def _read_file(path):
    with open(path, "r") as f:
        return json.load(f)


def _session_file(sm):
    return sm.sessions_dir / f"{sm.current_session_id}.json"


# =============================================================================
# 会话生命周期
# =============================================================================

class TestSessionLifecycle:
    """创建、使用、切换、列出、删除、归档"""

    @pytest.mark.asyncio
    async def test_create_session(self, sm):
        sid = await sm.create_session("my task")
        assert sid.startswith("session_")
        assert sm.current_session_id == sid
        assert len(sm.current_messages) == 1  # user only (no placeholder system)

    @pytest.mark.asyncio
    async def test_create_empty_task(self, sm):
        """空 task 也能创建 session，无 user 消息"""
        sid = await sm.create_session("")
        assert len(sm.current_messages) == 0  # no system placeholder

        sid2 = await sm.create_session("  ")
        assert len(sm.current_messages) == 0

    @pytest.mark.asyncio
    async def test_create_session_with_title(self, sm):
        sid = await sm.create_session("task", title="Custom Title")
        assert sm.sessions_index[sid].title == "Custom Title"

    @pytest.mark.asyncio
    async def test_create_multiple_sessions(self, sm):
        sid1 = await sm.create_session("first")
        sid2 = await sm.create_session("second")
        sid3 = await sm.create_session("third")
        assert sid1 != sid2
        assert sid2 != sid3
        assert len(sm.sessions_index) == 3

    @pytest.mark.asyncio
    async def test_list_sessions(self, sm):
        await sm.create_session("alpha")
        await sm.create_session("beta")
        sessions = await sm.list_sessions()
        assert len(sessions) == 2
        titles = [s["title"] for s in sessions]
        assert "alpha" in titles
        assert "beta" in titles

    @pytest.mark.asyncio
    async def test_list_sessions_empty(self, sm):
        sessions = await sm.list_sessions()
        assert sessions == []

    @pytest.mark.asyncio
    async def test_switch_session(self, sm):
        sid1 = await sm.create_session("first")
        await sm.add_message("user", "msg in first")
        sid2 = await sm.create_session("second")
        await sm.add_message("user", "msg in second")

        assert sm.current_session_id == sid2
        ok = await sm.switch_session(sid1)
        assert ok
        assert sm.current_session_id == sid1
        msgs = await sm.get_messages(include_system=False)
        contents = [m["content"] for m in msgs]
        assert "msg in first" in contents

    @pytest.mark.asyncio
    async def test_switch_to_nonexistent(self, sm):
        ok = await sm.switch_session("nosuch")
        assert not ok

    @pytest.mark.asyncio
    async def test_delete_session(self, sm):
        sid = await sm.create_session("delete me")
        assert sid in sm.sessions_index
        ok = await sm.delete_session(sid)
        assert ok
        assert sid not in sm.sessions_index
        assert not _session_file(sm).exists()

    @pytest.mark.asyncio
    async def test_delete_nonexistent(self, sm):
        ok = await sm.delete_session("nosuch")
        assert not ok

    @pytest.mark.asyncio
    async def test_delete_current_session(self, sm):
        sid = await sm.create_session("current")
        await sm.delete_session(sid)
        assert sm.current_session_id is None

    @pytest.mark.asyncio
    async def test_archive_session(self, sm):
        sid = await sm.create_session("archive me")
        ok = await sm.archive_session(sid)
        assert ok
        assert sm.sessions_index[sid].status == "archived"

        ok = await sm.archive_session("nosuch")
        assert not ok


# =============================================================================
# add_message 边界和行为
# =============================================================================

class TestAddMessage:
    """add_message 各种边界情况"""

    @pytest.mark.asyncio
    async def test_add_message_no_session(self, sm):
        """无当前 session 时 add_message 返回 False"""
        ok = await sm.add_message("user", "hello")
        assert not ok

    @pytest.mark.asyncio
    async def test_add_user_and_assistant(self, sm):
        await sm.create_session("")
        ok = await sm.add_message("user", "what is python?")
        assert ok
        ok = await sm.add_message("assistant", "Python is a language")
        assert ok

        msgs = await sm.get_messages(include_system=False)
        assert len(msgs) == 2
        assert msgs[0]["content"] == "what is python?"
        assert msgs[1]["content"] == "Python is a language"

    @pytest.mark.asyncio
    async def test_add_message_with_full_structured_fields(self, sm):
        await sm.create_session("test")

        await sm.add_message(
            "assistant", "let me check",
            tool_calls=[{"id": "c1", "name": "run_shell", "arguments": '{"cmd":"ls"}'}],
            tool_call_id=None,
            reasoning_content="need to list files",
        )
        msgs = await sm.get_messages(include_system=False)
        last = msgs[-1]
        assert last["role"] == "assistant"
        assert last["tool_calls"][0]["name"] == "run_shell"
        assert last["reasoning_content"] == "need to list files"

    @pytest.mark.asyncio
    async def test_add_tool_message_with_id(self, sm):
        await sm.create_session("test")
        await sm.add_message("tool", "result data", tool_call_id="call_abc")
        msgs = await sm.get_messages(include_system=False)
        last = msgs[-1]
        assert last["role"] == "tool"
        assert last["tool_call_id"] == "call_abc"

    @pytest.mark.asyncio
    async def test_add_message_updates_session_index(self, sm):
        await sm.create_session("test")
        before = sm.sessions_index[sm.current_session_id].total_messages
        await sm.add_message("user", "new msg")
        after = sm.sessions_index[sm.current_session_id].total_messages
        assert after == before + 1


# =============================================================================
# get_messages / get_conversation_history / get_session
# =============================================================================

class TestGetMessages:
    """消息获取方法"""

    @pytest.mark.asyncio
    async def test_get_messages_no_session(self, sm):
        msgs = await sm.get_messages()
        assert msgs == []

    @pytest.mark.asyncio
    async def test_get_messages_include_system(self, sm):
        await sm.create_session("test")
        await sm.add_message("system", "compaction summary")
        msgs = await sm.get_messages(include_system=True)
        roles = [m["role"] for m in msgs]
        assert "system" in roles  # 手动添加的 system 消息（如压缩摘要）仍然存在

        msgs_no = await sm.get_messages(include_system=False)
        roles_no = [m["role"] for m in msgs_no]
        assert "system" not in roles_no

    @pytest.mark.asyncio
    async def test_get_conversation_history(self, sm):
        await sm.create_session("test")
        await sm.add_message("user", "hello")
        await sm.add_message("assistant", "hi there")
        hist = await sm.get_conversation_history()
        assert "User" in hist
        assert "Assistant" in hist
        assert "hello" in hist
        assert "hi there" in hist

    @pytest.mark.asyncio
    async def test_get_conversation_history_max_length(self, sm):
        await sm.create_session("test")
        for i in range(20):
            await sm.add_message("user", f"msg{i}")
            await sm.add_message("assistant", f"resp{i}")

        hist = await sm.get_conversation_history(max_length=3)
        assert "msg19" in hist
        assert "msg0" not in hist

    @pytest.mark.asyncio
    async def test_get_conversation_history_empty(self, sm):
        hist = await sm.get_conversation_history()
        assert hist == ""

    @pytest.mark.asyncio
    async def test_get_conversation_history_skips_system(self, sm):
        await sm.create_session("test")
        await sm.add_message("user", "hello")
        hist = await sm.get_conversation_history()
        assert "System" not in hist

    @pytest.mark.asyncio
    async def test_get_session_by_id(self, sm):
        sid = await sm.create_session("test session")
        await sm.add_message("user", "hello")
        await sm.add_message("assistant", "world")

        data = await sm.get_session(sid)
        assert data["session_id"] == sid
        assert "messages" in data
        assert len(data["messages"]) >= 2

    @pytest.mark.asyncio
    async def test_get_session_current(self, sm):
        await sm.create_session("test")
        data = await sm.get_session()
        assert data["session_id"] == sm.current_session_id

    @pytest.mark.asyncio
    async def test_get_session_no_session(self, sm):
        data = await sm.get_session()
        assert data == {}

    @pytest.mark.asyncio
    async def test_get_session_nonexistent(self, sm):
        data = await sm.get_session("nosuch")
        assert data == {}


# =============================================================================
# 多轮对话数据完整性
# =============================================================================

class TestMultiTurnIntegrity:
    """多轮完整场景：create → add(多轮) → save → reload → verify"""

    @pytest.mark.asyncio
    async def test_simple_multiturn(self, sm):
        sid = await sm.create_session("")

        for i in range(3):
            await sm.add_message("user", f"Q{i}: hello")
            await sm.add_message("assistant", f"A{i}: world")

        await sm._save_session()

        sm2 = SessionManager(sm.project_path)
        await sm2.switch_session(sid)
        msgs = await sm2.get_messages(include_system=False)
        assert len(msgs) == 6
        for i in range(3):
            assert msgs[i * 2]["content"] == f"Q{i}: hello"
            assert msgs[i * 2 + 1]["content"] == f"A{i}: world"

    @pytest.mark.asyncio
    async def test_multiturn_with_tool_calls(self, sm):
        """多轮中包含 tool_calls、tool result、reasoning"""
        sid = await sm.create_session("tool heavy")

        await sm.add_message("user", "list files")
        await sm.add_message(
            "assistant", "let me check",
            tool_calls=[{"id": "c1", "name": "run_shell", "arguments": '{"cmd":"ls"}'}],
            reasoning_content="need to list directory",
        )
        await sm.add_message("tool", "file1.py\nfile2.py", tool_call_id="c1")
        await sm.add_message("assistant", "done checking")

        await sm.add_message("user", "read file1")
        await sm.add_message(
            "assistant", "reading",
            tool_calls=[{"id": "c2", "name": "run_shell", "arguments": '{"path":"file1.py"}'}],
        )
        await sm.add_message("tool", "print('hello')", tool_call_id="c2")
        await sm.add_message("assistant", "Task completed")

        await sm._save_session()

        sm2 = SessionManager(sm.project_path)
        await sm2.switch_session(sid)

        msgs = await sm2.get_messages(include_system=False)

        assert len([m for m in msgs if m["role"] == "user"]) == 3
        assert len([m for m in msgs if m["role"] == "assistant"]) >= 4
        assert len([m for m in msgs if m["role"] == "tool"]) == 2

        assert msgs[-1]["role"] == "assistant"

    @pytest.mark.asyncio
    async def test_tool_messages_survive_after_reload(self, sm):
        """重载后 tool 消息仍完整"""
        sid = await sm.create_session("survival")
        sm.current_messages.append(SessionMessage(role="user", content="q1", timestamp="0"))
        sm.current_messages.append(SessionMessage(role="assistant", content="a1", timestamp="0", tool_calls=[{"name": "search"}]))
        sm.current_messages.append(SessionMessage(role="tool", content="results", timestamp="0", tool_call_id="c1"))
        sm.current_messages.append(SessionMessage(role="user", content="q2", timestamp="0"))
        sm.current_messages.append(SessionMessage(role="assistant", content="a2", timestamp="0", tool_calls=[{"name": "finalize"}]))
        sm.current_messages.append(SessionMessage(role="tool", content="done", timestamp="0", tool_call_id="c2"))
        await sm._save_session()

        sm2 = SessionManager(sm.project_path)
        await sm2.switch_session(sid)
        msgs = await sm2.get_messages(include_system=False)
        tool_count = len([m for m in msgs if m["role"] == "tool"])
        assert tool_count == 2


# =============================================================================
# 压缩测试
# =============================================================================

class TestCompaction:
    """_compact_context (session 级) 和 compress_session"""

    @pytest.mark.asyncio
    async def test_compact_context_not_triggered_small(self, sm):
        """小 session 不应触发压缩"""
        await sm.create_session("test")
        for i in range(10):
            await sm.add_message("user", f"msg{i}")
        assert len(sm.current_messages) > 10

    @pytest.mark.asyncio
    async def test_compact_context_triggered(self, sm):
        """_compact_context 不再破坏持久化数据，仅记录警告。全量消息始终保留。"""
        low_max = 1
        sm2 = SessionManager(sm.project_path, max_tokens=low_max)
        await sm2.create_session("test")
        assert len(sm2.current_messages) >= 1

        for i in range(20):
            ok = await sm2.add_message("user", f"msg {i}")
            assert ok, f"add_message should always succeed (non-destructive), failed at i={i}"

        assert len(sm2.current_messages) == 21  # create_session adds 1, then 20 more

    @pytest.mark.asyncio
    async def test_compact_context_preserves_tool_messages(self, sm):
        """全量消息始终保留，包括 tool 消息（非破坏性压缩）"""
        low_max = 1
        sm2 = SessionManager(sm.project_path, max_tokens=low_max)
        await sm2.create_session("test")
        sm2.current_messages.append(SessionMessage(role="tool", content="keep me", timestamp="0", tool_call_id="c1"))

        for i in range(30):
            ok = await sm2.add_message("user", f"msg {i}")
            assert ok

        tool_msgs = [m for m in sm2.current_messages if m.role == "tool"]
        assert len(tool_msgs) >= 1

    @pytest.mark.asyncio
    async def test_compress_session(self, sm):
        """compress_session API 工作正常"""
        await sm.create_session("test")
        for i in range(60):
            await sm.add_message("user", f"msg{i}")
            await sm.add_message("assistant", f"resp{i}")

        result = await sm.compress_session(max_messages=10)
        assert result["removed_count"] > 0
        assert len(result["compressed_messages"]) == 10

    @pytest.mark.asyncio
    async def test_compress_session_no_session(self, sm):
        result = await sm.compress_session()
        assert result == {"compressed_messages": [], "removed_count": 0}

    @pytest.mark.asyncio
    async def test_compress_session_below_limit(self, sm):
        """消息数未达到阈值时不做压缩"""
        await sm.create_session("test")
        await sm.add_message("user", "msg")
        result = await sm.compress_session(max_messages=50)
        assert result["removed_count"] == 0


# =============================================================================
# 边界情况
# =============================================================================

class TestEdgeCases:
    """空消息、超长内容、特殊字符等"""

    @pytest.mark.asyncio
    async def test_add_message_empty_content(self, sm):
        await sm.create_session("test")
        ok = await sm.add_message("user", "")
        assert ok

    @pytest.mark.asyncio
    async def test_very_long_message(self, sm):
        await sm.create_session("test")
        long_content = "A" * 10000
        ok = await sm.add_message("user", long_content)
        assert ok
        msgs = await sm.get_messages(include_system=False)
        assert len(msgs[-1]["content"]) == 10000

    @pytest.mark.asyncio
    async def test_unicode_content(self, sm):
        await sm.create_session("test")
        content = "你好，世界！🔥 测试 Unicode: ñ, ü, 🚀, 日本語, 😊"
        await sm.add_message("user", content)
        msgs = await sm.get_messages(include_system=False)
        assert msgs[-1]["content"] == content

    @pytest.mark.asyncio
    async def test_json_special_chars(self, sm):
        await sm.create_session("test")
        content = '{"key": "value with \'quotes\' and \\"escaped\\""}'
        await sm.add_message("user", content)
        await sm._save_session()

        raw = _read_file(_session_file(sm))
        msgs = raw["messages"]
        assert msgs[-1]["content"] == content

    @pytest.mark.asyncio
    async def test_role_system(self, sm):
        """system 角色消息"""
        await sm.create_session("test")
        ok = await sm.add_message("system", "you are a bot")
        assert ok
        msgs = await sm.get_messages(include_system=True)
        roles = [m["role"] for m in msgs]
        assert roles.count("system") == 1  # 仅手动添加的 system（不再有占位 system）

    @pytest.mark.asyncio
    async def test_repeated_messages(self, sm):
        """重复内容不异常"""
        await sm.create_session("")
        for i in range(5):
            await sm.add_message("user", "same content")
            await sm.add_message("assistant", "same response")
        msgs = await sm.get_messages(include_system=False)
        assert len(msgs) == 10


# =============================================================================
# Token 计数和限制
# =============================================================================

class TestTokenCounting:
    """token 统计相关"""

    @pytest.mark.asyncio
    async def test_count_tokens(self, sm):
        sid = await sm.create_session("test")
        await sm.add_message("user", "a" * 100)

        count = await sm.count_tokens(sid)
        assert count == 0

        count = await sm.count_tokens()
        assert count == 0

    @pytest.mark.asyncio
    async def test_count_tokens_no_session(self, sm):
        count = await sm.count_tokens()
        assert count == 0

    @pytest.mark.asyncio
    async def test_token_limit_property(self, sm):
        assert sm.token_limit == 200000

        sm2 = SessionManager(sm.project_path, max_tokens=50000)
        assert sm2.token_limit == 50000

    @pytest.mark.asyncio
    async def test_token_limit_enforced(self, sm):
        """add_message 不再因 token 限制拒绝消息；全量持久化不破坏"""
        sm2 = SessionManager(sm.project_path, max_tokens=500)
        await sm2.create_session("test")

        ok = await sm2.add_message("user", "X" * 2000)
        assert ok  # 非破坏性：全量数据始终保留

    @pytest.mark.asyncio
    async def test_token_limit_enforced(self, sm):
        sm2 = SessionManager(sm.project_path, max_tokens=100)
        await sm2.create_session("")
        ok = await sm2.add_message("user", "X" * 10000)
        assert ok  # 非破坏性：全量数据始终保留

    @pytest.mark.asyncio
    async def test_token_limit_allows_small(self, sm):
        sm2 = SessionManager(sm.project_path, max_tokens=500)
        await sm2.create_session("")
        ok = await sm2.add_message("user", "hi")
        assert ok

    @pytest.mark.asyncio
    async def test_get_session_stats(self, sm):
        """get_session_stats 返回字段结构"""
        await sm.create_session("test")
        await sm.add_message("user", "hello")
        await sm.add_message("assistant", "world")

        stats = sm.get_session_stats()
        assert stats["session_id"] == sm.current_session_id
        assert stats["max_tokens"] == 200000
        assert stats["current_tokens"] > 0


# =============================================================================
# 文件持久化
# =============================================================================

class TestFilePersistence:
    """文件读写、数据完整性"""

    @pytest.mark.asyncio
    async def test_session_file_created(self, sm):
        sid = await sm.create_session("test")
        assert _session_file(sm).exists()

    @pytest.mark.asyncio
    async def test_session_file_contains_messages(self, sm):
        await sm.create_session("test")
        await sm.add_message("user", "hello")
        raw = _read_file(_session_file(sm))
        assert len(raw["messages"]) >= 2

    @pytest.mark.asyncio
    async def test_session_file_structure(self, sm):
        """JSON 文件包含所需字段，无 structured_messages 键"""
        sid = await sm.create_session("test")
        await sm.add_message("user", "msg")

        raw = _read_file(_session_file(sm))
        assert "session_id" in raw
        assert "created_at" in raw
        assert "messages" in raw
        assert "structured_messages" not in raw

    @pytest.mark.asyncio
    async def test_session_index_file(self, sm):
        await sm.create_session("alpha")
        await sm.create_session("beta")
        idx_file = sm.sessions_dir / "sessions_index.json"
        assert idx_file.exists()
        raw = _read_file(idx_file)
        assert len(raw) == 2

    @pytest.mark.asyncio
    async def test_current_session_file(self, sm):
        sid = await sm.create_session("test session")
        txt_file = sm.sessions_dir.parent / "current_session.txt"
        assert txt_file.exists()
        content = txt_file.read_text()
        assert sid in content
        assert "test session" in content


# =============================================================================
# 错误恢复
# =============================================================================

class TestErrorRecovery:
    """文件损坏、缺失等恢复能力"""

    @pytest.mark.asyncio
    async def test_load_corrupted_session(self, sm):
        """损坏的 session 文件应返回 False"""
        sid = await sm.create_session("test")
        await sm._save_session()

        f = _session_file(sm)
        f.write_text("{corrupted json")

        loaded = await sm._load_session(sid)
        assert not loaded

    @pytest.mark.asyncio
    async def test_load_missing_session(self, sm):
        loaded = await sm._load_session("nosuch")
        assert not loaded

    @pytest.mark.asyncio
    async def test_save_without_session(self, sm):
        """无当前 session 时 _save 不崩溃"""
        await sm._save_session()
        sm._save_sessions_index()

    @pytest.mark.asyncio
    async def test_reload_after_message_add(self, sm):
        """add_message 后重载，数据一致"""
        sid = await sm.create_session("test")
        await sm.add_message("user", "msg1")
        await sm.add_message("assistant", "resp1")
        await sm.add_message("user", "msg2")
        await sm.add_message("assistant", "resp2")

        sm2 = SessionManager(sm.project_path)
        await sm2.switch_session(sid)

        msgs = await sm2.get_messages(include_system=False)
        assert msgs[-1]["content"] == "resp2"
        assert msgs[-2]["content"] == "msg2"


# =============================================================================
# 并发安全
# =============================================================================

class TestConcurrency:
    """快速连续操作"""

    @pytest.mark.asyncio
    async def test_rapid_add_messages(self, sm):
        """快速添加多条消息不崩溃"""
        await sm.create_session("")
        tasks = []
        for i in range(50):
            tasks.append(sm.add_message("user", f"msg{i}"))
            tasks.append(sm.add_message("assistant", f"resp{i}"))
        results = await asyncio.gather(*tasks)
        assert all(results)

        msgs = await sm.get_messages(include_system=False)
        assert len(msgs) == 100


# =============================================================================
# 结构化消息单轨存储
# =============================================================================

class TestStructuredSingleTrack:
    """所有消息（含结构化字段）统一存储在 current_messages 中"""

    @pytest.mark.asyncio
    async def test_add_message_appends_to_current(self, sm):
        """add_message 仅追加到 current_messages，无其他存储路径"""
        sid = await sm.create_session("test")

        await sm.add_message("user", "new msg")
        assert len(sm.current_messages) >= 2  # user + new msg (no placeholder system)
        assert sm.current_messages[-1].content == "new msg"

    @pytest.mark.asyncio
    async def test_all_roles_and_fields_in_current(self, sm):
        """所有角色和结构化字段保存到 current_messages"""
        await sm.create_session("test")

        await sm.add_message("user", "u1")
        await sm.add_message("assistant", "a1", tool_calls=[{"name": "x"}], reasoning_content="r1")
        await sm.add_message("tool", "t1", tool_call_id="c1")
        await sm.add_message("system", "s1")

        msgs = sm.current_messages
        assert msgs[-4].role == "user"
        assert msgs[-4].content == "u1"
        assert msgs[-3].role == "assistant"
        assert msgs[-3].tool_calls == [{"name": "x"}]
        assert msgs[-3].reasoning_content == "r1"
        assert msgs[-2].role == "tool"
        assert msgs[-2].tool_call_id == "c1"
        assert msgs[-1].role == "system"
        assert msgs[-1].content == "s1"


# =============================================================================
# get_session API 完整返回
# =============================================================================

class TestGetSessionAPI:
    """get_session 返回的数据结构完整性"""

    @pytest.mark.asyncio
    async def test_get_session_response_structure(self, sm):
        sid = await sm.create_session("api test")
        await sm.add_message("user", "hello")
        data = await sm.get_session(sid)

        assert "session_id" in data
        assert "title" in data
        assert "messages" in data
        assert "total_messages" in data
        assert "total_tokens" in data
        assert "created_at" in data
        assert "last_activity" in data

    @pytest.mark.asyncio
    async def test_get_session_messages_contain_tool_role(self, sm):
        sid = await sm.create_session("api test")
        sm.current_messages.append(SessionMessage(role="user", content="q", timestamp="0"))
        sm.current_messages.append(SessionMessage(role="assistant", content="a", timestamp="0", tool_calls=[{"name": "run_shell"}]))
        sm.current_messages.append(SessionMessage(role="tool", content="r", timestamp="0", tool_call_id="c1"))
        await sm._save_session()
        data = await sm.get_session(sid)
        roles = [m["role"] for m in data["messages"]]
        assert "tool" in roles
