"""Comprehensive E2E tests for PeerDrop service layer.

Tests the full pipeline: DaemonClient → IPC → DaemonServer → RequestHandler → ServiceInterface
"""

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

import pytest
import trio

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.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"


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

class TestHealthPipeline:
    """E2E: client.health() → IPC → handler → service.get_identity()"""

    @pytest.mark.trio
    async def test_health_returns_ok(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:
    """E2E: client.get_identity() → IPC → handler → service.get_identity()"""

    @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:
    """E2E: client.discover_peers() → IPC → handler → service.discover_peers()"""

    @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:
    """E2E: client.get_peer() → IPC → handler → service.get_peer()"""

    @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"
            assert result["data"]["name"] == "Target"

            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()

    @pytest.mark.trio
    async def test_get_peer_missing_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("get_peer", params={})
            assert result["ok"] is False
            assert "Missing" in result["error"]

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


class TestConnectPeerPipeline:
    """E2E: client.connect_peer() → IPC → handler → service.connect_peer()"""

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

        async def fake_connect(addr):
            return PeerInfo(peer_id="QmConnected", addrs=[addr], name="NewPeer", 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:
    """E2E: client.ping() → IPC → handler → service.ping_peer()"""

    @pytest.mark.trio
    async def test_ping_success(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_with_loss(self, tmp_sock):
        service = _make_mock_service()
        service.ping_peer = AsyncMock(return_value=PingResult(
            peer_id="QmFlaky",
            count=3,
            latencies_ms=[10.0, -1, 12.0],
            avg_ms=11.0,
            loss_pct=33.3,
        ))
        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("QmFlaky", count=3)
            assert result["ok"] is True
            assert result["data"]["loss_pct"] == 33.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:
    """E2E: client.send_file() → IPC → handler → service.send_file()"""

    @pytest.mark.trio
    async def test_send_file_success(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 'file'" in result["error"]

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

    @pytest.mark.trio
    async def test_send_file_missing_target(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={"file": "test.txt"})
            assert result["ok"] is False
            assert "Missing" in result["error"]

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


class TestListTransfersPipeline:
    """E2E: client.list_transfers() → IPC → handler → service.list_transfers()"""

    @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[0]["status"] == "completed"
            assert transfers[1]["transfer_id"] == "t2"
            assert transfers[1]["progress"] == 0.5

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


class TestGetTransferPipeline:
    """E2E: client.get_transfer() → IPC → handler → service.get_transfer()"""

    @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"
            assert result["data"]["root_cid"] == "QmDoc"

            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()

    @pytest.mark.trio
    async def test_get_transfer_missing_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("get_transfer", params={})
            assert result["ok"] is False
            assert "Missing" in result["error"]

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


class TestCancelTransferPipeline:
    """E2E: client.cancel_transfer() → IPC → handler → service.cancel_transfer()"""

    @pytest.mark.trio
    async def test_cancel_transfer_success(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:
    """E2E: get/set download dir through full pipeline."""

    @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"
            service.set_download_dir.assert_called_once_with("/new/downloads")

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

    @pytest.mark.trio
    async def test_set_download_dir_missing(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("set_download_dir", params={})
            assert result["ok"] is False
            assert "Missing" in result["error"]

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


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

class TestMultipleClients:
    """E2E: Multiple clients connecting simultaneously."""

    @pytest.mark.trio
    async def test_two_clients_independent(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"
            assert r2["data"]["peer_id"] == "QmShared"

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

    @pytest.mark.trio
    async def test_sequential_requests_on_one_connection(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()

            # Send 5 different requests on same connection
            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:
    """E2E: Error handling through full pipeline."""

    @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()

            result = await client.discover_peers()
            # The error should be caught and returned as error response
            # discover_peers returns [] on error (client side)
            # But the raw request should fail
            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:
    """E2E: Client connection error handling."""

    @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()
