"""Comprehensive E2E tests for PeerDrop after restructuring.

Tests the full pipeline: DaemonClient → IPC → DaemonServer → RequestHandler → ServiceInterface
with the new module organization.
"""

import tempfile
from pathlib import Path
from unittest.mock import MagicMock, AsyncMock

import pytest
import trio

from peerdrop.constants import DEFAULT_SOCK_PATH, DEFAULT_PID_PATH, DEFAULT_DOWNLOAD_DIR
from peerdrop.core.service import ServiceInterface, IdentityInfo, PeerInfo, TransferInfo, PingResult
from peerdrop.core.models import Peer, Transfer, TransferStatus
from peerdrop.daemon.server import DaemonServer
from peerdrop.daemon.handler import RequestHandler, VALID_ACTIONS
from peerdrop.daemon.protocol import make_request, unpack_response
from peerdrop.utils.framing import length_prefix_pack, length_prefix_unpack
from peerdrop.interfaces.cli.client import DaemonClient


def _make_mock_service(
    peer_id: str = "QmTestNode",
    addrs: list[str] | None = None,
) -> MagicMock:
    """Create a mock ServiceInterface for E2E tests."""
    mock = MagicMock(spec=ServiceInterface)
    mock.get_identity.return_value = IdentityInfo(
        peer_id=peer_id,
        addrs=addrs or [f"/ip4/127.0.0.1/tcp/4001/p2p/{peer_id}"],
    )
    mock.discover_peers.return_value = []
    mock.list_transfers.return_value = []
    return mock


