"""
Tests for the ring buffer implementation.
"""

import pytest
import threading
import time
from esppal.buffer import BufferEntry, RingBuffer, BufferReader


class TestBufferEntry:
    """Tests for BufferEntry dataclass."""

    def test_entry_creation(self):
        entry = BufferEntry(timestamp=1234567890.123, source="serial", data=b"hello")
        assert entry.timestamp == 1234567890.123
        assert entry.source == "serial"
        assert entry.data == b"hello"
        assert entry.marker is None

    def test_marker_entry(self):
        entry = BufferEntry(
            timestamp=1234567890.123,
            source="marker",
            data=b"",
            marker="upload_complete"
        )
        assert entry.source == "marker"
        assert entry.marker == "upload_complete"

    def test_entry_size(self):
        entry = BufferEntry(timestamp=0, source="serial", data=b"x" * 100)
        assert entry.size >= 100  # At least the data size


class TestRingBuffer:
    """Tests for RingBuffer class."""

    def test_append_and_read(self):
        """Basic append and iteration."""
        buf = RingBuffer(max_size_bytes=1024)

        seq1 = buf.append("serial", b"hello")
        seq2 = buf.append("serial", b"world")

        entries = list(buf.iterate_from_seq(0))
        assert len(entries) == 2
        assert entries[0][0] == seq1
        assert entries[0][1].data == b"hello"
        assert entries[1][0] == seq2
        assert entries[1][1].data == b"world"

    def test_size_limit_eviction(self):
        """Entries evicted when buffer full."""
        buf = RingBuffer(max_size_bytes=200)

        # Add entries until we exceed the limit
        seqs = []
        for i in range(10):
            seqs.append(buf.append("serial", b"x" * 50))

        # Should have evicted some entries
        assert buf.entry_count < 10
        assert buf.size <= buf.max_size

        # First entry should be evicted
        entries = list(buf.iterate_from_seq(seqs[0]))
        first_seq = entries[0][0] if entries else None
        assert first_seq is None or first_seq > seqs[0]

    def test_marker_insertion(self):
        """Markers inserted at correct position."""
        buf = RingBuffer(max_size_bytes=1024)

        buf.append("serial", b"before")
        marker_seq = buf.mark("test_marker")
        buf.append("serial", b"after")

        # Marker should be between the two entries
        entries = list(buf.iterate_from_seq(0))
        assert len(entries) == 3
        assert entries[1][0] == marker_seq
        assert entries[1][1].source == "marker"
        assert entries[1][1].marker == "test_marker"

    def test_iterate_from_marker(self):
        """Iteration starts from marker."""
        buf = RingBuffer(max_size_bytes=1024)

        buf.append("serial", b"before1")
        buf.append("serial", b"before2")
        buf.mark("start_here")
        buf.append("serial", b"after1")
        buf.append("serial", b"after2")

        entries = list(buf.iterate_from_marker("start_here"))
        assert len(entries) == 3  # marker + 2 after
        assert entries[0][1].marker == "start_here"
        assert entries[1][1].data == b"after1"
        assert entries[2][1].data == b"after2"

    def test_marker_survives_eviction(self):
        """Marker remains valid after earlier entries evicted."""
        buf = RingBuffer(max_size_bytes=300)

        # Add some entries
        for i in range(3):
            buf.append("serial", b"x" * 50)

        # Add marker
        marker_seq = buf.mark("important")

        # Add more entries to evict the earlier ones
        for i in range(5):
            buf.append("serial", b"x" * 50)

        # Marker should still be retrievable if not evicted
        result_seq = buf.get_marker_seq("important")
        if result_seq is not None:
            assert result_seq == marker_seq
            entries = list(buf.iterate_from_marker("important"))
            assert len(entries) > 0

    def test_invalid_marker(self):
        """Graceful handling of unknown marker."""
        buf = RingBuffer(max_size_bytes=1024)

        assert buf.get_marker_seq("nonexistent") is None

        with pytest.raises(KeyError):
            list(buf.iterate_from_marker("nonexistent"))

    def test_thread_safety_writes(self):
        """Concurrent appends don't corrupt buffer."""
        buf = RingBuffer(max_size_bytes=10 * 1024 * 1024)  # 10MB
        errors = []
        entries_per_thread = 1000

        def writer(thread_id):
            try:
                for i in range(entries_per_thread):
                    buf.append("serial", f"thread{thread_id}:{i}".encode())
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=writer, args=(i,)) for i in range(5)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors
        assert buf.entry_count == 5 * entries_per_thread

    def test_thread_safety_reads(self):
        """Concurrent reads during writes."""
        buf = RingBuffer(max_size_bytes=10 * 1024 * 1024)
        errors = []
        stop_event = threading.Event()

        def writer():
            try:
                for i in range(1000):
                    buf.append("serial", f"entry{i}".encode())
                    time.sleep(0.001)
            except Exception as e:
                errors.append(e)
            finally:
                stop_event.set()

        def reader():
            try:
                while not stop_event.is_set():
                    list(buf.iterate_from_seq(0))
                    time.sleep(0.001)
            except Exception as e:
                errors.append(e)

        writer_thread = threading.Thread(target=writer)
        reader_threads = [threading.Thread(target=reader) for _ in range(3)]

        writer_thread.start()
        for t in reader_threads:
            t.start()

        writer_thread.join()
        for t in reader_threads:
            t.join()

        assert not errors

    def test_empty_buffer(self):
        """Edge case handling for empty buffer."""
        buf = RingBuffer(max_size_bytes=1024)

        assert buf.entry_count == 0
        assert buf.size == 0
        assert list(buf.iterate_from_seq(0)) == []
        assert buf.get_current_seq() == 0

    def test_single_entry(self):
        """Edge case with one entry."""
        buf = RingBuffer(max_size_bytes=1024)

        seq = buf.append("serial", b"only")
        assert buf.entry_count == 1

        entries = list(buf.iterate_from_seq(0))
        assert len(entries) == 1
        assert entries[0][0] == seq
        assert entries[0][1].data == b"only"

    def test_iterate_tail(self):
        """Iterate last N entries."""
        buf = RingBuffer(max_size_bytes=1024)

        for i in range(10):
            buf.append("serial", f"entry{i}".encode())

        entries = list(buf.iterate_tail(3))
        assert len(entries) == 3
        assert entries[0][1].data == b"entry7"
        assert entries[1][1].data == b"entry8"
        assert entries[2][1].data == b"entry9"

    def test_wait_for_data(self):
        """Wait for new data notification."""
        buf = RingBuffer(max_size_bytes=1024)
        result = []

        def waiter():
            current = buf.get_current_seq()
            if buf.wait_for_data(current, timeout=5.0):
                result.append("got_data")

        thread = threading.Thread(target=waiter)
        thread.start()

        time.sleep(0.1)  # Let thread start waiting
        buf.append("serial", b"new data")

        thread.join(timeout=2.0)
        assert result == ["got_data"]

    def test_wait_for_data_timeout(self):
        """Wait times out when no data."""
        buf = RingBuffer(max_size_bytes=1024)

        current = buf.get_current_seq()
        start = time.time()
        result = buf.wait_for_data(current, timeout=0.1)
        elapsed = time.time() - start

        assert result is False
        assert elapsed >= 0.1
        assert elapsed < 0.5

    def test_clear(self):
        """Clear removes all entries and markers."""
        buf = RingBuffer(max_size_bytes=1024)

        buf.append("serial", b"data")
        buf.mark("marker1")

        buf.clear()

        assert buf.entry_count == 0
        assert buf.size == 0
        assert buf.get_marker_seq("marker1") is None

    def test_multiple_markers(self):
        """Multiple markers tracked correctly."""
        buf = RingBuffer(max_size_bytes=1024)

        buf.append("serial", b"data1")
        seq1 = buf.mark("marker1")
        buf.append("serial", b"data2")
        seq2 = buf.mark("marker2")
        buf.append("serial", b"data3")

        markers = buf.get_markers()
        assert markers == {"marker1": seq1, "marker2": seq2}

    def test_sequence_range(self):
        """Sequence range tracking."""
        buf = RingBuffer(max_size_bytes=1024)

        start, end = buf.sequence_range
        assert start == 0
        assert end == 0

        buf.append("serial", b"data1")
        buf.append("serial", b"data2")

        start, end = buf.sequence_range
        assert start == 0
        assert end == 2

    def test_different_sources(self):
        """Different source types stored correctly."""
        buf = RingBuffer(max_size_bytes=1024)

        buf.append("serial", b"serial data")
        buf.append("stdout", b"build output")
        buf.append("stderr", b"error output")

        entries = list(buf.iterate_from_seq(0))
        assert entries[0][1].source == "serial"
        assert entries[1][1].source == "stdout"
        assert entries[2][1].source == "stderr"


