"""
Unit tests for visibe/integrations/anthropic.py.
No anthropic package or network required.
"""
import types
import uuid
from datetime import datetime, timezone

from visibe.integrations.anthropic import (
    _extract_prompt,
    _extract_output_text,
    _extract_tool_calls,
    _AnthropicGroupTracker,
    AnthropicIntegration,
)


# ------------------------------------------------------------------
# Shared mock
# ------------------------------------------------------------------

class _MockVisibe:
    def __init__(self):
        self.session_id = None
        self._trace_header = None
        self._spans = []
        self._summary = None

    def create_trace(self, data):
        self._trace_header = 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


# ------------------------------------------------------------------
# _extract_prompt
# ------------------------------------------------------------------

def test_extract_prompt_simple_string():
    msgs = [{"role": "user", "content": "Hello there"}]
    assert _extract_prompt(msgs) == "Hello there"


def test_extract_prompt_returns_last_user():
    msgs = [
        {"role": "user", "content": "First"},
        {"role": "assistant", "content": "Reply"},
        {"role": "user", "content": "Second"},
    ]
    assert _extract_prompt(msgs) == "Second"


def test_extract_prompt_skips_non_user():
    msgs = [{"role": "assistant", "content": "Hi"}]
    assert _extract_prompt(msgs) == ""


def test_extract_prompt_empty_list():
    assert _extract_prompt([]) == ""


def test_extract_prompt_none():
    assert _extract_prompt(None) == ""


def test_extract_prompt_multimodal_content():
    msgs = [{"role": "user", "content": [
        {"type": "image", "source": {}},
        {"type": "text", "text": "What is this?"},
    ]}]
    assert _extract_prompt(msgs) == "What is this?"


def test_extract_prompt_truncates_to_500():
    msgs = [{"role": "user", "content": "x" * 1000}]
    assert len(_extract_prompt(msgs)) == 500


# ------------------------------------------------------------------
# _extract_output_text
# ------------------------------------------------------------------

def _text_block(text):
    return types.SimpleNamespace(type='text', text=text)


def _tool_block(name='search', inp={}):
    return types.SimpleNamespace(type='tool_use', name=name, input=inp)


def test_extract_output_text_single_block():
    assert _extract_output_text([_text_block("Hello!")]) == "Hello!"


def test_extract_output_text_multiple_blocks():
    blocks = [_text_block("Part 1"), _text_block("Part 2")]
    result = _extract_output_text(blocks)
    assert "Part 1" in result and "Part 2" in result


def test_extract_output_text_skips_tool_blocks():
    blocks = [_tool_block("get_weather"), _text_block("It's sunny")]
    assert _extract_output_text(blocks) == "It's sunny"


def test_extract_output_text_empty_list():
    assert _extract_output_text([]) == ""


def test_extract_output_text_none():
    assert _extract_output_text(None) == ""


def test_extract_output_text_dict_blocks():
    blocks = [{"type": "text", "text": "Hello from dict"}]
    assert _extract_output_text(blocks) == "Hello from dict"


def test_extract_output_text_truncates_to_1000():
    blocks = [_text_block("x" * 2000)]
    assert len(_extract_output_text(blocks)) == 1000


# ------------------------------------------------------------------
# _extract_tool_calls
# ------------------------------------------------------------------

def test_extract_tool_calls_single():
    blocks = [_tool_block("search", {"query": "weather"})]
    tools = _extract_tool_calls(blocks)
    assert len(tools) == 1
    assert tools[0]['tool_name'] == 'search'
    assert tools[0]['agent_name'] == 'anthropic'


def test_extract_tool_calls_multiple():
    blocks = [_tool_block("search"), _tool_block("calculator")]
    tools = _extract_tool_calls(blocks)
    assert len(tools) == 2
    assert tools[0]['tool_name'] == 'search'
    assert tools[1]['tool_name'] == 'calculator'


def test_extract_tool_calls_no_tools():
    blocks = [_text_block("Hello")]
    assert _extract_tool_calls(blocks) == []


def test_extract_tool_calls_empty():
    assert _extract_tool_calls([]) == []


def test_extract_tool_calls_dict_blocks():
    blocks = [{"type": "tool_use", "name": "lookup", "input": {"id": 42}}]
    tools = _extract_tool_calls(blocks)
    assert len(tools) == 1
    assert tools[0]['tool_name'] == 'lookup'


# ------------------------------------------------------------------
# _AnthropicGroupTracker._complete_trace() — llm_call_count
# ------------------------------------------------------------------

def _make_tracker(n_calls=1):
    mock = _MockVisibe()
    integration = AnthropicIntegration.__new__(AnthropicIntegration)
    integration.client = mock
    integration._instrumented_clients = {}
    integration._insideStream = False

    mock_client = types.SimpleNamespace(
        messages=types.SimpleNamespace(create=lambda: None, stream=lambda: None)
    )
    tracker = _AnthropicGroupTracker(mock, mock_client, "test-trace", integration)
    tracker.start_time = datetime.now(timezone.utc)
    tracker.end_time = datetime.now(timezone.utc)

    for i in range(n_calls):
        tracker.calls.append({
            'timestamp': datetime.now(timezone.utc),
            'model': 'claude-3-5-sonnet-20241022',
            'prompt': f'prompt {i}',
            'output': f'response {i}',
            'input_tokens': 100,
            'output_tokens': 50,
            'cost': 0.001,
            'duration_ms': 400,
            'tools_used': [],
        })
    return tracker, mock


