"""
Tests for mcp_serve — Hermes MCP server.

Three layers of tests:
1. Unit tests — helpers, content extraction, attachment parsing
2. EventBridge tests — queue mechanics, cursors, waiters, concurrency
3. End-to-end tests — call actual MCP tools through FastMCP's tool manager
   with real session data in SQLite and sessions.json
"""

import asyncio
import inspect
import json
import os
import sqlite3
import time
import threading
from unittest.mock import MagicMock

import pytest


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest.fixture(autouse=True)
def _isolate_hermes_home(tmp_path, monkeypatch):
    """Redirect HERMES_HOME to a temp directory."""
    monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    try:
        import hermes_constants
        monkeypatch.setattr(hermes_constants, "get_hermes_home", lambda: tmp_path)
    except (ImportError, AttributeError):
        pass
    return tmp_path


@pytest.fixture
def sessions_dir(tmp_path):
    sdir = tmp_path / "sessions"
    sdir.mkdir(parents=True, exist_ok=True)
    return sdir


@pytest.fixture
def sample_sessions():
    return {
        "agent:main:telegram:dm:123456": {
            "session_key": "agent:main:telegram:dm:123456",
            "session_id": "20260329_120000_abc123",
            "platform": "telegram",
            "chat_type": "dm",
            "display_name": "Alice",
            "created_at": "2026-03-29T12:00:00",
            "updated_at": "2026-03-29T14:30:00",
            "input_tokens": 50000,
            "output_tokens": 2000,
            "total_tokens": 52000,
            "origin": {
                "platform": "telegram",
                "chat_id": "123456",
                "chat_name": "Alice",
                "chat_type": "dm",
                "user_id": "123456",
                "user_name": "Alice",
                "thread_id": None,
                "chat_topic": None,
            },
        },
        "agent:main:discord:group:789:456": {
            "session_key": "agent:main:discord:group:789:456",
            "session_id": "20260329_100000_def456",
            "platform": "discord",
            "chat_type": "group",
            "display_name": "Bob",
            "created_at": "2026-03-29T10:00:00",
            "updated_at": "2026-03-29T13:00:00",
            "input_tokens": 30000,
            "output_tokens": 1000,
            "total_tokens": 31000,
            "origin": {
                "platform": "discord",
                "chat_id": "789",
                "chat_name": "#general",
                "chat_type": "group",
                "user_id": "456",
                "user_name": "Bob",
                "thread_id": None,
                "chat_topic": None,
            },
        },
        "agent:main:slack:group:C1234:U5678": {
            "session_key": "agent:main:slack:group:C1234:U5678",
            "session_id": "20260328_090000_ghi789",
            "platform": "slack",
            "chat_type": "group",
            "display_name": "Carol",
            "created_at": "2026-03-28T09:00:00",
            "updated_at": "2026-03-28T11:00:00",
            "input_tokens": 10000,
            "output_tokens": 500,
            "total_tokens": 10500,
            "origin": {
                "platform": "slack",
                "chat_id": "C1234",
                "chat_name": "#engineering",
                "chat_type": "group",
                "user_id": "U5678",
                "user_name": "Carol",
                "thread_id": None,
                "chat_topic": None,
            },
        },
    }


@pytest.fixture
def populated_sessions_dir(sessions_dir, sample_sessions):
    (sessions_dir / "sessions.json").write_text(json.dumps(sample_sessions))
    return sessions_dir


