"""Tests for MemoryBuffer and BatchProcessor."""
from __future__ import annotations

import time
from unittest.mock import patch, MagicMock

import pytest

from fibinaci.buffer.memory_buffer import MemoryBuffer
from fibinaci.buffer.batch_processor import BatchProcessor
from fibinaci._config import FibinaciConfig


class TestMemoryBuffer:
    def test_push_and_drain(self):
        buf = MemoryBuffer(max_size=100)
        buf.push({"a": 1})
        buf.push({"b": 2})
        assert buf.size == 2
        items = buf.drain()
        assert len(items) == 2
        assert buf.size == 0

    def test_drain_with_limit(self):
        buf = MemoryBuffer()
        for i in range(10):
            buf.push({"i": i})
        items = buf.drain(max_items=3)
        assert len(items) == 3
        assert buf.size == 7

    def test_ring_buffer_drops_oldest(self):
        buf = MemoryBuffer(max_size=3)
        for i in range(5):
            buf.push({"i": i})
        assert buf.size == 3
        items = buf.drain()
        # Should have items 2, 3, 4 (oldest 0, 1 dropped)
        assert [i["i"] for i in items] == [2, 3, 4]

    def test_peek_doesnt_remove(self):
        buf = MemoryBuffer()
        buf.push({"x": 1})
        peeked = buf.peek()
        assert len(peeked) == 1
        assert buf.size == 1  # still there

    def test_clear(self):
        buf = MemoryBuffer()
        buf.push({"x": 1})
        buf.clear()
        assert buf.size == 0

    def test_thread_safety(self):
        import threading

        buf = MemoryBuffer(max_size=10_000)
        errors = []

        def pusher():
            try:
                for i in range(1000):
                    buf.push({"i": i})
            except Exception as e:
                errors.append(e)

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

        assert not errors
        assert buf.size == 4000


class TestBatchProcessor:
    def test_start_and_stop(self):
        buf = MemoryBuffer()
        config = FibinaciConfig(api_key="test", flush_interval=0.1)
        proc = BatchProcessor(config=config, buffer=buf)
        proc.start()
        assert proc._thread is not None
        assert proc._thread.is_alive()
        proc.stop()
        assert proc._thread is None

    def test_flush_on_stop(self):
        buf = MemoryBuffer()
        config = FibinaciConfig(api_key="test", flush_interval=60)  # long interval
        proc = BatchProcessor(config=config, buffer=buf)

        buf.push({"trace_id": "a" * 32, "span_id": "b" * 16, "span_kind": "model_call",
                   "started_at": "2025-01-01T00:00:00Z", "status": "ok"})

        proc.start()

        with patch("httpx.post") as mock_post:
            mock_post.return_value = MagicMock(status_code=202)
            proc.stop()
            # Should have flushed the span
            assert mock_post.called

    def test_no_api_key_drops_silently(self):
        buf = MemoryBuffer()
        config = FibinaciConfig(api_key="", flush_interval=60)
        proc = BatchProcessor(config=config, buffer=buf)

        buf.push({"trace_id": "a" * 32, "span_id": "b" * 16, "span_kind": "custom",
                   "started_at": "2025-01-01T00:00:00Z", "status": "ok"})

        # Should not raise
        proc._flush()
        assert buf.size == 0  # drained but not sent

    def test_outcomes_sent_separately(self):
        buf = MemoryBuffer()
        config = FibinaciConfig(api_key="test", flush_interval=60)
        proc = BatchProcessor(config=config, buffer=buf)

        buf.push({"_fibinaci_type": "outcome", "trace_id": "t1", "success": True})

        with patch("httpx.post") as mock_post:
            mock_post.return_value = MagicMock(status_code=201)
            proc._flush()
            # Should POST to /v1/traces/t1/outcome
            assert mock_post.called
            call_url = mock_post.call_args_list[0][0][0]
            assert "/v1/traces/t1/outcome" in call_url
