"""Comprehensive unit tests for PeerDrop ServiceInterface."""

import pytest
from unittest.mock import MagicMock, AsyncMock, patch

from peerdrop.core.service import (
    ServiceInterface,
    IdentityInfo,
    PeerInfo,
    TransferInfo,
    PingResult,
)
from peerdrop.core.models import Peer, Transfer, TransferStatus


# ─── Fixtures ────────────────────────────────────────────────

def _make_engine():
    """Create a mock PeerEngine."""
    engine = MagicMock()
    engine._started = False
    engine.get_peer_id.return_value = "QmLocalPeer"
    engine.get_addrs.return_value = ["/ip4/127.0.0.1/tcp/4001"]
    engine.get_download_dir.return_value = "/tmp/downloads"
    engine.discover_peers.return_value = []
    engine.list_transfers.return_value = []
    engine.get_transfer.return_value = None
    engine.get_peer.return_value = None
    return engine


def _make_service(**engine_overrides):
    """Create a ServiceInterface with a mock engine."""
    engine = _make_engine()
    for k, v in engine_overrides.items():
        setattr(engine, k, v)
    return ServiceInterface(engine)


# ─── Identity tests ──────────────────────────────────────────

class TestIdentity:
    def test_get_identity(self):
        service = _make_service()
        identity = service.get_identity()
        assert identity.peer_id == "QmLocalPeer"
        assert identity.addrs == ["/ip4/127.0.0.1/tcp/4001"]

    def test_get_identity_multiple_addrs(self):
        addrs = ["/ip4/127.0.0.1/tcp/4001", "/ip4/192.168.1.1/tcp/4001"]
        service = _make_service()
        service._engine.get_addrs.return_value = addrs
        identity = service.get_identity()
        identity = service.get_identity()
        assert len(identity.addrs) == 2


# ─── Lifecycle tests ─────────────────────────────────────────

class TestLifecycle:
    def test_is_started_false(self):
        service = _make_service()
        assert service.is_started is False

    def test_is_started_true(self):
        engine = _make_engine()
        engine._started = True
        service = ServiceInterface(engine)
        assert service.is_started is True

    @pytest.mark.trio
    async def test_start(self):
        engine = _make_engine()
        engine.start = AsyncMock()
        service = ServiceInterface(engine)
        await service.start()
        engine.start.assert_called_once()

    @pytest.mark.trio
    async def test_stop(self):
        engine = _make_engine()
        engine.stop = AsyncMock()
        service = ServiceInterface(engine)
        await service.stop()
        engine.stop.assert_called_once()


# ─── Discovery tests ─────────────────────────────────────────

class TestDiscovery:
    def test_discover_peers_empty(self):
        service = _make_service()
        peers = service.discover_peers()
        assert peers == []

    def test_discover_peers(self):
        peers_data = [
            Peer(peer_id="QmPeer1", addrs=["/ip4/1.2.3.4/tcp/4001"], name="Device1", is_online=True),
            Peer(peer_id="QmPeer2", addrs=["/ip4/5.6.7.8/tcp/4002"], name="Device2", is_online=False),
        ]
        service = _make_service()
        service._engine.discover_peers.return_value = peers_data
        result = service.discover_peers()
        assert len(result) == 2
        assert result[0].peer_id == "QmPeer1"
        assert result[0].name == "Device1"
        assert result[0].is_online is True
        assert result[1].peer_id == "QmPeer2"
        assert result[1].is_online is False

    def test_get_peer_not_found(self):
        service = _make_service()
        peer = service.get_peer("QmNonexistent")
        assert peer is None

    def test_get_peer_found(self):
        peer_data = Peer(peer_id="QmFound", addrs=["/ip4/1.1.1.1/tcp/4001"], name="Found", is_online=True)
        service = _make_service()
        service._engine.get_peer.return_value = peer_data
        result = service.get_peer("QmFound")
        assert result is not None
        assert result.peer_id == "QmFound"
        assert result.name == "Found"

    @pytest.mark.trio
    async def test_connect_peer(self):
        peer_data = Peer(peer_id="QmNew", addrs=["/ip4/1.2.3.4/tcp/4001"], name="New")

        async def fake_connect(addr):
            return peer_data

        service = _make_service()
        service._engine.connect_peer = fake_connect
        result = await service.connect_peer("/ip4/1.2.3.4/tcp/4001/p2p/QmNew")
        assert result.peer_id == "QmNew"
        assert result.is_online is True

    @pytest.mark.trio
    async def test_ping_peer(self):
        ping_result = {
            "peer_id": "QmTarget",
            "count": 3,
            "latencies_ms": [10.5, 12.3, 11.1],
            "avg_ms": 11.3,
            "loss_pct": 0.0,
        }
        service = _make_service()
        service._engine.ping = AsyncMock(return_value=ping_result)
        result = await service.ping_peer("QmTarget", count=3)
        assert result.peer_id == "QmTarget"
        assert result.count == 3
        assert len(result.latencies_ms) == 3
        assert result.avg_ms == 11.3
        assert result.loss_pct == 0.0

    @pytest.mark.trio
    async def test_ping_peer_with_loss(self):
        ping_result = {
            "peer_id": "QmTarget",
            "count": 3,
            "latencies_ms": [10.0, -1, 12.0],
            "avg_ms": 11.0,
            "loss_pct": 33.3,
        }
        service = _make_service()
        service._engine.ping = AsyncMock(return_value=ping_result)
        result = await service.ping_peer("QmTarget", count=3)
        assert result.loss_pct == 33.3


