import asyncio

import pytest

from relay.server.models import Peer
from relay.server.state import RelayState


class TestRegister:
    async def test_register_creates_peer_and_group(self, state: RelayState):
        peer = await state.register("s1", "g1", "host", "/work", "main")
        assert isinstance(peer, Peer)
        assert "g1" in state.groups
        assert peer.peer_id in state.groups["g1"].peers

    async def test_register_stores_session_mapping(self, state: RelayState):
        peer = await state.register("s1", "g1", "host", "/work", None)
        peer_id, group_ids = state.sessions["s1"]
        assert peer_id == peer.peer_id
        assert group_ids == ["g1"]

    async def test_register_multiple_peers_same_group(self, state: RelayState):
        p1 = await state.register("s1", "g1", "host-a", "/a", None)
        p2 = await state.register("s2", "g1", "host-b", "/b", None)
        assert len(state.groups["g1"].peers) == 2
        assert p1.peer_id != p2.peer_id

    async def test_register_auto_creates_group(self, state: RelayState):
        await state.register("s1", "new-group", "host", "/work", None)
        assert "new-group" in state.groups

    async def test_register_peer_fields(self, state: RelayState):
        peer = await state.register("s1", "g1", "myhost", "/home/user/project", "feature")
        assert peer.hostname == "myhost"
        assert peer.working_directory == "/home/user/project"
        assert peer.git_branch == "feature"
        assert peer.group_id == "g1"

    async def test_register_same_session_multiple_groups(self, state: RelayState):
        await state.register("s1", "g1", "host", "/work", None)
        await state.register("s1", "g2", "host", "/work", None)
        _, group_ids = state.sessions["s1"]
        assert "g1" in group_ids
        assert "g2" in group_ids


class TestResolveSession:
    async def test_resolve_existing_session(self, state: RelayState):
        peer = await state.register("s1", "g1", "host", "/work", None)
        peer_id, group_ids = await state.resolve_session("s1")
        assert peer_id == peer.peer_id
        assert group_ids == ["g1"]

    async def test_resolve_unknown_session_raises(self, state: RelayState):
        with pytest.raises(ValueError, match="Unknown session"):
            await state.resolve_session("nonexistent")


class TestEnsurePeerSession:
    async def test_creates_session_if_missing(self, state: RelayState):
        result = await state.ensure_peer_session("s1", "p1", ["g1", "g2"], "myhost", "/work")
        assert result == "p1"
        assert "s1" in state.sessions

    async def test_returns_existing_peer_id(self, state: RelayState):
        peer = await state.register("s1", "g1", "host", "/work", None)
        result = await state.ensure_peer_session("s1", "other-id", ["g1"], "host", "/work")
        assert result == peer.peer_id