def _create_test_db(db_path, session_id, messages):
    """Create a minimal SQLite DB mimicking hermes_state schema."""
    conn = sqlite3.connect(str(db_path))
    conn.execute("""
        CREATE TABLE IF NOT EXISTS sessions (
            id TEXT PRIMARY KEY,
            source TEXT DEFAULT 'cli',
            started_at TEXT,
            message_count INTEGER DEFAULT 0
        )
    """)
    conn.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            session_id TEXT NOT NULL,
            role TEXT NOT NULL,
            content TEXT,
            tool_call_id TEXT,
            tool_calls TEXT,
            tool_name TEXT,
            timestamp TEXT,
            token_count INTEGER DEFAULT 0,
            finish_reason TEXT,
            reasoning TEXT,
            reasoning_details TEXT,
            codex_reasoning_items TEXT
        )
    """)
    conn.execute(
        "INSERT OR IGNORE INTO sessions (id, source, started_at, message_count) VALUES (?, 'gateway', ?, ?)",
        (session_id, "2026-03-29T12:00:00", len(messages)),
    )
    for msg in messages:
        content = msg.get("content", "")
        if isinstance(content, (list, dict)):
            content = json.dumps(content)
        conn.execute(
            "INSERT INTO messages (session_id, role, content, timestamp, tool_calls) VALUES (?, ?, ?, ?, ?)",
            (session_id, msg["role"], content,
             msg.get("timestamp", "2026-03-29T12:00:00"),
             json.dumps(msg["tool_calls"]) if msg.get("tool_calls") else None),
        )
    conn.commit()
    conn.close()


@pytest.fixture
def mock_session_db(tmp_path, populated_sessions_dir):
    """Create a real SQLite DB with test messages and wire it up."""
    db_path = tmp_path / "state.db"
    messages = [
        {"role": "user", "content": "Hello Alice!", "timestamp": "2026-03-29T12:00:01"},
        {"role": "assistant", "content": "Hi! How can I help?", "timestamp": "2026-03-29T12:00:05"},
        {"role": "user", "content": "Check the image MEDIA: /tmp/screenshot.png please",
         "timestamp": "2026-03-29T12:01:00"},
        {"role": "assistant", "content": "I see the screenshot. It shows a terminal.",
         "timestamp": "2026-03-29T12:01:10"},
        {"role": "tool", "content": '{"result": "ok"}', "timestamp": "2026-03-29T12:01:15"},
        {"role": "user", "content": "Thanks!", "timestamp": "2026-03-29T12:02:00"},
    ]
    _create_test_db(db_path, "20260329_120000_abc123", messages)

    # Create a mock SessionDB that reads from our test DB
    class TestSessionDB:
        def __init__(self):
            self._db_path = db_path

        def get_messages(self, session_id):
            conn = sqlite3.connect(str(self._db_path))
            conn.row_factory = sqlite3.Row
            rows = conn.execute(
                "SELECT * FROM messages WHERE session_id = ? ORDER BY id",
                (session_id,),
            ).fetchall()
            conn.close()
            result = []
            for r in rows:
                d = dict(r)
                if d.get("tool_calls"):
                    d["tool_calls"] = json.loads(d["tool_calls"])
                result.append(d)
            return result

    return TestSessionDB()


class _FakeTool:
    def __init__(self, fn):
        self.name = fn.__name__
        self.description = inspect.getdoc(fn) or ""
        self.fn = fn


class _FakeToolManager:
    def __init__(self):
        self._tools = {}

    def add_tool(self, fn):
        self._tools[fn.__name__] = _FakeTool(fn)

    async def call_tool(self, name, args=None):
        return self._tools[name].fn(**(args or {}))

    def list_tools(self):
        return list(self._tools.values())


class _FakeFastMCP:
    def __init__(self, *args, **kwargs):
        self._tool_manager = _FakeToolManager()

    def tool(self):
        def decorator(fn):
            self._tool_manager.add_tool(fn)
            return fn

        return decorator


@pytest.fixture
def fake_mcp_server(populated_sessions_dir, mock_session_db, monkeypatch):
    import mcp_serve

    monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: populated_sessions_dir)
    monkeypatch.setattr(mcp_serve, "_get_session_db", lambda: mock_session_db)
    monkeypatch.setattr(mcp_serve, "_load_channel_directory", lambda: {})
    monkeypatch.setattr(mcp_serve, "_MCP_SERVER_AVAILABLE", True)
    monkeypatch.setattr(mcp_serve, "FastMCP", _FakeFastMCP)

    bridge = mcp_serve.EventBridge()
    server = mcp_serve.create_mcp_server(event_bridge=bridge)
    return server, bridge


# ---------------------------------------------------------------------------
# 1. UNIT TESTS — helpers, extraction, attachments
# ---------------------------------------------------------------------------



class TestHelpers:

    def test_coerce_int_handles_invalid_and_out_of_range_values(self):
        from mcp_serve import _coerce_int

        assert _coerce_int(None, default=50, minimum=1, maximum=200) == 50
        assert _coerce_int("20", default=50, minimum=1, maximum=200) == 20
        assert _coerce_int("bad", default=50, minimum=1, maximum=200) == 50
        assert _coerce_int(999, default=50, minimum=1, maximum=200) == 200
        assert _coerce_int(-5, default=50, minimum=1, maximum=200) == 1



    def test_load_sessions_index_corrupt(self, sessions_dir, monkeypatch):
        (sessions_dir / "sessions.json").write_text("not json!")
        import mcp_serve
        monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: sessions_dir)
        assert mcp_serve._load_sessions_index() == {}


class TestContentExtraction:
    def test_text(self):
        from mcp_serve import _extract_message_content
        assert _extract_message_content({"content": "Hello"}) == "Hello"

    def test_multipart(self):
        from mcp_serve import _extract_message_content
        msg = {"content": [
            {"type": "text", "text": "A"},
            {"type": "image", "url": "http://x.com/i.png"},
            {"type": "text", "text": "B"},
        ]}
        assert _extract_message_content(msg) == "A\nB"



class TestAttachmentExtraction:
    def test_image_url_block(self):
        from mcp_serve import _extract_attachments
        msg = {"content": [
            {"type": "image_url", "image_url": {"url": "http://x.com/pic.jpg"}},
        ]}
        att = _extract_attachments(msg)
        assert len(att) == 1
        assert att[0] == {"type": "image", "url": "http://x.com/pic.jpg"}




    def test_image_content_block(self):
        from mcp_serve import _extract_attachments
        msg = {"content": [{"type": "image", "url": "http://x.com/p.png"}]}
        att = _extract_attachments(msg)
        assert att[0]["type"] == "image"


# ---------------------------------------------------------------------------
# 2. EVENT BRIDGE TESTS — queue, cursors, waiters, concurrency
# ---------------------------------------------------------------------------

class TestEventBridge:

    def test_enqueue_and_poll(self):
        from mcp_serve import EventBridge, QueueEvent
        b = EventBridge()
        b._enqueue(QueueEvent(cursor=0, type="message", session_key="k1",
                              data={"content": "hi"}))
        r = b.poll_events(after_cursor=0)
        assert len(r["events"]) == 1
        assert r["events"][0]["type"] == "message"
        assert r["next_cursor"] == 1




    def test_poll_limit(self):
        from mcp_serve import EventBridge, QueueEvent
        b = EventBridge()
        for i in range(10):
            b._enqueue(QueueEvent(cursor=0, type="message", session_key=f"s{i}"))
        r = b.poll_events(after_cursor=0, limit=3)
        assert len(r["events"]) == 3




    def test_queue_limit(self):
        from mcp_serve import EventBridge, QueueEvent, QUEUE_LIMIT
        b = EventBridge()
        for i in range(QUEUE_LIMIT + 50):
            b._enqueue(QueueEvent(cursor=0, type="message", session_key=f"s{i}"))
        assert len(b._queue) == QUEUE_LIMIT

    def test_concurrent_enqueue(self):
        from mcp_serve import EventBridge, QueueEvent
        b = EventBridge()
        errors = []

        def batch(start):
            try:
                for i in range(100):
                    b._enqueue(QueueEvent(cursor=0, type="message",
                                          session_key=f"s{start}_{i}"))
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=batch, args=(i,)) for i in range(5)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()
        assert not errors
        assert len(b._queue) == 500
        assert b._cursor == 500

    def test_approvals_lifecycle(self):
        from mcp_serve import EventBridge
        b = EventBridge()
        b._pending_approvals["a1"] = {
            "id": "a1", "kind": "exec",
            "description": "rm -rf /tmp",
            "session_key": "test", "created_at": "2026-03-29T12:00:00",
        }
        assert len(b.list_pending_approvals()) == 1
        result = b.respond_to_approval("a1", "deny")
        assert result["resolved"] is True
        assert len(b.list_pending_approvals()) == 0



# ---------------------------------------------------------------------------
# 3. END-TO-END TESTS — call MCP tools through FastMCP server
# ---------------------------------------------------------------------------

@pytest.fixture
def mcp_server_e2e(populated_sessions_dir, mock_session_db, monkeypatch):
    """Create a fully wired MCP server for E2E testing."""
    mcp = pytest.importorskip("mcp", reason="MCP SDK not installed")
    import mcp_serve
    monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: populated_sessions_dir)
    monkeypatch.setattr(mcp_serve, "_get_session_db", lambda: mock_session_db)
    monkeypatch.setattr(mcp_serve, "_load_channel_directory", lambda: {})

    bridge = mcp_serve.EventBridge()
    server = mcp_serve.create_mcp_server(event_bridge=bridge)
    return server, bridge


def _run_tool(server, name, args=None):
    """Call an MCP tool through FastMCP's tool manager and return parsed JSON."""
    result = asyncio.get_event_loop().run_until_complete(
        server._tool_manager.call_tool(name, args or {})
    )
    return json.loads(result) if isinstance(result, str) else result