# ─── Transfer tests ──────────────────────────────────────────

class TestTransfers:
    def test_list_transfers_empty(self):
        service = _make_service()
        transfers = service.list_transfers()
        assert transfers == []

    def test_list_transfers(self):
        transfers_data = [
            Transfer(
                transfer_id="abc123",
                file_name="test.txt",
                file_size=1024,
                status=TransferStatus.COMPLETED,
                progress=1.0,
                sender_peer_id="QmSender",
                receiver_peer_id="QmReceiver",
            ),
            Transfer(
                transfer_id="def456",
                file_name="data.bin",
                file_size=2048,
                status=TransferStatus.TRANSFERRING,
                progress=0.5,
            ),
        ]
        service = _make_service()
        service._engine.list_transfers.return_value = transfers_data
        result = service.list_transfers()
        assert len(result) == 2
        assert result[0].transfer_id == "abc123"
        assert result[0].status == "completed"
        assert result[0].progress == 1.0
        assert result[1].transfer_id == "def456"
        assert result[1].status == "transferring"
        assert result[1].progress == 0.5

    def test_get_transfer_not_found(self):
        service = _make_service()
        transfer = service.get_transfer("nonexistent")
        assert transfer is None

    def test_get_transfer_found(self):
        transfer_data = Transfer(
            transfer_id="abc123",
            file_name="test.txt",
            file_size=1024,
            status=TransferStatus.PENDING,
            progress=0.0,
            root_cid="QmRoot",
        )
        service = _make_service()
        service._engine.get_transfer.return_value = transfer_data
        result = service.get_transfer("abc123")
        assert result is not None
        assert result.transfer_id == "abc123"
        assert result.file_name == "test.txt"
        assert result.root_cid == "QmRoot"

    @pytest.mark.trio
    async def test_send_file(self):
        transfer_data = Transfer(
            transfer_id="xyz789",
            file_name="sent.txt",
            file_size=512,
            status=TransferStatus.PENDING,
            root_cid="QmSent",
        )

        async def fake_send(path, target):
            return transfer_data

        service = _make_service()
        service._engine.send_file = fake_send
        result = await service.send_file("/path/to/sent.txt", "QmTarget")
        assert result.transfer_id == "xyz789"
        assert result.file_name == "sent.txt"

    def test_cancel_transfer_not_found(self):
        service = _make_service()
        assert service.cancel_transfer("nonexistent") is False

    def test_cancel_transfer_already_completed(self):
        transfer = Transfer(
            transfer_id="abc",
            status=TransferStatus.COMPLETED,
        )
        service = _make_service()
        service._engine.get_transfer.return_value = transfer
        assert service.cancel_transfer("abc") is False

    def test_cancel_transfer_already_cancelled(self):
        transfer = Transfer(
            transfer_id="abc",
            status=TransferStatus.CANCELLED,
        )
        service = _make_service()
        service._engine.get_transfer.return_value = transfer
        assert service.cancel_transfer("abc") is False

    def test_cancel_transfer_pending(self):
        transfer = Transfer(
            transfer_id="abc",
            status=TransferStatus.PENDING,
        )
        service = _make_service()
        service._engine.get_transfer.return_value = transfer
        assert service.cancel_transfer("abc") is True
        assert transfer.status == TransferStatus.CANCELLED

    def test_cancel_transfer_transferring(self):
        transfer = Transfer(
            transfer_id="abc",
            status=TransferStatus.TRANSFERRING,
        )
        service = _make_service()
        service._engine.get_transfer.return_value = transfer
        assert service.cancel_transfer("abc") is True
        assert transfer.status == TransferStatus.CANCELLED


