"""F7 Inbox JSONL Watcher 单元测试

按测试计划 test-plan-v2.6.md §F7：
- T1: 写入+消费（P0）
- T2: truncate（P0）
- T3: 并发写入（P0）
- T4: 损坏行恢复（P1）
- T5: 空文件处理（P1）
"""

import asyncio
import json
import threading
import pytest

pytestmark = pytest.mark.unit
from pathlib import Path

from src.daemon.inbox import InboxWatcher


@pytest.fixture
def inbox_path(tmp_path):
    return tmp_path / "inbox" / "daemon.jsonl"


@pytest.fixture
def watcher(inbox_path):
    return InboxWatcher(inbox_path, watch_interval=0.05)


# ---------------------------------------------------------------------------
# T1: 写入+消费
# ---------------------------------------------------------------------------

class TestWriteConsume:
    def test_single_event(self, inbox_path):
        """写入单条事件 → 消费到"""
        InboxWatcher.write_event(inbox_path, {"type": "task_done", "task_id": "t1", "agent": "a"})

        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert len(events) == 1
        assert events[0]["type"] == "task_done"
        assert events[0]["task_id"] == "t1"

    def test_multiple_events(self, inbox_path):
        """写入多条事件 → 全部消费到"""
        for i in range(5):
            InboxWatcher.write_event(inbox_path, {"type": "progress", "task_id": f"t{i}", "pct": i * 20})

        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert len(events) == 5
        assert events[0]["task_id"] == "t0"
        assert events[4]["task_id"] == "t4"

    def test_consume_increments_counter(self, inbox_path):
        """total_processed 递增"""
        watcher = InboxWatcher(inbox_path)
        assert watcher.total_processed == 0

        InboxWatcher.write_event(inbox_path, {"type": "test"})
        asyncio.run(watcher.poll())
        assert watcher.total_processed == 1

        InboxWatcher.write_event(inbox_path, {"type": "test2"})
        InboxWatcher.write_event(inbox_path, {"type": "test3"})
        asyncio.run(watcher.poll())
        assert watcher.total_processed == 3

    def test_consume_no_file_returns_empty(self, inbox_path):
        """文件不存在时返回空列表"""
        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert events == []

    def test_batch_write(self, inbox_path):
        """批量写入"""
        evts = [{"type": "batch", "seq": i} for i in range(10)]
        InboxWatcher.write_events(inbox_path, evts)

        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert len(events) == 10


# ---------------------------------------------------------------------------
# T2: truncate
# ---------------------------------------------------------------------------

class TestTruncate:
    def test_file_cleared_after_consume(self, inbox_path):
        """消费后文件被清空"""
        InboxWatcher.write_event(inbox_path, {"type": "test"})
        assert inbox_path.exists()
        assert inbox_path.stat().st_size > 0

        watcher = InboxWatcher(inbox_path)
        asyncio.run(watcher.poll())

        # 文件存在但为空
        assert inbox_path.exists()
        assert inbox_path.stat().st_size == 0

    def test_second_consume_returns_empty(self, inbox_path):
        """二次消费无新事件"""
        InboxWatcher.write_event(inbox_path, {"type": "test"})
        watcher = InboxWatcher(inbox_path)

        events1 = asyncio.run(watcher.poll())
        assert len(events1) == 1

        events2 = asyncio.run(watcher.poll())
        assert len(events2) == 0

    def test_write_after_truncate(self, inbox_path):
        """truncate 后可以继续写入"""
        InboxWatcher.write_event(inbox_path, {"type": "first"})
        watcher = InboxWatcher(inbox_path)
        asyncio.run(watcher.poll())

        InboxWatcher.write_event(inbox_path, {"type": "second"})
        events = asyncio.run(watcher.poll())
        assert len(events) == 1
        assert events[0]["type"] == "second"


# ---------------------------------------------------------------------------
# T3: 并发写入
# ---------------------------------------------------------------------------

