"""
Unit tests for _OpenAIGroupTracker — the object that aggregates multiple
OpenAI calls into one grouped trace.

Tests add_call(), record_error(), and _complete_trace() in isolation
using a mock Visibe client. No openai package or network required.
"""
import types
from datetime import datetime, timezone

from visibe.integrations.openai import _OpenAIGroupTracker


# ------------------------------------------------------------------
# Minimal mocks
# ------------------------------------------------------------------

class MockVisibe:
    def __init__(self):
        self._trace = None
        self._spans = []
        self._summary = None

    def create_trace(self, data):
        self._trace = data
        return True

    def queue_span(self, trace_id, span):
        self._spans.append(span)

    def complete_trace(self, trace_id, summary):
        self._summary = summary
        return True


class MockIntegration:
    """Minimal stand-in for OpenAIIntegration (only _instrumented_clients needed)."""
    _instrumented_clients = {}


def _call_data(model="gpt-4o-mini", input_tokens=50, output_tokens=30,
               cost=0.000042, duration_ms=500, prompt="Hello", output="Hi there",
               tools=None):
    return {
        "model": model,
        "input_tokens": input_tokens,
        "output_tokens": output_tokens,
        "cost": cost,
        "duration_ms": duration_ms,
        "prompt": prompt,
        "output": output,
        "tools_used": tools or [],
        "timestamp": datetime.now(timezone.utc),
    }


def make_tracker():
    mock_visibe = MockVisibe()
    mock_openai = types.SimpleNamespace(
        chat=types.SimpleNamespace(
            completions=types.SimpleNamespace(create=lambda: None)
        )
    )
    tracker = _OpenAIGroupTracker(mock_visibe, mock_openai, "test-trace", MockIntegration())
    tracker.start_time = datetime.now(timezone.utc)
    return tracker, mock_visibe


# ------------------------------------------------------------------
# add_call
# ------------------------------------------------------------------

def test_add_call_appends_to_calls():
    tracker, _ = make_tracker()
    tracker.add_call(_call_data())
    assert len(tracker.calls) == 1


def test_add_call_queues_span():
    tracker, mock_visibe = make_tracker()
    tracker.add_call(_call_data())
    assert len(mock_visibe._spans) == 1


def test_add_call_span_type_is_llm_call():
    tracker, mock_visibe = make_tracker()
    tracker.add_call(_call_data())
    assert mock_visibe._spans[0]["type"] == "llm_call"


def test_add_call_span_has_correct_model():
    tracker, mock_visibe = make_tracker()
    tracker.add_call(_call_data(model="gpt-4o"))
    assert mock_visibe._spans[0]["model"] == "gpt-4o"


def test_add_call_span_has_token_counts():
    tracker, mock_visibe = make_tracker()
    tracker.add_call(_call_data(input_tokens=100, output_tokens=50))
    span = mock_visibe._spans[0]
    assert span["input_tokens"] == 100
    assert span["output_tokens"] == 50


def test_add_call_span_has_cost():
    tracker, mock_visibe = make_tracker()
    tracker.add_call(_call_data(cost=0.00042))
    assert mock_visibe._spans[0]["cost"] == 0.00042


def test_add_call_span_truncates_input_text():
    tracker, mock_visibe = make_tracker()
    tracker.add_call(_call_data(prompt="x" * 2000))
    assert len(mock_visibe._spans[0]["input_text"]) <= 1000


def test_add_call_span_truncates_output_text():
    tracker, mock_visibe = make_tracker()
    tracker.add_call(_call_data(output="y" * 2000))
    assert len(mock_visibe._spans[0]["output_text"]) <= 1000


def test_add_call_multiple_spans_get_incrementing_ids():
    tracker, mock_visibe = make_tracker()
    tracker.add_call(_call_data())
    tracker.add_call(_call_data())
    ids = [s["span_id"] for s in mock_visibe._spans]
    assert ids[0] != ids[1]
    assert ids == ["step_1", "step_2"]


# ------------------------------------------------------------------
# record_error
# ------------------------------------------------------------------

def test_record_error_appends_to_errors():
    tracker, _ = make_tracker()
    tracker.record_error(RuntimeError("API failed"))
    assert len(tracker.errors) == 1


def test_record_error_queues_error_span():
    tracker, mock_visibe = make_tracker()
    tracker.record_error(ValueError("Bad request"))
    assert mock_visibe._spans[0]["type"] == "error"


def test_record_error_span_has_error_type():
    tracker, mock_visibe = make_tracker()
    tracker.record_error(ConnectionError("Refused"))
    assert mock_visibe._spans[0]["error_type"] == "ConnectionError"


def test_record_error_span_has_error_message():
    tracker, mock_visibe = make_tracker()
    tracker.record_error(RuntimeError("something broke"))
    assert "something broke" in mock_visibe._spans[0]["error_message"]


# ------------------------------------------------------------------
# _complete_trace — token / cost aggregation
# ------------------------------------------------------------------

def test_complete_trace_sums_input_tokens():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [
        _call_data(input_tokens=100),
        _call_data(input_tokens=200),
    ]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["total_input_tokens"] == 300


def test_complete_trace_sums_output_tokens():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [
        _call_data(output_tokens=50),
        _call_data(output_tokens=75),
    ]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["total_output_tokens"] == 125


def test_complete_trace_sums_total_tokens():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [_call_data(input_tokens=100, output_tokens=50)]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["total_tokens"] == 150


def test_complete_trace_sums_cost():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [
        _call_data(cost=0.001),
        _call_data(cost=0.002),
    ]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert abs(mock_visibe._summary["total_cost"] - 0.003) < 1e-6


def test_complete_trace_status_completed_when_no_errors():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [_call_data()]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["status"] == "completed"


def test_complete_trace_status_failed_when_errors_exist():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [_call_data()]
    tracker.errors = [{"error_type": "RuntimeError", "message": "fail"}]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["status"] == "failed"


def test_complete_trace_uses_first_call_prompt():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [
        _call_data(prompt="First prompt"),
        _call_data(prompt="Second prompt"),
    ]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["prompt"] == "First prompt"


def test_complete_trace_uses_first_call_model():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [_call_data(model="gpt-4o-mini")]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["model"] == "gpt-4o-mini"


def test_complete_trace_duration_is_positive():
    tracker, mock_visibe = make_tracker()
    tracker.calls = [_call_data()]
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["duration_ms"] >= 0


def test_complete_trace_empty_calls_uses_name_as_prompt():
    tracker, mock_visibe = make_tracker()
    tracker.calls = []
    tracker.end_time = datetime.now(timezone.utc)
    tracker._complete_trace()
    assert mock_visibe._summary["prompt"] == "test-trace"