class TestBufferReader:
    """Tests for BufferReader class."""

    def test_reader_creation(self):
        """Create reader at current position."""
        buf = RingBuffer(max_size_bytes=1024)
        buf.append("serial", b"existing")

        reader = BufferReader(buf)
        assert reader.position == buf.get_current_seq()

    def test_reader_from_marker(self):
        """Create reader from marker."""
        buf = RingBuffer(max_size_bytes=1024)
        buf.append("serial", b"before")
        buf.mark("start")
        buf.append("serial", b"after")

        reader = BufferReader.from_marker(buf, "start")
        entries = reader.read_available()

        assert len(entries) == 2  # marker + after
        assert entries[0][1].marker == "start"

    def test_reader_from_tail(self):
        """Create reader from tail."""
        buf = RingBuffer(max_size_bytes=1024)
        for i in range(10):
            buf.append("serial", f"entry{i}".encode())

        reader = BufferReader.from_tail(buf, 3)
        entries = reader.read_available()

        assert len(entries) == 3
        assert entries[0][1].data == b"entry7"

    def test_read_available(self):
        """Read all available entries."""
        buf = RingBuffer(max_size_bytes=1024)
        reader = BufferReader(buf)

        buf.append("serial", b"data1")
        buf.append("serial", b"data2")

        entries = reader.read_available()
        assert len(entries) == 2

        # Position should be updated
        assert reader.position == buf.get_current_seq()

        # Reading again should return empty
        entries = reader.read_available()
        assert len(entries) == 0

    def test_read_one_blocking(self):
        """Read one entry with blocking."""
        buf = RingBuffer(max_size_bytes=1024)
        reader = BufferReader(buf)
        result = []

        def read_thread():
            entry = reader.read_one(timeout=5.0)
            if entry:
                result.append(entry[1].data)

        thread = threading.Thread(target=read_thread)
        thread.start()

        time.sleep(0.1)
        buf.append("serial", b"new entry")

        thread.join(timeout=2.0)
        assert result == [b"new entry"]

    def test_read_one_timeout(self):
        """Read one times out when no data."""
        buf = RingBuffer(max_size_bytes=1024)
        reader = BufferReader(buf)

        start = time.time()
        entry = reader.read_one(timeout=0.1)
        elapsed = time.time() - start

        assert entry is None
        assert elapsed >= 0.1

    def test_invalid_marker(self):
        """Reader from invalid marker raises KeyError."""
        buf = RingBuffer(max_size_bytes=1024)

        with pytest.raises(KeyError):
            BufferReader.from_marker(buf, "nonexistent")