@pytest.fixture
def tmp_sock():
    """Provide a temporary socket path."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir) / "test.sock"


# ─── Constants Module Tests ──────────────────────────────────

class TestConstants:
    def test_sock_path_is_path(self):
        assert isinstance(DEFAULT_SOCK_PATH, Path)

    def test_pid_path_is_path(self):
        assert isinstance(DEFAULT_PID_PATH, Path)

    def test_download_dir_is_string(self):
        assert isinstance(DEFAULT_DOWNLOAD_DIR, str)

    def test_sock_and_pid_same_dir(self):
        assert DEFAULT_PID_PATH.parent == DEFAULT_SOCK_PATH.parent


# ─── Framing Integration Tests ───────────────────────────────

class TestFramingIntegration:
    """Test framing works correctly in IPC context."""

    @pytest.mark.trio
    async def test_framing_through_socket(self, tmp_sock):
        service = _make_mock_service()
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            stream = await trio.open_unix_socket(str(tmp_sock))

            # Send request using framing
            request = make_request("health", request_id="test-1")
            await stream.send_all(length_prefix_pack(request))

            # Read response using framing
            response_data = await stream.receive_some(4096)
            result = length_prefix_unpack(response_data)
            assert result is not None
            msg, _ = result
            response = unpack_response(msg)
            assert response["ok"] is True

            await stream.aclose()
            nursery.cancel_scope.cancel()

    def test_framing_multiple_messages(self):
        """Test multiple messages can be framed correctly."""
        msgs = [
            length_prefix_pack(b"msg1"),
            length_prefix_pack(b"msg2"),
            length_prefix_pack(b"msg3"),
        ]
        buffer = b"".join(msgs)

        for expected in [b"msg1", b"msg2", b"msg3"]:
            result = length_prefix_unpack(buffer)
            assert result is not None
            msg, buffer = result
            assert msg == expected
        assert buffer == b""


# ─── Handler Validation Tests ────────────────────────────────

class TestHandlerValidation:
    @pytest.mark.trio
    async def test_all_valid_actions_registered(self):
        service = _make_mock_service()
        handler = RequestHandler(service)
        assert len(VALID_ACTIONS) == 17

    @pytest.mark.trio
    async def test_invalid_action(self):
        service = _make_mock_service()
        handler = RequestHandler(service)
        request = make_request("nonexistent", request_id="r1")
        response = await handler.handle(request)
        msg = unpack_response(response)
        assert msg["ok"] is False
        assert "Unknown action" in msg["error"]

    @pytest.mark.trio
    async def test_invalid_msgpack(self):
        service = _make_mock_service()
        handler = RequestHandler(service)
        response = await handler.handle(b"not valid msgpack")
        msg = unpack_response(response)
        assert msg["ok"] is False
        assert "Invalid message" in msg["error"]


# ─── Full Pipeline Tests ─────────────────────────────────────

class TestHealthPipeline:
    @pytest.mark.trio
    async def test_health(self, tmp_sock):
        service = _make_mock_service(peer_id="QmHealthTest")
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.health()
            assert result["ok"] is True
            assert result["data"]["status"] == "ok"
            assert result["data"]["peer_id"] == "QmHealthTest"

            await client.close()
            nursery.cancel_scope.cancel()


class TestIdentityPipeline:
    @pytest.mark.trio
    async def test_get_identity(self, tmp_sock):
        addrs = ["/ip4/127.0.0.1/tcp/4001", "/ip4/192.168.1.100/tcp/4001"]
        service = _make_mock_service(peer_id="QmIdentityTest", addrs=addrs)
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.get_identity()
            assert result["ok"] is True
            assert result["data"]["peer_id"] == "QmIdentityTest"
            assert result["data"]["addrs"] == addrs

            await client.close()
            nursery.cancel_scope.cancel()


class TestDiscoverPeersPipeline:
    @pytest.mark.trio
    async def test_discover_empty(self, tmp_sock):
        service = _make_mock_service()
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            peers = await client.discover_peers()
            assert peers == []

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_discover_with_peers(self, tmp_sock):
        service = _make_mock_service()
        service.discover_peers.return_value = [
            PeerInfo(peer_id="QmPeer1", addrs=["/ip4/1.2.3.4/tcp/4001"], name="Phone", is_online=True),
            PeerInfo(peer_id="QmPeer2", addrs=["/ip4/5.6.7.8/tcp/4002"], name="Laptop", is_online=True),
        ]
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            peers = await client.discover_peers()
            assert len(peers) == 2
            assert peers[0]["peer_id"] == "QmPeer1"
            assert peers[0]["name"] == "Phone"
            assert peers[1]["peer_id"] == "QmPeer2"

            await client.close()
            nursery.cancel_scope.cancel()


class TestGetPeerPipeline:
    @pytest.mark.trio
    async def test_get_peer_found(self, tmp_sock):
        service = _make_mock_service()
        service.get_peer.return_value = PeerInfo(
            peer_id="QmTarget", addrs=["/ip4/10.0.0.1/tcp/4001"], name="Target", is_online=True
        )
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.get_peer("QmTarget")
            assert result["ok"] is True
            assert result["data"]["peer_id"] == "QmTarget"

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_get_peer_not_found(self, tmp_sock):
        service = _make_mock_service()
        service.get_peer.return_value = None
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.get_peer("QmNonexistent")
            assert result["ok"] is False
            assert "not found" in result["error"]

            await client.close()
            nursery.cancel_scope.cancel()


class TestConnectPeerPipeline:
    @pytest.mark.trio
    async def test_connect_peer(self, tmp_sock):
        service = _make_mock_service()

        async def fake_connect(addr):
            return PeerInfo(peer_id="QmConnected", addrs=[addr], name="New", is_online=True)

        service.connect_peer = fake_connect
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.connect_peer("/ip4/1.2.3.4/tcp/4001/p2p/QmConnected")
            assert result["ok"] is True
            assert result["data"]["peer_id"] == "QmConnected"

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_connect_peer_missing_addr(self, tmp_sock):
        service = _make_mock_service()
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.request("connect_peer", params={})
            assert result["ok"] is False
            assert "Missing" in result["error"]

            await client.close()
            nursery.cancel_scope.cancel()


class TestPingPipeline:
    @pytest.mark.trio
    async def test_ping(self, tmp_sock):
        service = _make_mock_service()
        service.ping_peer = AsyncMock(return_value=PingResult(
            peer_id="QmTarget", count=3,
            latencies_ms=[10.5, 12.3, 11.1], avg_ms=11.3, loss_pct=0.0,
        ))
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.ping("QmTarget", count=3)
            assert result["ok"] is True
            assert result["data"]["peer_id"] == "QmTarget"
            assert len(result["data"]["latencies_ms"]) == 3
            assert result["data"]["avg_ms"] == 11.3

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_ping_missing_peer_id(self, tmp_sock):
        service = _make_mock_service()
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.request("ping", params={"count": 1})
            assert result["ok"] is False
            assert "Missing" in result["error"]

            await client.close()
            nursery.cancel_scope.cancel()


class TestSendFilePipeline:
    @pytest.mark.trio
    async def test_send_file(self, tmp_sock):
        service = _make_mock_service()
        service.send_file = AsyncMock(return_value=TransferInfo(
            transfer_id="abc123", file_name="test.txt", file_size=1024,
            status="pending", progress=0.0, root_cid="QmRoot",
        ))
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.send_file("/path/to/test.txt", "QmTarget")
            assert result["ok"] is True
            assert result["data"]["transfer_id"] == "abc123"
            assert result["data"]["file_name"] == "test.txt"

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_send_file_missing_file(self, tmp_sock):
        service = _make_mock_service()
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.request("send_file", params={"target": "Qm123"})
            assert result["ok"] is False
            assert "Missing" in result["error"]

            await client.close()
            nursery.cancel_scope.cancel()


class TestListTransfersPipeline:
    @pytest.mark.trio
    async def test_list_empty(self, tmp_sock):
        service = _make_mock_service()
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            transfers = await client.list_transfers()
            assert transfers == []

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_list_with_transfers(self, tmp_sock):
        service = _make_mock_service()
        service.list_transfers.return_value = [
            TransferInfo(transfer_id="t1", file_name="a.txt", file_size=100, status="completed", progress=1.0),
            TransferInfo(transfer_id="t2", file_name="b.bin", file_size=200, status="transferring", progress=0.5),
        ]
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            transfers = await client.list_transfers()
            assert len(transfers) == 2
            assert transfers[0]["transfer_id"] == "t1"
            assert transfers[1]["progress"] == 0.5

            await client.close()
            nursery.cancel_scope.cancel()


class TestGetTransferPipeline:
    @pytest.mark.trio
    async def test_get_transfer_found(self, tmp_sock):
        service = _make_mock_service()
        service.get_transfer.return_value = TransferInfo(
            transfer_id="abc", file_name="doc.pdf", file_size=4096,
            status="pending", progress=0.0, root_cid="QmDoc",
        )
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.get_transfer("abc")
            assert result["ok"] is True
            assert result["data"]["file_name"] == "doc.pdf"

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_get_transfer_not_found(self, tmp_sock):
        service = _make_mock_service()
        service.get_transfer.return_value = None
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.get_transfer("nonexistent")
            assert result["ok"] is False
            assert "not found" in result["error"]

            await client.close()
            nursery.cancel_scope.cancel()


class TestCancelTransferPipeline:
    @pytest.mark.trio
    async def test_cancel_transfer(self, tmp_sock):
        service = _make_mock_service()
        service.cancel_transfer.return_value = True
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.cancel_transfer("abc")
            assert result["ok"] is True
            assert result["data"]["transfer_id"] == "abc"

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_cancel_transfer_not_found(self, tmp_sock):
        service = _make_mock_service()
        service.cancel_transfer.return_value = False
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.cancel_transfer("nonexistent")
            assert result["ok"] is False

            await client.close()
            nursery.cancel_scope.cancel()


class TestDownloadDirPipeline:
    @pytest.mark.trio
    async def test_get_download_dir(self, tmp_sock):
        service = _make_mock_service()
        service.get_download_dir.return_value = "/home/user/downloads"
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.get_download_dir()
            assert result == "/home/user/downloads"

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_set_download_dir(self, tmp_sock):
        service = _make_mock_service()
        service.set_download_dir.return_value = "/new/downloads"
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.set_download_dir("/new/downloads")
            assert result == "/new/downloads"

            await client.close()
            nursery.cancel_scope.cancel()


# ─── Multiple Client Tests ───────────────────────────────────

class TestMultipleClients:
    @pytest.mark.trio
    async def test_two_clients(self, tmp_sock):
        service = _make_mock_service(peer_id="QmShared")
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            c1 = DaemonClient(tmp_sock)
            c2 = DaemonClient(tmp_sock)
            await c1.connect()
            await c2.connect()

            r1 = await c1.health()
            r2 = await c2.health()
            assert r1["ok"] is True
            assert r2["ok"] is True
            assert r1["data"]["peer_id"] == "QmShared"

            await c1.close()
            await c2.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_sequential_requests(self, tmp_sock):
        service = _make_mock_service(addrs=["/ip4/127.0.0.1/tcp/4001"])
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            r1 = await client.health()
            assert r1["ok"] is True

            r2 = await client.get_identity()
            assert r2["ok"] is True

            r3 = await client.discover_peers()
            assert isinstance(r3, list)

            r4 = await client.list_transfers()
            assert isinstance(r4, list)

            r5 = await client.get_download_dir()
            assert isinstance(r5, str)

            await client.close()
            nursery.cancel_scope.cancel()


# ─── Error Handling Tests ─────────────────────────────────────

class TestErrorHandling:
    @pytest.mark.trio
    async def test_unknown_action(self, tmp_sock):
        service = _make_mock_service()
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            result = await client.request("nonexistent_action")
            assert result["ok"] is False
            assert "Unknown action" in result["error"]

            await client.close()
            nursery.cancel_scope.cancel()

    @pytest.mark.trio
    async def test_service_error_propagates(self, tmp_sock):
        service = _make_mock_service()
        service.discover_peers.side_effect = RuntimeError("Engine exploded")
        server = DaemonServer(service, sock_path=tmp_sock)

        async with trio.open_nursery() as nursery:
            await nursery.start(server.serve)

            client = DaemonClient(tmp_sock)
            await client.connect()

            raw = await client.request("discover_peers")
            assert raw["ok"] is False
            assert "Engine exploded" in raw["error"]

            await client.close()
            nursery.cancel_scope.cancel()


# ─── Client Connection Error Tests ────────────────────────────

class TestClientConnectionErrors:
    @pytest.mark.trio
    async def test_not_connected_error(self):
        client = DaemonClient()
        with pytest.raises(ConnectionError, match="Not connected"):
            await client.health()

    @pytest.mark.trio
    async def test_daemon_not_running_error(self):
        client = DaemonClient("/tmp/nonexistent_peerdrop_test.sock")
        with pytest.raises(ConnectionError, match="Daemon not running"):
            await client.connect()


# ─── Import Verification Tests ────────────────────────────────

class TestImports:
    """Verify all modules import correctly after restructuring."""

    def test_import_constants(self):
        from peerdrop.constants import DEFAULT_SOCK_PATH, DEFAULT_PID_PATH
        assert DEFAULT_SOCK_PATH is not None

    def test_import_framing(self):
        from peerdrop.utils.framing import length_prefix_pack, length_prefix_unpack
        assert length_prefix_pack is not None

    def test_import_service(self):
        from peerdrop.core.service import ServiceInterface
        assert ServiceInterface is not None

    def test_import_handler(self):
        from peerdrop.daemon.handler import RequestHandler, VALID_ACTIONS
        assert len(VALID_ACTIONS) == 17

    def test_import_server(self):
        from peerdrop.daemon.server import DaemonServer
        assert DaemonServer is not None

    def test_import_client(self):
        from peerdrop.interfaces.cli.client import DaemonClient
        assert DaemonClient is not None

    def test_import_rest(self):
        from peerdrop.interfaces.rest.app import RestServer
        assert RestServer is not None

    def test_import_mcp(self):
        from peerdrop.interfaces.mcp.server import run
        assert run is not None

    def test_no_stale_daemon_rest_import(self):
        """Verify daemon/rest.py no longer exists."""
        import importlib
        with pytest.raises(ModuleNotFoundError):
            importlib.import_module("peerdrop.daemon.rest")

    def test_no_stale_mcp_import(self):
        """Verify peerdrop/mcp/ no longer exists."""
        import importlib
        with pytest.raises(ModuleNotFoundError):
            importlib.import_module("peerdrop.mcp.server")
