"""Tests for the SQLite-backed disk buffer."""

import os
import tempfile
import threading
import uuid

import pytest

from genosis.buffer import DiskBuffer


@pytest.fixture
def buf(tmp_path):
    """Create a buffer with a temp DB path."""
    db_path = str(tmp_path / "test_buffer.db")
    b = DiskBuffer(path=db_path, max_size=10000)
    yield b
    b.close()


class TestDiskBuffer:
    def test_put_and_peek(self, buf):
        buf.put("telemetry", {"event_id": "e1", "data": "hello"})
        items = buf.peek(10)
        assert len(items) == 1
        assert items[0]["id"] == "e1"
        assert items[0]["type"] == "telemetry"
        assert items[0]["payload"]["data"] == "hello"

    def test_put_generates_id_if_missing(self, buf):
        event_id = buf.put("telemetry", {"data": "no_id"})
        assert event_id is not None
        items = buf.peek(10)
        assert len(items) == 1
        assert items[0]["id"] == event_id

    def test_remove_confirmed_events(self, buf):
        buf.put("telemetry", {"event_id": "e1"})
        buf.put("telemetry", {"event_id": "e2"})
        buf.put("telemetry", {"event_id": "e3"})
        assert buf.size() == 3

        removed = buf.remove(["e1", "e3"])
        assert removed == 2
        assert buf.size() == 1

        items = buf.peek(10)
        assert items[0]["id"] == "e2"

    def test_remove_empty_list(self, buf):
        assert buf.remove([]) == 0

    def test_remove_nonexistent_ids(self, buf):
        buf.put("telemetry", {"event_id": "e1"})
        removed = buf.remove(["nonexistent"])
        assert removed == 0
        assert buf.size() == 1

    def test_dedup_on_event_id(self, buf):
        buf.put("telemetry", {"event_id": "e1", "data": "first"})
        buf.put("telemetry", {"event_id": "e1", "data": "duplicate"})
        assert buf.size() == 1
        items = buf.peek(10)
        assert items[0]["payload"]["data"] == "first"  # First write wins

    def test_peek_respects_limit(self, buf):
        for i in range(10):
            buf.put("telemetry", {"event_id": f"e{i}"})
        items = buf.peek(3)
        assert len(items) == 3

    def test_peek_returns_oldest_first(self, buf):
        buf.put("telemetry", {"event_id": "old"})
        buf.put("telemetry", {"event_id": "new"})
        items = buf.peek(10)
        assert items[0]["id"] == "old"
        assert items[1]["id"] == "new"

    def test_size(self, buf):
        assert buf.size() == 0
        buf.put("telemetry", {"event_id": "e1"})
        assert buf.size() == 1
        buf.put("telemetry", {"event_id": "e2"})
        assert buf.size() == 2
        buf.remove(["e1"])
        assert buf.size() == 1

    def test_clear(self, buf):
        for i in range(5):
            buf.put("telemetry", {"event_id": f"e{i}"})
        assert buf.size() == 5
        buf.clear()
        assert buf.size() == 0

    def test_max_size_enforcement(self, tmp_path):
        db_path = str(tmp_path / "small_buffer.db")
        buf = DiskBuffer(path=db_path, max_size=5)
        for i in range(10):
            buf.put("telemetry", {"event_id": f"e{i}"})
        assert buf.size() == 5
        items = buf.peek(10)
        # Should have the 5 most recent (oldest dropped)
        ids = [item["id"] for item in items]
        assert "e0" not in ids
        assert "e9" in ids
        buf.close()

    def test_survives_reopen(self, tmp_path):
        db_path = str(tmp_path / "persist_buffer.db")
        buf1 = DiskBuffer(path=db_path)
        buf1.put("telemetry", {"event_id": "survive_me"})
        buf1.close()

        buf2 = DiskBuffer(path=db_path)
        assert buf2.size() == 1
        items = buf2.peek(10)
        assert items[0]["id"] == "survive_me"
        buf2.close()

    def test_multiple_event_types(self, buf):
        buf.put("telemetry", {"event_id": "t1"})
        buf.put("manifest_ack", {"event_id": "m1"})
        buf.put("error_report", {"event_id": "err1"})
        items = buf.peek(10)
        types = [item["type"] for item in items]
        assert "telemetry" in types
        assert "manifest_ack" in types
        assert "error_report" in types

    def test_thread_safety(self, buf):
        """Multiple threads writing concurrently should not corrupt data."""
        errors = []

        def writer(prefix, count):
            try:
                for i in range(count):
                    buf.put("telemetry", {"event_id": f"{prefix}_{i}"})
            except Exception as exc:
                errors.append(exc)

        threads = [threading.Thread(target=writer, args=(f"t{t}", 50)) for t in range(5)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert buf.size() == 250  # 5 threads × 50 events
