"""Tests for BufferedDatabaseUsageEventSink."""

from __future__ import annotations

from collections.abc import Sequence
from unittest.mock import AsyncMock, MagicMock

import pytest

from noteflow.application.observability.ports import UsageEvent, UsageMetrics
from noteflow.infrastructure.observability.usage import BufferedDatabaseUsageEventSink


class TestBufferedDatabaseUsageEventSink:
    """Tests for BufferedDatabaseUsageEventSink."""

    def test_record_adds_to_buffer(self) -> None:
        """Recording an event adds it to the internal buffer."""
        mock_factory = MagicMock()
        sink = BufferedDatabaseUsageEventSink(mock_factory, buffer_size=10)

        event = UsageEvent(
            event_type="summarization.completed",
            meeting_id="meeting-123",
            provider_name="openai",
        )
        sink.record(event)

        assert sink.pending_count == 1, "buffer should contain exactly one event"

    def test_record_simple_creates_event(self) -> None:
        """record_simple creates an event and adds to buffer."""
        mock_factory = MagicMock()
        sink = BufferedDatabaseUsageEventSink(mock_factory, buffer_size=10)
        metrics = UsageMetrics(latency_ms=150.5)

        sink.record_simple(
            "transcription.completed",
            metrics,
            meeting_id="meeting-456",
        )

        assert sink.pending_count == 1, "buffer should contain exactly one event"

    def test_multiple_records_accumulate(self) -> None:
        """Multiple records accumulate in the buffer."""
        mock_factory = MagicMock()
        sink = BufferedDatabaseUsageEventSink(mock_factory, buffer_size=100)

        sink.record_simple("event.0", meeting_id="meeting-0")
        sink.record_simple("event.1", meeting_id="meeting-1")
        sink.record_simple("event.2", meeting_id="meeting-2")
        sink.record_simple("event.3", meeting_id="meeting-3")
        sink.record_simple("event.4", meeting_id="meeting-4")

        assert sink.pending_count == 5, "buffer should contain all 5 events"

    @pytest.mark.asyncio
    async def test_flush_persists_events(self) -> None:
        """Flush persists buffered events to database."""
        mock_repo = AsyncMock()
        mock_repo.add_batch = AsyncMock(return_value=3)
        mock_factory = MagicMock(return_value=mock_repo)

        sink = BufferedDatabaseUsageEventSink(mock_factory, buffer_size=100)

        sink.record_simple("event.0")
        sink.record_simple("event.1")
        sink.record_simple("event.2")

        count = await sink.flush()

        assert count == 3, "flush should return count of persisted events"
        assert sink.pending_count == 0, "buffer should be empty after flush"
        mock_repo.add_batch.assert_called_once()

    @pytest.mark.asyncio
    async def test_flush_empty_buffer_returns_zero(self) -> None:
        """Flushing empty buffer returns zero."""
        mock_factory = MagicMock()
        sink = BufferedDatabaseUsageEventSink(mock_factory, buffer_size=10)

        count = await sink.flush()

        assert count == 0, "flush of empty buffer should return zero"
        mock_factory.assert_not_called()

    @pytest.mark.asyncio
    async def test_flush_failure_requeues_events(self) -> None:
        """Failed flush re-adds events to buffer for retry."""
        mock_repo = AsyncMock()
        mock_repo.add_batch = AsyncMock(side_effect=Exception("DB error"))
        mock_factory = MagicMock(return_value=mock_repo)

        sink = BufferedDatabaseUsageEventSink(mock_factory, buffer_size=100)

        sink.record_simple("event.0")
        sink.record_simple("event.1")
        sink.record_simple("event.2")

        count = await sink.flush()

        assert count == 0, "failed flush should return zero"
        assert sink.pending_count == 3, "events should be re-queued after failure"

    def test_buffer_respects_max_size(self) -> None:
        """Buffer uses maxlen to prevent unbounded growth."""
        mock_factory = MagicMock()
        buffer_size = 5
        max_expected = buffer_size * 2
        sink = BufferedDatabaseUsageEventSink(mock_factory, buffer_size=buffer_size)

        # Add 15 events (more than buffer_size * 2) inline
        sink.record_simple("event.0")
        sink.record_simple("event.1")
        sink.record_simple("event.2")
        sink.record_simple("event.3")
        sink.record_simple("event.4")
        sink.record_simple("event.5")
        sink.record_simple("event.6")
        sink.record_simple("event.7")
        sink.record_simple("event.8")
        sink.record_simple("event.9")
        sink.record_simple("event.10")
        sink.record_simple("event.11")
        sink.record_simple("event.12")
        sink.record_simple("event.13")
        sink.record_simple("event.14")

        assert sink.pending_count <= max_expected, (
            f"buffer should not exceed {max_expected} (2x buffer_size)"
        )


class TestBufferedDatabaseUsageEventSinkIntegration:
    """Integration-style tests for the sink."""

    @pytest.mark.asyncio
    async def test_full_flow_record_and_flush(self) -> None:
        """Complete flow of recording and flushing events."""
        persisted_events: list[Sequence[UsageEvent]] = []

        async def mock_add_batch(events: Sequence[UsageEvent]) -> int:
            persisted_events.append(events)
            return len(events)

        mock_repo = AsyncMock()
        mock_repo.add_batch = mock_add_batch
        mock_factory = MagicMock(return_value=mock_repo)

        sink = BufferedDatabaseUsageEventSink(mock_factory, buffer_size=50)

        # Record various events
        metrics_summarization = UsageMetrics(
            provider_name="anthropic",
            model_name="claude-3-opus",
            tokens_input=1500,
            latency_ms=2500.0,
        )
        sink.record_simple(
            "summarization.completed",
            metrics_summarization,
            meeting_id="meeting-1",
        )
        metrics_transcription = UsageMetrics(latency_ms=500.0)
        sink.record_simple(
            "transcription.completed",
            metrics_transcription,
            meeting_id="meeting-1",
        )

        count = await sink.flush()

        assert count == 2, "flush should return count of 2 events"
        assert len(persisted_events) == 1, "add_batch should be called once"
        assert len(persisted_events[0]) == 2, "batch should contain 2 events"

        events = list(persisted_events[0])
        assert (
            events[0].event_type == "summarization.completed"
        ), "first event should be summarization.completed"
        assert (
            events[0].provider_name == "anthropic"
        ), "first event provider should be anthropic"
        assert (
            events[1].event_type == "transcription.completed"
        ), "second event should be transcription.completed"
