"""
Unit tests for visibe/api.py — APIClient and SpanBatcher.
HTTP calls are mocked — no network required.
"""
import time
from unittest.mock import patch, MagicMock

import pytest

from visibe.api import APIClient, SpanBatcher


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

def make_client(api_key=None):
    return APIClient(api_url="http://localhost:3000", api_key=api_key)


def mock_response(status_code=200):
    resp = MagicMock()
    resp.status_code = status_code
    resp.raise_for_status = MagicMock()
    if status_code >= 400:
        from requests.exceptions import HTTPError
        resp.raise_for_status.side_effect = HTTPError(response=resp)
    return resp


# ------------------------------------------------------------------
# _headers
# ------------------------------------------------------------------

def test_headers_content_type():
    client = make_client()
    assert client._headers()["Content-Type"] == "application/json"


def test_headers_no_auth_without_key():
    client = make_client()
    assert "Authorization" not in client._headers()


def test_headers_auth_with_key():
    client = make_client(api_key="sk_live_abc123")
    assert client._headers()["Authorization"] == "Bearer sk_live_abc123"


# ------------------------------------------------------------------
# create_trace
# ------------------------------------------------------------------

def test_create_trace_posts_to_correct_url():
    client = make_client()
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.create_trace({"trace_id": "t1", "name": "test"})
        url = mock_req.call_args[0][1]
        assert url == "http://localhost:3000/api/traces"


def test_create_trace_uses_post():
    client = make_client()
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.create_trace({"trace_id": "t1"})
        method = mock_req.call_args[0][0]
        assert method == "POST"


def test_create_trace_sends_payload():
    client = make_client()
    payload = {"trace_id": "t1", "name": "my-trace", "framework": "openai"}
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.create_trace(payload)
        sent = mock_req.call_args[1]["json"]
        assert sent == payload


def test_create_trace_returns_true_on_success():
    client = make_client()
    with patch("requests.request", return_value=mock_response(200)):
        result = client.create_trace({"trace_id": "t1"})
        assert result is True


def test_create_trace_returns_false_on_http_error():
    client = make_client()
    with patch("requests.request", return_value=mock_response(500)):
        result = client.create_trace({"trace_id": "t1"})
        assert result is False


# ------------------------------------------------------------------
# send_span
# ------------------------------------------------------------------

def test_send_span_posts_to_correct_url():
    client = make_client()
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.send_span("trace-abc", {"span_id": "s1", "type": "llm_call"})
        url = mock_req.call_args[0][1]
        assert url == "http://localhost:3000/api/traces/trace-abc/spans"


def test_send_span_uses_post():
    client = make_client()
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.send_span("trace-abc", {"span_id": "s1"})
        assert mock_req.call_args[0][0] == "POST"


def test_send_span_sends_payload():
    client = make_client()
    span = {"span_id": "s1", "type": "llm_call", "model": "gpt-4o-mini"}
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.send_span("trace-abc", span)
        assert mock_req.call_args[1]["json"] == span


# ------------------------------------------------------------------
# send_spans_batch
# ------------------------------------------------------------------

def test_send_spans_batch_posts_to_batch_url():
    client = make_client()
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.send_spans_batch("trace-abc", [{"span_id": "s1"}])
        url = mock_req.call_args[0][1]
        assert url == "http://localhost:3000/api/traces/trace-abc/spans/batch"


def test_send_spans_batch_wraps_in_spans_key():
    client = make_client()
    spans = [{"span_id": "s1"}, {"span_id": "s2"}]
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.send_spans_batch("trace-abc", spans)
        sent = mock_req.call_args[1]["json"]
        assert sent == {"spans": spans}


# ------------------------------------------------------------------
# complete_trace
# ------------------------------------------------------------------

def test_complete_trace_patches_correct_url():
    client = make_client()
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.complete_trace("trace-abc", {"status": "completed"})
        url = mock_req.call_args[0][1]
        assert url == "http://localhost:3000/api/traces/trace-abc"


