"""Tests for BatchQueue and AsyncBatchQueue."""

from __future__ import annotations

import asyncio
import threading
import time
from typing import Any, Dict, List
from unittest.mock import MagicMock

import pytest

from kanban_analytics.batch import AsyncBatchQueue, BatchQueue


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_event(name: str = "test_event") -> Dict[str, Any]:
    return {
        "messageId": f"msg-{name}",
        "type": "track",
        "event": name,
        "timestamp": "2025-01-01T00:00:00Z",
        "userId": "u1",
    }


# ---------------------------------------------------------------------------
# BatchQueue (sync)
# ---------------------------------------------------------------------------


class TestBatchQueue:
    def test_enqueue_does_not_send_immediately(self) -> None:
        transport = MagicMock()
        q = BatchQueue(transport, batch_size=10, flush_interval=60.0)
        q.enqueue(_make_event())
        q.enqueue(_make_event())
        assert q.length == 2
        transport.assert_not_called()
        q.destroy()

    def test_flush_at_batch_size(self) -> None:
        transport = MagicMock()
        q = BatchQueue(transport, batch_size=3, flush_interval=60.0)
        q.enqueue(_make_event("e1"))
        q.enqueue(_make_event("e2"))
        q.enqueue(_make_event("e3"))  # triggers flush
        transport.assert_called_once()
        assert len(transport.call_args[0][0]) == 3
        assert q.length == 0
        q.destroy()

    def test_manual_flush(self) -> None:
        transport = MagicMock()
        q = BatchQueue(transport, batch_size=100, flush_interval=60.0)
        q.enqueue(_make_event())
        q.enqueue(_make_event())
        result = q.flush()
        assert result["sent"] == 2
        assert result["failed"] == 0
        assert q.length == 0
        q.destroy()

    def test_flush_empty_queue(self) -> None:
        transport = MagicMock()
        q = BatchQueue(transport, batch_size=10, flush_interval=60.0)
        result = q.flush()
        assert result == {"sent": 0, "failed": 0}
        q.destroy()

    def test_reports_failed_on_transport_error(self) -> None:
        transport = MagicMock(side_effect=Exception("network failure"))
        q = BatchQueue(transport, batch_size=100, flush_interval=60.0)
        q.enqueue(_make_event())
        q.enqueue(_make_event())
        result = q.flush()
        assert result["sent"] == 0
        assert result["failed"] == 2
        q.destroy()

    def test_destroy_flushes_remaining(self) -> None:
        transport = MagicMock()
        q = BatchQueue(transport, batch_size=100, flush_interval=60.0)
        q.enqueue(_make_event())
        q.enqueue(_make_event())
        q.enqueue(_make_event())
        result = q.destroy()
        assert result["sent"] == 3
        transport.assert_called_once()

    def test_stops_accepting_after_destroy(self) -> None:
        transport = MagicMock()
        q = BatchQueue(transport, batch_size=100, flush_interval=60.0)
        q.destroy()
        q.enqueue(_make_event())
        assert q.length == 0

    def test_flush_triggered_at_interval(self) -> None:
        transport = MagicMock()
        q = BatchQueue(transport, batch_size=100, flush_interval=0.1)
        q.enqueue(_make_event())
        # Wait for the timer to fire
        time.sleep(0.3)
        transport.assert_called()
        q.destroy()

    def test_thread_safety(self) -> None:
        transport = MagicMock()
        q = BatchQueue(transport, batch_size=1000, flush_interval=60.0)
        errors: List[Exception] = []

        def _enqueue_many() -> None:
            try:
                for i in range(100):
                    q.enqueue(_make_event(f"thread-{threading.current_thread().name}-{i}"))
            except Exception as e:
                errors.append(e)

        threads = [threading.Thread(target=_enqueue_many) for _ in range(5)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert not errors
        # All 500 events should be in the queue (batch_size=1000 so no flush)
        result = q.flush()
        assert result["sent"] == 500
        q.destroy()


# ---------------------------------------------------------------------------
# AsyncBatchQueue
# ---------------------------------------------------------------------------


class TestAsyncBatchQueue:
    @pytest.mark.asyncio
    async def test_enqueue_and_flush(self) -> None:
        async def async_transport(batch: List[Dict[str, Any]]) -> None:
            pass

        q = AsyncBatchQueue(async_transport, batch_size=100, flush_interval=60.0)
        await q.enqueue(_make_event())
        await q.enqueue(_make_event())
        result = await q.flush()
        assert result["sent"] == 2
        assert result["failed"] == 0
        await q.destroy()

    @pytest.mark.asyncio
    async def test_flush_at_batch_size(self) -> None:
        batches_sent: List[List[Dict[str, Any]]] = []

        async def async_transport(batch: List[Dict[str, Any]]) -> None:
            batches_sent.append(batch)

        q = AsyncBatchQueue(async_transport, batch_size=3, flush_interval=60.0)
        await q.enqueue(_make_event("e1"))
        await q.enqueue(_make_event("e2"))
        await q.enqueue(_make_event("e3"))  # triggers flush
        assert len(batches_sent) == 1
        assert len(batches_sent[0]) == 3
        await q.destroy()

    @pytest.mark.asyncio
    async def test_destroy_flushes_remaining(self) -> None:
        batches_sent: List[List[Dict[str, Any]]] = []

        async def async_transport(batch: List[Dict[str, Any]]) -> None:
            batches_sent.append(batch)

        q = AsyncBatchQueue(async_transport, batch_size=100, flush_interval=60.0)
        await q.enqueue(_make_event())
        await q.enqueue(_make_event())
        result = await q.destroy()
        assert result["sent"] == 2

    @pytest.mark.asyncio
    async def test_concurrent_enqueue(self) -> None:
        batches_sent: List[List[Dict[str, Any]]] = []

        async def async_transport(batch: List[Dict[str, Any]]) -> None:
            batches_sent.append(batch)

        q = AsyncBatchQueue(async_transport, batch_size=1000, flush_interval=60.0)

        async def _enqueue_many(prefix: str) -> None:
            for i in range(100):
                await q.enqueue(_make_event(f"{prefix}-{i}"))

        await asyncio.gather(*[_enqueue_many(f"t{i}") for i in range(5)])
        result = await q.flush()
        assert result["sent"] == 500
        await q.destroy()
