"""Tests for RecordBuffer background flush."""

import threading
import time

import structlog.testing

from caliper._annotation import Annotation
from caliper._buffer import RecordBuffer
from caliper._record import Record


def _make_record(model: str = "test-model") -> Record:
    return Record(
        request_id="test-uuid",
        timestamp="2026-02-20T10:00:00+00:00",
        provider="anthropic",
        model=model,
        endpoint="chat",
        duration_ms=100,
        tokens_input=10,
        tokens_output=5,
        tokens_total=15,
        status="success",
        cached=False,
        sdk_version="0.1.0",
    )


class FakeHandler:
    """Captures exported batches for test assertions."""

    def __init__(self):
        self.batches: list[list[Record]] = []
        self.annotation_batches: list[list[Annotation]] = []
        self.closed = False
        self._lock = threading.Lock()

    def export(self, records: list[Record]) -> None:
        with self._lock:
            self.batches.append(list(records))

    def export_annotations(self, annotations: list[Annotation]) -> None:
        with self._lock:
            self.annotation_batches.append(list(annotations))

    def close(self) -> None:
        self.closed = True

    @property
    def all_records(self) -> list[Record]:
        with self._lock:
            return [r for batch in self.batches for r in batch]

    @property
    def all_annotations(self) -> list[Annotation]:
        with self._lock:
            return [a for batch in self.annotation_batches for a in batch]


class FailingHandler:
    """Handler that raises on export."""

    def __init__(self):
        self.export_count = 0
        self.export_annotations_count = 0
        self.closed = False

    def export(self, records: list[Record]) -> None:
        self.export_count += 1
        raise RuntimeError("export failed")

    def export_annotations(self, annotations: list[Annotation]) -> None:
        self.export_annotations_count += 1
        raise RuntimeError("export annotations failed")

    def close(self) -> None:
        self.closed = True


def test_push_and_flush():
    handler = FakeHandler()
    buf = RecordBuffer(handler, flush_interval=0.1, batch_size=100)

    for _ in range(5):
        buf.push(_make_record())

    time.sleep(0.3)
    buf.shutdown()

    assert len(handler.all_records) == 5


def test_shutdown_flushes_remaining():
    handler = FakeHandler()
    # Long interval so background thread won't flush on its own
    buf = RecordBuffer(handler, flush_interval=60.0, batch_size=100)

    for _ in range(3):
        buf.push(_make_record())

    buf.shutdown(timeout=2.0)

    assert len(handler.all_records) == 3


def test_batch_size_respected():
    handler = FakeHandler()
    buf = RecordBuffer(handler, flush_interval=0.1, batch_size=2)

    for _ in range(5):
        buf.push(_make_record())

    time.sleep(0.3)
    buf.shutdown()

    # 5 records with batch_size=2 should produce 3 batches (2+2+1)
    assert len(handler.all_records) == 5
    assert all(len(batch) <= 2 for batch in handler.batches)


def test_max_queue_size_drops_oldest():
    handler = FakeHandler()
    # Long interval so nothing flushes during push
    buf = RecordBuffer(handler, flush_interval=60.0, batch_size=100, max_queue_size=3)

    for i in range(5):
        buf.push(_make_record(model=f"model-{i}"))

    buf.shutdown()

    # Only the last 3 should survive (deque maxlen drops oldest)
    models = [r.model for r in handler.all_records]
    assert models == ["model-2", "model-3", "model-4"]


def test_handler_failure_does_not_stop_thread():
    handler = FailingHandler()
    buf = RecordBuffer(handler, flush_interval=0.1, batch_size=100)

    buf.push(_make_record())
    time.sleep(0.3)

    # Push another record — thread should still be alive
    buf.push(_make_record())
    time.sleep(0.2)

    assert handler.export_count >= 2
    buf.shutdown()
    assert handler.closed


def test_handler_closed_on_shutdown():
    handler = FakeHandler()
    buf = RecordBuffer(handler, flush_interval=0.1, batch_size=100)
    buf.shutdown()
    assert handler.closed


def test_push_annotation():
    handler = FakeHandler()
    buf = RecordBuffer(handler, flush_interval=0.1, batch_size=100)

    annotation = Annotation(request_id="req-1", metadata={"sentiment": "positive"})
    buf.push(annotation)

    time.sleep(0.3)
    buf.shutdown()

    assert len(handler.all_annotations) == 1
    assert handler.all_annotations[0].request_id == "req-1"
    assert handler.all_annotations[0].metadata == {"sentiment": "positive"}