@pytest.fixture
def _event_loop():
    """Ensure an event loop exists for sync tests calling async tools."""
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    yield loop
    loop.close()


class TestE2EConversationsList:
    def test_list_all(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "conversations_list")
        assert result["count"] == 3
        platforms = {c["platform"] for c in result["conversations"]}
        assert platforms == {"telegram", "discord", "slack"}


    def test_filter_by_platform(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "conversations_list", {"platform": "discord"})
        assert result["count"] == 1
        assert result["conversations"][0]["platform"] == "discord"




    def test_limit(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "conversations_list", {"limit": 2})
        assert result["count"] == 2


class TestE2EConversationGet:
    def test_get_existing(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "conversation_get",
                          {"session_key": "agent:main:telegram:dm:123456"})
        assert result["platform"] == "telegram"
        assert result["display_name"] == "Alice"
        assert result["chat_id"] == "123456"
        assert result["input_tokens"] == 50000

    def test_get_nonexistent(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "conversation_get",
                          {"session_key": "nonexistent:key"})
        assert "error" in result


class TestE2EMessagesRead:
    def test_read_messages(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "messages_read",
                          {"session_key": "agent:main:telegram:dm:123456"})
        assert result["count"] > 0
        # Should filter out tool messages — only user/assistant
        roles = {m["role"] for m in result["messages"]}
        assert "tool" not in roles
        assert "user" in roles
        assert "assistant" in roles



    def test_read_with_limit(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "messages_read",
                          {"session_key": "agent:main:telegram:dm:123456",
                           "limit": 2})
        assert result["count"] == 2

    def test_read_nonexistent_session(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "messages_read",
                          {"session_key": "nonexistent:key"})
        assert "error" in result