class TestConcurrentWrite:
    def test_multi_agent_concurrent(self, inbox_path):
        """多 Agent 同时写入不同事件，全部消费到"""
        n_agents = 5
        n_events_per_agent = 10
        barrier = threading.Barrier(n_agents)

        def writer(agent_id):
            barrier.wait()
            for i in range(n_events_per_agent):
                InboxWatcher.write_event(inbox_path, {
                    "agent": agent_id,
                    "seq": i,
                })

        threads = [threading.Thread(target=writer, args=(f"agent-{j}",))
                   for j in range(n_agents)]
        for t in threads:
            t.start()
        for t in threads:
            t.join(timeout=5)

        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert len(events) == n_agents * n_events_per_agent

        # 每个 agent 的事件都到齐
        agents_seen = {e["agent"] for e in events}
        assert len(agents_seen) == n_agents


# ---------------------------------------------------------------------------
# T4: 损坏行恢复（P1）
# ---------------------------------------------------------------------------

class TestBadLineRecovery:
    def test_skip_invalid_json(self, inbox_path):
        """非法 JSON 行跳过不崩溃"""
        inbox_path.parent.mkdir(parents=True, exist_ok=True)
        with open(inbox_path, "w") as f:
            f.write('{"type": "good"}\n')
            f.write('not json at all\n')
            f.write('{"type": "also_good"}\n')
            f.write('{broken json\n')

        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert len(events) == 2
        assert events[0]["type"] == "good"
        assert events[1]["type"] == "also_good"

    def test_non_dict_json_skipped(self, inbox_path):
        """合法 JSON 但非 dict 也跳过"""
        inbox_path.parent.mkdir(parents=True, exist_ok=True)
        with open(inbox_path, "w") as f:
            f.write('{"type": "good"}\n')
            f.write('[1, 2, 3]\n')
            f.write('"just a string"\n')
            f.write('42\n')

        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert len(events) == 1
        assert events[0]["type"] == "good"

    def test_empty_lines_skipped(self, inbox_path):
        """空行不影响"""
        inbox_path.parent.mkdir(parents=True, exist_ok=True)
        with open(inbox_path, "w") as f:
            f.write('\n\n{"type": "good"}\n\n\n')

        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert len(events) == 1


# ---------------------------------------------------------------------------
# T5: 空文件处理（P1）
# ---------------------------------------------------------------------------

class TestEmptyFile:
    def test_empty_file_no_events(self, inbox_path):
        """空文件返回空列表"""
        inbox_path.parent.mkdir(parents=True, exist_ok=True)
        inbox_path.touch()
        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert events == []

    def test_whitespace_only_file(self, inbox_path):
        """只有空白的文件返回空列表"""
        inbox_path.parent.mkdir(parents=True, exist_ok=True)
        inbox_path.write_text("   \n  \n  \n")
        watcher = InboxWatcher(inbox_path)
        events = asyncio.run(watcher.poll())
        assert events == []


# ---------------------------------------------------------------------------
# Watcher 生命周期
# ---------------------------------------------------------------------------

class TestWatcherLifecycle:
    def test_start_stop(self, watcher, inbox_path):
        """可以启动和停止"""
        async def run():
            await watcher.start()
            assert watcher.is_running
            await asyncio.sleep(0.2)
            await watcher.stop()
            assert not watcher.is_running

        asyncio.run(run())

    def test_auto_consume_via_callback(self, inbox_path):
        """watcher 循环自动消费并通过回调传递事件"""
        collected = []

        async def on_event(event):
            collected.append(event)

        watcher = InboxWatcher(inbox_path, process_callback=on_event, watch_interval=0.05)

        async def run():
            await watcher.start()
            # 写入事件
            InboxWatcher.write_event(inbox_path, {"type": "auto"})
            await asyncio.sleep(0.3)  # 等待 watcher 消费
            await watcher.stop()

        asyncio.run(run())
        assert len(collected) >= 1
        assert collected[0]["type"] == "auto"