def test_mixed_record_and_annotation_flush():
    handler = FakeHandler()
    buf = RecordBuffer(handler, flush_interval=0.1, batch_size=100)

    buf.push(_make_record(model="model-a"))
    buf.push(Annotation(request_id="req-1", metadata={"label": "good"}))
    buf.push(_make_record(model="model-b"))
    buf.push(Annotation(request_id="req-2", metadata={"label": "bad"}))

    time.sleep(0.3)
    buf.shutdown()

    assert len(handler.all_records) == 2
    assert [r.model for r in handler.all_records] == ["model-a", "model-b"]
    assert len(handler.all_annotations) == 2
    assert [a.request_id for a in handler.all_annotations] == ["req-1", "req-2"]


def test_inter_batch_sleep():
    """Flushing multiple batches should include 0.1s sleep between them."""
    handler = FakeHandler()
    # Long interval so only the shutdown flush runs
    buf = RecordBuffer(handler, flush_interval=60.0, batch_size=2)

    for _ in range(6):
        buf.push(_make_record())

    start = time.monotonic()
    buf.shutdown(timeout=5.0)
    elapsed = time.monotonic() - start

    assert len(handler.all_records) == 6
    # 6 records, batch_size=2 → 3 batches → 2 inter-batch sleeps → ≥0.15s
    assert elapsed >= 0.15


# --- Capacity logging tests ---


def test_error_logged_when_dropping_oldest():
    handler = FakeHandler()
    buf = RecordBuffer(handler, flush_interval=60.0, batch_size=100, max_queue_size=3)

    # Fill to capacity
    for i in range(3):
        buf.push(_make_record(model=f"model-{i}"))

    # Next push will evict the oldest — should trigger an error log
    with structlog.testing.capture_logs() as logs:
        buf.push(_make_record(model="overflow"))

    buf.shutdown()

    drop_logs = [entry for entry in logs if entry["event"] == "caliper.buffer.dropping_oldest"]
    assert len(drop_logs) == 1
    assert drop_logs[0]["log_level"] == "error"
    assert drop_logs[0]["queue_size"] == 3
    assert drop_logs[0]["max_queue_size"] == 3


def test_drop_log_throttled():
    handler = FakeHandler()
    buf = RecordBuffer(handler, flush_interval=60.0, batch_size=100, max_queue_size=3)

    # Fill to capacity
    for _ in range(3):
        buf.push(_make_record())

    # Push many items into the full buffer rapidly — error should only log once
    with structlog.testing.capture_logs() as logs:
        for _ in range(10):
            buf.push(_make_record())

    buf.shutdown()

    drop_logs = [entry for entry in logs if entry["event"] == "caliper.buffer.dropping_oldest"]
    assert len(drop_logs) == 1


def test_warn_logged_above_80_pct():
    handler = FakeHandler()
    # Long interval so background thread won't flush; we call _log_utilization directly
    buf = RecordBuffer(handler, flush_interval=60.0, batch_size=100, max_queue_size=10)

    # Fill to 90% (9/10)
    for _ in range(9):
        buf.push(_make_record())

    with structlog.testing.capture_logs() as logs:
        buf._log_utilization()

    buf.shutdown()

    warn_logs = [entry for entry in logs if entry["event"] == "caliper.buffer.high_utilization"]
    assert len(warn_logs) == 1
    assert warn_logs[0]["log_level"] == "warning"
    assert warn_logs[0]["utilization_pct"] == 90


def test_info_logged_above_50_pct():
    handler = FakeHandler()
    buf = RecordBuffer(handler, flush_interval=60.0, batch_size=100, max_queue_size=10)

    # Fill to 60% (6/10)
    for _ in range(6):
        buf.push(_make_record())

    with structlog.testing.capture_logs() as logs:
        buf._log_utilization()

    buf.shutdown()

    info_logs = [entry for entry in logs if entry["event"] == "caliper.buffer.elevated_utilization"]
    assert len(info_logs) == 1
    assert info_logs[0]["log_level"] == "info"
    assert info_logs[0]["utilization_pct"] == 60
