"""Tests for poller module."""

import asyncio
import contextlib
from unittest.mock import MagicMock, patch

import pytest

from webterm.poller import Poller, Write


class TestWrite:
    """Tests for Write dataclass."""

    def test_create_write(self):
        """Test creating a Write object."""
        write = Write(data=b"test")
        assert write.data == b"test"
        assert write.position == 0
        assert write.done_event is not None

    def test_write_with_position(self):
        """Test Write with custom position."""
        write = Write(data=b"test", position=5)
        assert write.position == 5


class TestPoller:
    """Tests for Poller class."""

    def test_init(self):
        """Test Poller initialization."""
        poller = Poller()
        assert poller._loop is None
        assert poller._read_queues == {}
        assert poller._write_queues == {}
        assert not poller._exit_event.is_set()

    def test_set_loop(self):
        """Test setting the asyncio loop."""
        poller = Poller()
        mock_loop = MagicMock()
        poller.set_loop(mock_loop)
        assert poller._loop == mock_loop

    def test_add_file(self):
        """Test adding a file descriptor."""
        poller = Poller()
        # Use a mock file descriptor
        with patch.object(poller._selector, "register"):
            queue = poller.add_file(42)
            assert 42 in poller._read_queues
            assert isinstance(queue, asyncio.Queue)

    def test_remove_file(self):
        """Test removing a file descriptor."""
        poller = Poller()
        # Add first
        with patch.object(poller._selector, "register"):
            poller.add_file(42)

        # Remove
        with patch.object(poller._selector, "unregister"):
            poller.remove_file(42)
            assert 42 not in poller._read_queues

    def test_remove_nonexistent_file(self):
        """Test removing a non-existent file descriptor."""
        poller = Poller()
        with patch.object(poller._selector, "unregister"):
            # Should not raise
            poller.remove_file(999)

    @pytest.mark.asyncio
    async def test_write_handles_removed_fd(self):
        poller = Poller()
        poller._loop = asyncio.get_event_loop()

        with patch.object(poller._selector, "register"):
            poller.add_file(42)

        with patch.object(poller._selector, "modify", side_effect=KeyError()):
            await poller.write(42, b"test")

    @pytest.mark.asyncio
    async def test_write_creates_queue(self):
        """Test that write creates a write queue if needed."""
        poller = Poller()
        poller._loop = asyncio.get_event_loop()

        # Mock selector
        with patch.object(poller._selector, "register"):
            poller.add_file(42)

        with patch.object(poller._selector, "modify"):
            # Start write in background (won't complete without poller running)
            task = asyncio.create_task(poller.write(42, b"test"))

            # Give it time to set up
            await asyncio.sleep(0.01)

            assert 42 in poller._write_queues
            assert len(poller._write_queues[42]) == 1

            # Cancel to clean up
            task.cancel()
            with contextlib.suppress(asyncio.CancelledError):
                await task

    def test_exit_sets_event(self):
        """Test that exit sets the exit event."""
        poller = Poller()
        poller._exit_event.clear()

        # Mock join to avoid blocking
        with patch.object(poller, "join"):
            poller.exit()

        assert poller._exit_event.is_set()
        assert poller._read_queues == {}
        assert poller._write_queues == {}

    def test_exit_puts_none_in_queues(self):
        """Test that exit puts None in all read queues."""
        poller = Poller()

        # Add some queues
        with patch.object(poller._selector, "register"):
            q1 = poller.add_file(1)
            q2 = poller.add_file(2)

        # Mock join
        with patch.object(poller, "join"):
            poller.exit()

        # Queues should have None
        assert q1.get_nowait() is None
        assert q2.get_nowait() is None

    @pytest.mark.asyncio
    async def test_write_with_timeout_returns_true_on_success(self):
        """write_with_timeout returns True when write completes."""
        poller = Poller()
        poller._loop = asyncio.get_event_loop()

        with patch.object(poller._selector, "register"):
            poller.add_file(42)

        async def instant_write(fd, data):
            # Simulate immediate completion
            pass

        with patch.object(poller, "write", side_effect=instant_write):
            result = await poller.write_with_timeout(42, b"test", timeout=1.0)
        assert result is True

    @pytest.mark.asyncio
    async def test_write_with_timeout_returns_false_on_timeout(self):
        """write_with_timeout returns False when write times out."""
        poller = Poller()
        poller._loop = asyncio.get_event_loop()

        with patch.object(poller._selector, "register"):
            poller.add_file(42)

        async def slow_write(fd, data):
            await asyncio.sleep(999)

        with patch.object(poller, "write", side_effect=slow_write):
            result = await poller.write_with_timeout(42, b"test", timeout=0.01)
        assert result is False