class TestE2EAttachmentsFetch:
    def test_fetch_media_from_message(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        # First get message IDs
        msgs = _run_tool(server, "messages_read",
                        {"session_key": "agent:main:telegram:dm:123456"})
        # Find the message with MEDIA: tag
        media_msg = None
        for m in msgs["messages"]:
            if "MEDIA:" in m["content"]:
                media_msg = m
                break
        assert media_msg is not None, "Should have a message with MEDIA: tag"

        result = _run_tool(server, "attachments_fetch", {
            "session_key": "agent:main:telegram:dm:123456",
            "message_id": media_msg["id"],
        })
        assert result["count"] >= 1
        assert result["attachments"][0]["type"] == "media"
        assert result["attachments"][0]["path"] == "/tmp/screenshot.png"




class TestE2EEventsPoll:

    def test_poll_with_events(self, mcp_server_e2e, _event_loop):
        from mcp_serve import QueueEvent
        server, bridge = mcp_server_e2e
        bridge._enqueue(QueueEvent(cursor=0, type="message",
                                   session_key="agent:main:telegram:dm:123456",
                                   data={"role": "user", "content": "Hello"}))
        bridge._enqueue(QueueEvent(cursor=0, type="message",
                                   session_key="agent:main:telegram:dm:123456",
                                   data={"role": "assistant", "content": "Hi"}))

        result = _run_tool(server, "events_poll")
        assert len(result["events"]) == 2
        assert result["events"][0]["content"] == "Hello"
        assert result["events"][1]["content"] == "Hi"
        assert result["next_cursor"] == 2

    def test_poll_cursor_pagination(self, mcp_server_e2e, _event_loop):
        from mcp_serve import QueueEvent
        server, bridge = mcp_server_e2e
        for i in range(5):
            bridge._enqueue(QueueEvent(cursor=0, type="message",
                                       session_key=f"s{i}"))

        page1 = _run_tool(server, "events_poll", {"limit": 2})
        assert len(page1["events"]) == 2
        assert page1["next_cursor"] == 2

        page2 = _run_tool(server, "events_poll",
                         {"after_cursor": page1["next_cursor"], "limit": 2})
        assert len(page2["events"]) == 2
        assert page2["next_cursor"] == 4

    def test_poll_session_filter(self, mcp_server_e2e, _event_loop):
        from mcp_serve import QueueEvent
        server, bridge = mcp_server_e2e
        bridge._enqueue(QueueEvent(cursor=0, type="message", session_key="a"))
        bridge._enqueue(QueueEvent(cursor=0, type="message", session_key="b"))
        bridge._enqueue(QueueEvent(cursor=0, type="message", session_key="a"))

        result = _run_tool(server, "events_poll",
                          {"session_key": "b"})
        assert len(result["events"]) == 1


class TestE2EEventsWait:
    def test_wait_timeout(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "events_wait", {"timeout_ms": 100})
        assert result["event"] is None
        assert result["reason"] == "timeout"



class TestMCPToolParameterCoercion:
    def test_conversations_list_coerces_string_limit(self, fake_mcp_server, _event_loop):
        server, _ = fake_mcp_server
        result = _run_tool(server, "conversations_list", {"limit": "2"})
        assert result["count"] == 2


    def test_events_poll_coerces_string_cursor_and_limit(self, fake_mcp_server, _event_loop):
        from mcp_serve import QueueEvent

        server, bridge = fake_mcp_server
        bridge._enqueue(QueueEvent(cursor=0, type="message", session_key="a"))
        bridge._enqueue(QueueEvent(cursor=0, type="message", session_key="b"))

        result = _run_tool(server, "events_poll", {"after_cursor": "0", "limit": "1"})
        assert len(result["events"]) == 1
        assert result["next_cursor"] == 1

    def test_events_wait_coerces_invalid_timeout(self, fake_mcp_server, _event_loop):
        from mcp_serve import QueueEvent

        server, bridge = fake_mcp_server
        bridge._enqueue(
            QueueEvent(
                cursor=0,
                type="message",
                session_key="test",
                data={"content": "waiting for this"},
            )
        )

        result = _run_tool(server, "events_wait", {"after_cursor": "0", "timeout_ms": "bad"})
        assert result["event"] is not None
        assert result["event"]["content"] == "waiting for this"


class TestE2EMessagesSend:
    def test_send_missing_args(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "messages_send", {"target": "", "message": "hi"})
        assert "error" in result

    def test_send_delegates_to_tool(self, mcp_server_e2e, _event_loop, monkeypatch):
        server, _ = mcp_server_e2e
        mock = MagicMock(return_value=json.dumps({"success": True, "platform": "telegram"}))
        monkeypatch.setattr("tools.send_message_tool.send_message_tool", mock)

        result = _run_tool(server, "messages_send",
                          {"target": "telegram:123456", "message": "Hello!"})
        assert result["success"] is True
        mock.assert_called_once()
        call_args = mock.call_args[0][0]
        assert call_args["action"] == "send"
        assert call_args["target"] == "telegram:123456"


class TestE2EChannelsList:
    def test_channels_from_sessions(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "channels_list")
        assert result["count"] == 3
        targets = {c["target"] for c in result["channels"]}
        assert "telegram:123456" in targets
        assert "discord:789" in targets
        assert "slack:C1234" in targets

    def test_channels_platform_filter(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        result = _run_tool(server, "channels_list", {"platform": "slack"})
        assert result["count"] == 1
        assert result["channels"][0]["target"] == "slack:C1234"




class TestE2EPermissions:

    def test_list_with_approvals(self, mcp_server_e2e, _event_loop):
        server, bridge = mcp_server_e2e
        bridge._pending_approvals["a1"] = {
            "id": "a1", "kind": "exec",
            "description": "sudo rm -rf /",
            "session_key": "test",
            "created_at": "2026-03-29T12:00:00",
        }
        result = _run_tool(server, "permissions_list_open")
        assert result["count"] == 1
        assert result["approvals"][0]["id"] == "a1"

    def test_respond_allow(self, mcp_server_e2e, _event_loop):
        server, bridge = mcp_server_e2e
        bridge._pending_approvals["a1"] = {"id": "a1", "kind": "exec"}
        result = _run_tool(server, "permissions_respond",
                          {"id": "a1", "decision": "allow-once"})
        assert result["resolved"] is True
        assert result["decision"] == "allow-once"
        # Should be gone now
        check = _run_tool(server, "permissions_list_open")
        assert check["count"] == 0


    def test_respond_invalid_decision(self, mcp_server_e2e, _event_loop):
        server, bridge = mcp_server_e2e
        bridge._pending_approvals["a3"] = {"id": "a3", "kind": "exec"}
        result = _run_tool(server, "permissions_respond",
                          {"id": "a3", "decision": "maybe"})
        assert "error" in result



# ---------------------------------------------------------------------------
# 4. TOOL LISTING — verify all 10 tools are registered
# ---------------------------------------------------------------------------

class TestToolRegistration:
    def test_all_tools_registered(self, mcp_server_e2e, _event_loop):
        server, _ = mcp_server_e2e
        tools = server._tool_manager.list_tools()
        tool_names = {t.name for t in tools}

        expected = {
            "conversations_list", "conversation_get", "messages_read",
            "attachments_fetch", "events_poll", "events_wait",
            "messages_send", "channels_list",
            "permissions_list_open", "permissions_respond",
        }
        assert expected == tool_names, f"Missing: {expected - tool_names}, Extra: {tool_names - expected}"



# ---------------------------------------------------------------------------
# 5. SERVER LIFECYCLE / CLI INTEGRATION
# ---------------------------------------------------------------------------

class TestServerCreation:

    def test_create_with_bridge(self, populated_sessions_dir, monkeypatch):
        pytest.importorskip("mcp", reason="MCP SDK not installed")
        import mcp_serve
        monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: populated_sessions_dir)
        bridge = mcp_serve.EventBridge()
        assert mcp_serve.create_mcp_server(event_bridge=bridge) is not None

    def test_create_without_mcp_sdk(self, monkeypatch):
        import mcp_serve
        monkeypatch.setattr(mcp_serve, "_MCP_SERVER_AVAILABLE", False)
        with pytest.raises(ImportError, match="MCP server requires"):
            mcp_serve.create_mcp_server()


class TestRunMcpServer:
    def test_run_without_mcp_exits(self, monkeypatch):
        import mcp_serve
        monkeypatch.setattr(mcp_serve, "_MCP_SERVER_AVAILABLE", False)
        with pytest.raises(SystemExit) as exc_info:
            mcp_serve.run_mcp_server()
        assert exc_info.value.code == 1


class TestCliIntegration:
    def test_parse_serve(self):
        import argparse
        parser = argparse.ArgumentParser()
        subs = parser.add_subparsers(dest="command")
        mcp_p = subs.add_parser("mcp")
        mcp_sub = mcp_p.add_subparsers(dest="mcp_action")
        serve_p = mcp_sub.add_parser("serve")
        serve_p.add_argument("-v", "--verbose", action="store_true")

        args = parser.parse_args(["mcp", "serve"])
        assert args.mcp_action == "serve"
        assert args.verbose is False

    def test_parse_serve_verbose(self):
        import argparse
        parser = argparse.ArgumentParser()
        subs = parser.add_subparsers(dest="command")
        mcp_p = subs.add_parser("mcp")
        mcp_sub = mcp_p.add_subparsers(dest="mcp_action")
        serve_p = mcp_sub.add_parser("serve")
        serve_p.add_argument("-v", "--verbose", action="store_true")

        args = parser.parse_args(["mcp", "serve", "--verbose"])
        assert args.verbose is True

    def test_dispatcher_routes_serve(self, monkeypatch, tmp_path):
        monkeypatch.setenv("HERMES_HOME", str(tmp_path))
        mock_run = MagicMock()
        monkeypatch.setattr("mcp_serve.run_mcp_server", mock_run)

        import argparse
        args = argparse.Namespace(mcp_action="serve", verbose=True)
        from hermes_cli.mcp_config import mcp_command
        mcp_command(args)
        mock_run.assert_called_once_with(verbose=True)


# ---------------------------------------------------------------------------
# 6. EDGE CASES
# ---------------------------------------------------------------------------

class TestEdgeCases:

    def test_sessions_without_origin(self, sessions_dir, monkeypatch):
        data = {"agent:main:telegram:dm:111": {
            "session_key": "agent:main:telegram:dm:111",
            "session_id": "20260329_120000_xyz",
            "platform": "telegram",
            "updated_at": "2026-03-29T12:00:00",
        }}
        (sessions_dir / "sessions.json").write_text(json.dumps(data))
        import mcp_serve
        monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: sessions_dir)
        entries = mcp_serve._load_sessions_index()
        assert entries["agent:main:telegram:dm:111"]["platform"] == "telegram"

    def test_bridge_start_stop(self):
        from mcp_serve import EventBridge
        b = EventBridge()
        assert not b._running
        b._running = True
        b.stop()
        assert not b._running

    def test_truncation(self):
        assert len(("x" * 5000)[:2000]) == 2000