class TestSendMessage:
    async def test_send_message_appends_to_group(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        msg = await state.send_message("g1", peer_a.peer_id, "hello everyone")
        assert len(state.groups["g1"].messages) == 1
        assert msg.content == "hello everyone"
        assert msg.from_peer == peer_a.peer_id

    async def test_send_message_sets_display_name(self, registered_pair):
        state, peer_a, _ = registered_pair
        msg = await state.send_message("g1", peer_a.peer_id, "hi")
        assert msg.from_display == peer_a.display_name

    async def test_send_message_notifies_other_peers_via_sse(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        queue_b = await state.connect_sse(peer_b.peer_id)
        await state.send_message("g1", peer_a.peer_id, "hello")
        assert not queue_b.empty()
        notification = queue_b.get_nowait()
        assert notification["type"] == "message"
        assert notification["preview"] == "hello"

    async def test_send_message_does_not_notify_sender(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        queue_a = await state.connect_sse(peer_a.peer_id)
        await state.send_message("g1", peer_a.peer_id, "hello")
        assert queue_a.empty()

    async def test_send_message_truncates_preview_at_100(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        queue_b = await state.connect_sse(peer_b.peer_id)
        long_content = "x" * 200
        await state.send_message("g1", peer_a.peer_id, long_content)
        notification = queue_b.get_nowait()
        assert len(notification["preview"]) == 100

    async def test_send_message_unknown_group_raises(self, state: RelayState):
        with pytest.raises(KeyError):
            await state.send_message("nonexistent", "p1", "hi")


class TestReadNewMessages:
    async def test_read_new_group_messages(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        await state.send_message("g1", peer_a.peer_id, "msg1")
        await state.send_message("g1", peer_a.peer_id, "msg2")
        msgs = await state.read_new_messages("g1", peer_b.peer_id)
        assert len(msgs) == 2
        assert msgs[0].content == "msg1"
        assert msgs[1].content == "msg2"

    async def test_read_advances_cursor(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        await state.send_message("g1", peer_a.peer_id, "msg1")
        await state.read_new_messages("g1", peer_b.peer_id)
        await state.send_message("g1", peer_a.peer_id, "msg2")
        msgs = await state.read_new_messages("g1", peer_b.peer_id)
        assert len(msgs) == 1
        assert msgs[0].content == "msg2"

    async def test_read_returns_empty_when_no_new(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        await state.send_message("g1", peer_a.peer_id, "msg1")
        await state.read_new_messages("g1", peer_b.peer_id)
        msgs = await state.read_new_messages("g1", peer_b.peer_id)
        assert msgs == []


class TestListPeers:
    async def test_list_peers_returns_all_in_group(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        peers = await state.list_peers("g1")
        ids = {p.peer_id for p in peers}
        assert peer_a.peer_id in ids
        assert peer_b.peer_id in ids
        assert len(peers) == 2

    async def test_list_peers_unknown_group_raises(self, state: RelayState):
        with pytest.raises(KeyError):
            await state.list_peers("nonexistent")


class TestGetHistory:
    async def test_get_group_history(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        await state.send_message("g1", peer_a.peer_id, "msg1")
        await state.send_message("g1", peer_b.peer_id, "msg2")
        history = await state.get_history("g1")
        assert len(history) == 2

    async def test_get_history_empty(self, registered_pair):
        state, _, _ = registered_pair
        history = await state.get_history("g1")
        assert history == []


class TestSseConnectDisconnect:
    async def test_connect_sse_returns_queue(self, registered_pair):
        state, peer_a, _ = registered_pair
        queue = await state.connect_sse(peer_a.peer_id)
        assert isinstance(queue, asyncio.Queue)

    async def test_connect_sse_sets_peer_online(self, registered_pair):
        state, peer_a, _ = registered_pair
        await state.connect_sse(peer_a.peer_id)
        assert state.groups["g1"].peers[peer_a.peer_id].online is True

    async def test_disconnect_sse_removes_queue(self, registered_pair):
        state, peer_a, _ = registered_pair
        await state.connect_sse(peer_a.peer_id)
        await state.disconnect_sse(peer_a.peer_id)
        assert peer_a.peer_id not in state.sse_queues

    async def test_disconnect_sse_sets_peer_offline(self, registered_pair):
        state, peer_a, _ = registered_pair
        await state.connect_sse(peer_a.peer_id)
        await state.disconnect_sse(peer_a.peer_id)
        assert state.groups["g1"].peers[peer_a.peer_id].online is False

    async def test_disconnect_nonexistent_is_noop(self, state: RelayState):
        await state.disconnect_sse("nobody")


class TestLeaveGroup:
    async def test_leave_removes_peer_from_group(self, registered_pair):
        state, peer_a, _ = registered_pair
        await state.leave_group("sess-a", "g1")
        assert "sess-a" not in state.sessions
        assert peer_a.peer_id not in state.groups["g1"].peers

    async def test_leave_removes_sse_queue(self, registered_pair):
        state, peer_a, _ = registered_pair
        await state.connect_sse(peer_a.peer_id)
        await state.leave_group("sess-a", "g1")
        assert peer_a.peer_id not in state.sse_queues

    async def test_leave_unknown_session_is_noop(self, state: RelayState):
        await state.leave_group("unknown-session", "g1")

    async def test_leave_does_not_affect_other_peers(self, registered_pair):
        state, peer_a, peer_b = registered_pair
        await state.leave_group("sess-a", "g1")
        assert peer_b.peer_id in state.groups["g1"].peers
        assert "sess-b" in state.sessions

    async def test_leave_one_group_keeps_other(self, state: RelayState):
        await state.register("s1", "g1", "host", "/work", None)
        await state.register("s1", "g2", "host", "/work", None)
        await state.leave_group("s1", "g1")
        assert "s1" in state.sessions
        _, group_ids = state.sessions["s1"]
        assert group_ids == ["g2"]