# ─── Download directory tests ────────────────────────────────

class TestDownloadDir:
    def test_get_download_dir(self):
        service = _make_service()
        assert service.get_download_dir() == "/tmp/downloads"

    def test_set_download_dir(self):
        engine = _make_engine()
        engine.set_download_dir.return_value = "/new/path"
        service = ServiceInterface(engine)
        result = service.set_download_dir("/new/path")
        assert result == "/new/path"
        engine.set_download_dir.assert_called_once_with("/new/path")


# ─── Dataclass tests ─────────────────────────────────────────

class TestDataclasses:
    def test_identity_info(self):
        info = IdentityInfo(peer_id="Qm123", addrs=["/ip4/1.1.1.1/tcp/4001"])
        assert info.peer_id == "Qm123"
        assert len(info.addrs) == 1

    def test_peer_info(self):
        info = PeerInfo(peer_id="Qm456", addrs=[], name="Test", is_online=True)
        assert info.is_online is True

    def test_transfer_info(self):
        info = TransferInfo(
            transfer_id="abc",
            file_name="test.txt",
            file_size=1024,
            status="pending",
            progress=0.0,
        )
        assert info.transfer_id == "abc"

    def test_ping_result(self):
        result = PingResult(
            peer_id="Qm789",
            count=5,
            latencies_ms=[10.0, 12.0],
            avg_ms=11.0,
            loss_pct=0.0,
        )
        assert result.count == 5


# ─── ServiceInterface method dispatch tests ──────────────────

class TestServiceDispatch:
    """Verify all service methods correctly delegate to engine."""

    def test_get_identity_calls_engine(self):
        engine = _make_engine()
        service = ServiceInterface(engine)
        service.get_identity()
        engine.get_peer_id.assert_called_once()
        engine.get_addrs.assert_called_once()

    def test_discover_peers_calls_engine(self):
        engine = _make_engine()
        service = ServiceInterface(engine)
        service.discover_peers()
        engine.discover_peers.assert_called_once()

    def test_get_peer_calls_engine(self):
        engine = _make_engine()
        service = ServiceInterface(engine)
        service.get_peer("Qm123")
        engine.get_peer.assert_called_once_with("Qm123")

    def test_list_transfers_calls_engine(self):
        engine = _make_engine()
        service = ServiceInterface(engine)
        service.list_transfers()
        engine.list_transfers.assert_called_once()

    def test_get_transfer_calls_engine(self):
        engine = _make_engine()
        service = ServiceInterface(engine)
        service.get_transfer("abc")
        engine.get_transfer.assert_called_once_with("abc")

    def test_get_download_dir_calls_engine(self):
        engine = _make_engine()
        service = ServiceInterface(engine)
        service.get_download_dir()
        engine.get_download_dir.assert_called_once()

    def test_set_download_dir_calls_engine(self):
        engine = _make_engine()
        service = ServiceInterface(engine)
        service.set_download_dir("/new")
        engine.set_download_dir.assert_called_once_with("/new")

    @pytest.mark.trio
    async def test_connect_peer_calls_engine(self):
        engine = _make_engine()

        async def fake_connect(addr):
            return Peer(peer_id="QmNew", addrs=[addr])

        engine.connect_peer = fake_connect
        service = ServiceInterface(engine)
        await service.connect_peer("/ip4/1.1.1.1/tcp/4001/p2p/QmNew")
        # Verify it was called (can't use assert_called_once with custom func)

    @pytest.mark.trio
    async def test_ping_calls_engine(self):
        engine = _make_engine()
        engine.ping = AsyncMock(return_value={
            "peer_id": "Qm123", "count": 1, "latencies_ms": [10.0],
            "avg_ms": 10.0, "loss_pct": 0.0,
        })
        service = ServiceInterface(engine)
        await service.ping_peer("Qm123", count=1)
        engine.ping.assert_called_once_with("Qm123", count=1)

    @pytest.mark.trio
    async def test_send_file_calls_engine(self):
        engine = _make_engine()

        async def fake_send(path, target):
            return Transfer(file_name="test.txt")

        engine.send_file = fake_send
        service = ServiceInterface(engine)
        await service.send_file("/path/test.txt", "QmTarget")
        # Verify it was called