# ---------------------------------------------------------------------------
# 7. EVENT BRIDGE POLL LOOP E2E — real SQLite DB, mtime optimization
# ---------------------------------------------------------------------------

class TestEventBridgePollE2E:
    """End-to-end tests for the EventBridge polling loop with real files."""

    def test_poll_detects_new_messages(self, tmp_path, monkeypatch):
        """Write to SQLite + sessions.json, verify EventBridge picks it up."""
        import mcp_serve
        sessions_dir = tmp_path / "sessions"
        sessions_dir.mkdir()
        monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: sessions_dir)

        session_id = "20260329_150000_poll_test"
        db_path = tmp_path / "state.db"

        # Write sessions.json
        sessions_data = {
            "agent:main:telegram:dm:poll_test": {
                "session_key": "agent:main:telegram:dm:poll_test",
                "session_id": session_id,
                "platform": "telegram",
                "chat_type": "dm",
                "display_name": "PollTest",
                "updated_at": "2026-03-29T15:00:05",
                "origin": {"platform": "telegram", "chat_id": "poll_test"},
            }
        }
        (sessions_dir / "sessions.json").write_text(json.dumps(sessions_data))

        # Write messages to SQLite
        messages = [
            {"role": "user", "content": "First message",
             "timestamp": "2026-03-29T15:00:01"},
            {"role": "assistant", "content": "Reply",
             "timestamp": "2026-03-29T15:00:03"},
        ]
        _create_test_db(db_path, session_id, messages)

        # Create a mock SessionDB that reads our test DB
        class TestDB:
            def get_messages(self, sid):
                conn = sqlite3.connect(str(db_path))
                conn.row_factory = sqlite3.Row
                rows = conn.execute(
                    "SELECT * FROM messages WHERE session_id = ? ORDER BY id",
                    (sid,),
                ).fetchall()
                conn.close()
                return [dict(r) for r in rows]

        monkeypatch.setattr(mcp_serve, "_get_session_db", lambda: TestDB())

        bridge = mcp_serve.EventBridge()
        # Run one poll cycle manually
        bridge._poll_once(TestDB())

        # Should have found the messages
        result = bridge.poll_events(after_cursor=0)
        assert len(result["events"]) == 2
        assert result["events"][0]["role"] == "user"
        assert result["events"][0]["content"] == "First message"
        assert result["events"][1]["role"] == "assistant"

    def test_poll_skips_when_unchanged(self, tmp_path, monkeypatch):
        """Second poll with no file changes should be a no-op."""
        import mcp_serve
        sessions_dir = tmp_path / "sessions"
        sessions_dir.mkdir()
        monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: sessions_dir)

        session_id = "20260329_150000_skip_test"
        db_path = tmp_path / "state.db"

        sessions_data = {
            "agent:main:telegram:dm:skip": {
                "session_key": "agent:main:telegram:dm:skip",
                "session_id": session_id,
                "platform": "telegram",
                "updated_at": "2026-03-29T15:00:05",
                "origin": {"platform": "telegram", "chat_id": "skip"},
            }
        }
        (sessions_dir / "sessions.json").write_text(json.dumps(sessions_data))
        _create_test_db(db_path, session_id, [
            {"role": "user", "content": "Hello", "timestamp": "2026-03-29T15:00:01"},
        ])

        class TestDB:
            def __init__(self):
                self.call_count = 0

            def get_messages(self, sid):
                self.call_count += 1
                conn = sqlite3.connect(str(db_path))
                conn.row_factory = sqlite3.Row
                rows = conn.execute(
                    "SELECT * FROM messages WHERE session_id = ? ORDER BY id",
                    (sid,),
                ).fetchall()
                conn.close()
                return [dict(r) for r in rows]

        db = TestDB()
        bridge = mcp_serve.EventBridge()

        # First poll — should process
        bridge._poll_once(db)
        first_calls = db.call_count
        assert first_calls >= 1

        # Second poll — files unchanged, should skip entirely
        bridge._poll_once(db)
        assert db.call_count == first_calls, \
            "Second poll should skip DB queries when files unchanged"


    def test_poll_picks_up_new_conversation_on_db_change(
        self, tmp_path, monkeypatch
    ):
        """A brand-new conversation must be picked up on the tick where
        state.db changes.

        Since #9006 the routing index lives IN state.db (session rows carry
        session_key/origin metadata), so a new conversation's registration and
        its first message land in the same file — a single mtime check covers
        both and the old dual-file (sessions.json + state.db) race (#8925) is
        structurally impossible. This test asserts the index is refreshed on a
        db-mtime bump, so a conversation the bridge has never seen before is
        emitted on the same tick.
        """
        import mcp_serve

        sessions_dir = tmp_path / "sessions"
        sessions_dir.mkdir()
        monkeypatch.setattr(mcp_serve, "_get_sessions_dir", lambda: sessions_dir)

        # _poll_once reads <HERMES_HOME>/state.db for its mtime gate; the autouse
        # fixture points HERMES_HOME at tmp_path.
        db_path = tmp_path / "state.db"
        db_path.write_text("placeholder")

        session_id = "20260329_150000_late_register"
        # The routing index now comes from _load_sessions_index() (state.db
        # primary, sessions.json fallback). Stub it to return the new
        # conversation, simulating the gateway having just written the
        # session row + first message in one state.db transaction.
        monkeypatch.setattr(
            mcp_serve, "_load_sessions_index",
            lambda: {
                "agent:main:telegram:dm:late": {
                    "session_id": session_id,
                    "platform": "telegram",
                    "origin": {"platform": "telegram", "chat_id": "late"},
                }
            },
        )

        class DB:
            def get_messages(self, sid):
                return [{
                    "id": 1, "role": "user",
                    "content": "Hello from a freshly-registered conversation",
                    "timestamp": "2026-03-29T15:00:00",
                }]

        bridge = mcp_serve.EventBridge()
        # Bridge has never seen this db state (mtime differs) and has an
        # empty cached index — exactly the state after a new conversation's
        # first write.
        bridge._state_db_mtime = 0.0
        assert bridge._cached_sessions_index == {}

        bridge._poll_once(DB())

        result = bridge.poll_events(after_cursor=0)
        assert len(result["events"]) == 1
        assert result["events"][0]["session_key"] == "agent:main:telegram:dm:late"
        assert result["events"][0]["content"].startswith("Hello from a freshly")