def test_group_tracker_llm_call_count_single():
    tracker, mock = _make_tracker(n_calls=1)
    tracker._complete_trace()
    assert mock._summary['llm_call_count'] == 1


def test_group_tracker_llm_call_count_multiple():
    tracker, mock = _make_tracker(n_calls=4)
    tracker._complete_trace()
    assert mock._summary['llm_call_count'] == 4


def test_group_tracker_llm_call_count_zero():
    tracker, mock = _make_tracker(n_calls=0)
    tracker._complete_trace()
    assert mock._summary['llm_call_count'] == 0


def test_group_tracker_summary_fields():
    tracker, mock = _make_tracker(n_calls=2)
    tracker._complete_trace()
    s = mock._summary
    assert s['status'] == 'completed'
    assert s['total_input_tokens'] == 200
    assert s['total_output_tokens'] == 100
    assert s['model'] == 'claude-3-5-sonnet-20241022'


def test_group_tracker_status_failed_on_error():
    tracker, mock = _make_tracker(n_calls=1)
    tracker.errors.append({'type': 'APIError', 'message': 'rate limited'})
    tracker._complete_trace()
    assert mock._summary['status'] == 'failed'


# ------------------------------------------------------------------
# AnthropicIntegration._execute_and_trace() — instrument mode
# ------------------------------------------------------------------

def _make_response(model='claude-3-5-sonnet-20241022', input_tokens=80,
                   output_tokens=40, text='Hello!', tool_use=None):
    content = [types.SimpleNamespace(type='text', text=text)]
    if tool_use:
        content.append(types.SimpleNamespace(
            type='tool_use', name=tool_use, input={}
        ))
    return types.SimpleNamespace(
        model=model,
        content=content,
        usage=types.SimpleNamespace(
            input_tokens=input_tokens,
            output_tokens=output_tokens,
        ),
    )


def test_execute_and_trace_sends_trace():
    mock = _MockVisibe()
    integration = AnthropicIntegration.__new__(AnthropicIntegration)
    integration.client = mock
    integration._instrumented_clients = {}
    integration._insideStream = False

    response = _make_response()
    integration._execute_and_trace(
        lambda *a, **kw: response,
        mock,
        None,  # no group_tracker
        (),
        {'model': 'claude-3-5-sonnet-20241022',
         'messages': [{'role': 'user', 'content': 'Hi'}],
         'max_tokens': 100},
        name='test-call',
    )

    assert mock._trace_header is not None
    assert mock._trace_header['framework'] == 'anthropic'
    assert mock._summary is not None
    assert mock._summary['llm_call_count'] == 1
    assert mock._summary['total_input_tokens'] == 80
    assert mock._summary['total_output_tokens'] == 40


def test_execute_and_trace_prompt_captured():
    mock = _MockVisibe()
    integration = AnthropicIntegration.__new__(AnthropicIntegration)
    integration.client = mock
    integration._instrumented_clients = {}
    integration._insideStream = False

    response = _make_response()
    integration._execute_and_trace(
        lambda *a, **kw: response,
        mock,
        None,
        (),
        {'model': 'claude-3-5-sonnet-20241022',
         'messages': [{'role': 'user', 'content': 'What time is it?'}],
         'max_tokens': 100},
    )
    assert mock._summary['prompt'] == 'What time is it?'


def test_execute_and_trace_with_tool_call():
    mock = _MockVisibe()
    integration = AnthropicIntegration.__new__(AnthropicIntegration)
    integration.client = mock
    integration._instrumented_clients = {}
    integration._insideStream = False

    response = _make_response(tool_use='get_weather')
    integration._execute_and_trace(
        lambda *a, **kw: response,
        mock,
        None,
        (),
        {'model': 'claude-3-5-sonnet-20241022',
         'messages': [{'role': 'user', 'content': 'Weather?'}],
         'max_tokens': 100},
    )

    tool_spans = [s for s in mock._spans if s.get('type') == 'tool_call']
    assert len(tool_spans) == 1
    assert tool_spans[0]['tool_name'] == 'get_weather'


def test_execute_and_trace_reraises_exception():
    mock = _MockVisibe()
    integration = AnthropicIntegration.__new__(AnthropicIntegration)
    integration.client = mock
    integration._instrumented_clients = {}
    integration._insideStream = False

    import pytest
    with pytest.raises(RuntimeError, match="API down"):
        integration._execute_and_trace(
            lambda *a, **kw: (_ for _ in ()).throw(RuntimeError("API down")),
            mock,
            None,
            (),
            {'model': 'claude-3-5-sonnet-20241022',
             'messages': [{'role': 'user', 'content': 'test'}],
             'max_tokens': 10},
        )

    # Should have sent a failed trace
    assert mock._summary['status'] == 'failed'
