"""Tests for OutputBuffer."""

from unittest.mock import patch

from skop_runner.output_buffer import OutputBuffer, BUFFERED_METHODS


class TestOutputBufferBasic:
    """Basic add/drain behavior."""

    def test_add_and_drain(self):
        buf = OutputBuffer()
        buf.add("kernel.output", {"cell_id": "c1", "output": {"output_type": "stream"}})
        buf.add("kernel.statusChanged", {"kernel_id": "k1", "execution_state": "idle"})
        result = buf.drain()
        assert len(result) == 2
        assert result[0]["method"] == "kernel.output"
        assert result[1]["method"] == "kernel.statusChanged"

    def test_drain_clears_buffer(self):
        buf = OutputBuffer()
        buf.add("kernel.output", {"x": 1})
        assert len(buf) == 1
        buf.drain()
        assert len(buf) == 0
        assert buf.drain() == []

    def test_drain_returns_dicts(self):
        buf = OutputBuffer()
        buf.add("kernel.died", {"kernel_id": "k1"})
        result = buf.drain()
        assert isinstance(result, list)
        assert isinstance(result[0], dict)
        assert set(result[0].keys()) == {"method", "params"}

    def test_clear(self):
        buf = OutputBuffer()
        buf.add("kernel.output", {"x": 1})
        buf.add("kernel.output", {"x": 2})
        assert len(buf) == 2
        buf.clear()
        assert len(buf) == 0
        assert buf.drain() == []


class TestOutputBufferFiltering:
    """Only allowed methods are buffered."""

    def test_only_allowed_methods(self):
        buf = OutputBuffer()
        buf.add("kernel.output", {"x": 1})
        buf.add("file.read", {"path": "foo.py"})
        buf.add("kernel.backendDied", {"message": "oops"})
        buf.add("kernel.statusChanged", {"state": "idle"})
        assert len(buf) == 2  # Only kernel.output and kernel.statusChanged

    def test_buffered_methods_constant(self):
        assert "kernel.output" in BUFFERED_METHODS
        assert "kernel.statusChanged" in BUFFERED_METHODS
        assert "kernel.died" in BUFFERED_METHODS
        assert len(BUFFERED_METHODS) == 11


class TestOutputBufferFIFO:
    """FIFO eviction when over max_size."""

    def test_fifo_eviction(self):
        buf = OutputBuffer(max_size=3)
        for i in range(5):
            buf.add("kernel.output", {"index": i})
        assert len(buf) == 3
        result = buf.drain()
        # Oldest entries (0, 1) should be evicted
        assert result[0]["params"]["index"] == 2
        assert result[1]["params"]["index"] == 3
        assert result[2]["params"]["index"] == 4


class TestOutputBufferTTL:
    """TTL-based pruning."""

    def test_ttl_pruning_on_add(self):
        buf = OutputBuffer(ttl_seconds=10.0)
        # Add an entry "in the past"
        with patch("skop_runner.output_buffer.time.monotonic", return_value=100.0):
            buf.add("kernel.output", {"old": True})

        # Now time has advanced past TTL — prune happens inside add()
        with patch("skop_runner.output_buffer.time.monotonic", return_value=111.0):
            buf.add("kernel.output", {"new": True})
            # Assert inside the patch so drain()'s _prune_stale uses 111.0 too
            assert len(buf) == 1
            result = buf.drain()
            assert result[0]["params"] == {"new": True}

    def test_ttl_pruning_on_drain(self):
        buf = OutputBuffer(ttl_seconds=5.0)
        with patch("skop_runner.output_buffer.time.monotonic", return_value=100.0):
            buf.add("kernel.output", {"stale": True})

        with patch("skop_runner.output_buffer.time.monotonic", return_value=106.0):
            result = buf.drain()

        assert result == []


class TestOutputBufferConnectedState:
    """Connected state and should_buffer()."""

    def test_starts_connected(self):
        buf = OutputBuffer()
        assert buf.should_buffer() is False

    def test_disconnected_should_buffer(self):
        buf = OutputBuffer()
        buf.set_connected(False)
        assert buf.should_buffer() is True

    def test_reconnected_should_not_buffer(self):
        buf = OutputBuffer()
        buf.set_connected(False)
        buf.set_connected(True)
        assert buf.should_buffer() is False

    def test_len(self):
        buf = OutputBuffer()
        assert len(buf) == 0
        buf.add("kernel.output", {"x": 1})
        assert len(buf) == 1
        buf.add("kernel.output", {"x": 2})
        assert len(buf) == 2