def test_complete_trace_uses_patch():
    client = make_client()
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.complete_trace("trace-abc", {"status": "completed"})
        assert mock_req.call_args[0][0] == "PATCH"


def test_complete_trace_sends_summary():
    client = make_client()
    summary = {"status": "completed", "total_cost": 0.001, "total_tokens": 100}
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.complete_trace("trace-abc", summary)
        assert mock_req.call_args[1]["json"] == summary


# ------------------------------------------------------------------
# Error handling
# ------------------------------------------------------------------

def test_connection_error_returns_false():
    from requests.exceptions import ConnectionError as ReqConnectionError
    client = make_client()
    with patch("requests.request", side_effect=ReqConnectionError("refused")):
        assert client.create_trace({"trace_id": "t1"}) is False


def test_timeout_returns_false():
    from requests.exceptions import Timeout
    client = make_client()
    with patch("requests.request", side_effect=Timeout()):
        assert client.create_trace({"trace_id": "t1"}) is False


def test_generic_exception_returns_false():
    client = make_client()
    with patch("requests.request", side_effect=RuntimeError("unexpected")):
        assert client.create_trace({"trace_id": "t1"}) is False


def test_url_trailing_slash_stripped():
    client = APIClient(api_url="http://localhost:3000/", api_key=None)
    with patch("requests.request", return_value=mock_response(200)) as mock_req:
        client.create_trace({"trace_id": "t1"})
        url = mock_req.call_args[0][1]
        assert not url.startswith("http://localhost:3000//")


# ------------------------------------------------------------------
# SpanBatcher
# ------------------------------------------------------------------

def test_spanbatcher_add_buffers_span():
    mock_api = MagicMock()
    batcher = SpanBatcher(mock_api, batch_size=100, flush_interval=60)
    try:
        batcher.add("trace-1", {"span_id": "s1"})
        with batcher._lock:
            assert len(batcher._buffer) == 1
    finally:
        batcher.shutdown()


def test_spanbatcher_flush_sends_batch():
    mock_api = MagicMock()
    batcher = SpanBatcher(mock_api, batch_size=100, flush_interval=60)
    try:
        batcher.add("trace-1", {"span_id": "s1"})
        batcher.add("trace-1", {"span_id": "s2"})
        batcher.flush()
        mock_api.send_spans_batch.assert_called_once_with(
            "trace-1", [{"span_id": "s1"}, {"span_id": "s2"}]
        )
    finally:
        batcher.shutdown()


def test_spanbatcher_flush_clears_buffer():
    mock_api = MagicMock()
    batcher = SpanBatcher(mock_api, batch_size=100, flush_interval=60)
    try:
        batcher.add("trace-1", {"span_id": "s1"})
        batcher.flush()
        with batcher._lock:
            assert batcher._buffer == []
    finally:
        batcher.shutdown()


def test_spanbatcher_groups_by_trace_id():
    mock_api = MagicMock()
    batcher = SpanBatcher(mock_api, batch_size=100, flush_interval=60)
    try:
        batcher.add("trace-A", {"span_id": "s1"})
        batcher.add("trace-B", {"span_id": "s2"})
        batcher.add("trace-A", {"span_id": "s3"})
        batcher.flush()

        calls = {call[0][0]: call[0][1] for call in mock_api.send_spans_batch.call_args_list}
        assert len(calls["trace-A"]) == 2
        assert len(calls["trace-B"]) == 1
    finally:
        batcher.shutdown()


def test_spanbatcher_auto_flushes_at_batch_size():
    mock_api = MagicMock()
    batcher = SpanBatcher(mock_api, batch_size=3, flush_interval=60)
    try:
        batcher.add("trace-1", {"span_id": "s1"})
        batcher.add("trace-1", {"span_id": "s2"})
        batcher.add("trace-1", {"span_id": "s3"})  # hits batch_size
        # Give background thread a moment to flush
        time.sleep(0.2)
        assert mock_api.send_spans_batch.called
    finally:
        batcher.shutdown()
