"""Tests for the providers layer: detect_provider(), OpenAIProvider, AnthropicProvider, GeminiProvider."""

import pytest
from types import SimpleNamespace

from agentbudget.providers.base import detect_provider, UsageData
from agentbudget.providers.openai import OpenAIProvider
from agentbudget.providers.anthropic import AnthropicProvider
from agentbudget.providers.gemini import GeminiProvider


# ---------------------------------------------------------------------------
# Helpers / fixtures
# ---------------------------------------------------------------------------

def make_openai_response(model: str = "gpt-4o", prompt_tokens: int = 100, completion_tokens: int = 50) -> SimpleNamespace:
    """Construct a minimal OpenAI-style response via SimpleNamespace.

    Note: Since this is a SimpleNamespace (not openai.types.chat.ChatCompletion),
    detect_provider() step-1 (isinstance check) fails and falls through to the
    model-prefix fallback (step 3).
    """
    return SimpleNamespace(
        model=model,
        usage=SimpleNamespace(prompt_tokens=prompt_tokens, completion_tokens=completion_tokens),
    )


def make_anthropic_response(
    model: str = "claude-3-5-sonnet-20241022",
    input_tokens: int = 100,
    output_tokens: int = 50,
) -> SimpleNamespace:
    """Construct a minimal Anthropic-style response via SimpleNamespace.

    The presence of usage.input_tokens triggers step-2 detection.
    """
    return SimpleNamespace(
        model=model,
        usage=SimpleNamespace(input_tokens=input_tokens, output_tokens=output_tokens),
    )


# ---------------------------------------------------------------------------
# detect_provider — OpenAI isinstance path (real SDK object)
# ---------------------------------------------------------------------------

class TestDetectProviderOpenAIIsInstance:
    """Step 1: isinstance check for openai.types.chat.ChatCompletion."""

    def test_real_openai_chatcompletion_instance_detected(self) -> None:
        """A real openai.ChatCompletion object should be detected as OpenAIProvider."""
        try:
            import openai.types.chat
        except ImportError:
            pytest.skip("openai SDK not installed")

        # Build a real ChatCompletion using the SDK's constructor helpers.
        from openai.types.chat.chat_completion import ChatCompletion
        from openai.types.chat.chat_completion import Choice
        from openai.types.chat.chat_completion_message import ChatCompletionMessage
        from openai.types import CompletionUsage

        response = ChatCompletion(
            id="chatcmpl-test",
            choices=[
                Choice(
                    finish_reason="stop",
                    index=0,
                    message=ChatCompletionMessage(role="assistant", content="hello"),
                )
            ],
            created=1234567890,
            model="gpt-4o",
            object="chat.completion",
            usage=CompletionUsage(
                prompt_tokens=10,
                completion_tokens=5,
                total_tokens=15,
            ),
        )

        provider = detect_provider(response)
        assert isinstance(provider, OpenAIProvider)


# ---------------------------------------------------------------------------
# detect_provider — Anthropic fingerprint (step 2)
# ---------------------------------------------------------------------------

class TestDetectProviderAnthropicFingerprint:
    """Step 2: usage.input_tokens attribute fingerprint."""

    def test_anthropic_fingerprint_detected(self) -> None:
        response = make_anthropic_response()
        provider = detect_provider(response)
        assert isinstance(provider, AnthropicProvider)

    def test_anthropic_fingerprint_with_different_model(self) -> None:
        response = make_anthropic_response(model="claude-opus-4-6")
        provider = detect_provider(response)
        assert isinstance(provider, AnthropicProvider)

    def test_no_usage_attribute_falls_through(self) -> None:
        """Object with no usage falls through to model-prefix check."""
        response = SimpleNamespace(model="claude-3-5-sonnet-20241022")
        # No usage attr — falls through to step 3 (model prefix)
        provider = detect_provider(response)
        assert isinstance(provider, AnthropicProvider)


# ---------------------------------------------------------------------------
# detect_provider — model prefix fallback (step 3)
# ---------------------------------------------------------------------------

class TestDetectProviderModelPrefix:
    def test_gpt_prefix_detected_as_openai(self) -> None:
        response = SimpleNamespace(model="gpt-4o")
        provider = detect_provider(response)
        assert isinstance(provider, OpenAIProvider)

    def test_gpt4o_mini_prefix_detected_as_openai(self) -> None:
        response = SimpleNamespace(model="gpt-4o-mini")
        provider = detect_provider(response)
        assert isinstance(provider, OpenAIProvider)

    def test_o1_prefix_detected_as_openai(self) -> None:
        response = SimpleNamespace(model="o1-mini")
        provider = detect_provider(response)
        assert isinstance(provider, OpenAIProvider)

    def test_o3_prefix_detected_as_openai(self) -> None:
        response = SimpleNamespace(model="o3-mini")
        provider = detect_provider(response)
        assert isinstance(provider, OpenAIProvider)

    def test_claude_prefix_detected_as_anthropic(self) -> None:
        response = SimpleNamespace(model="claude-opus-4-6")
        provider = detect_provider(response)
        assert isinstance(provider, AnthropicProvider)

    def test_claude_haiku_prefix_detected_as_anthropic(self) -> None:
        response = SimpleNamespace(model="claude-3-5-haiku-20241022")
        provider = detect_provider(response)
        assert isinstance(provider, AnthropicProvider)


# ---------------------------------------------------------------------------
# detect_provider — ValueError on unknown
# ---------------------------------------------------------------------------

class TestDetectProviderUnknown:
    def test_unknown_response_raises_value_error(self) -> None:
        response = SimpleNamespace(model="llama-3-8b")
        with pytest.raises(ValueError, match="Cannot detect provider"):
            detect_provider(response)

    def test_no_model_no_usage_raises_value_error(self) -> None:
        response = SimpleNamespace()
        with pytest.raises(ValueError, match="Cannot detect provider"):
            detect_provider(response)

    def test_usage_without_input_tokens_raises_value_error(self) -> None:
        """usage present but no input_tokens — should not be Anthropic fingerprint."""
        response = SimpleNamespace(
            model="llama-2",
            usage=SimpleNamespace(prompt_tokens=10, completion_tokens=5),
        )
        with pytest.raises(ValueError, match="Cannot detect provider"):
            detect_provider(response)


# ---------------------------------------------------------------------------
# OpenAIProvider.extract_usage
# ---------------------------------------------------------------------------

class TestOpenAIProviderExtractUsage:
    def test_returns_correct_usage_data(self) -> None:
        response = make_openai_response(model="gpt-4o", prompt_tokens=800, completion_tokens=443)
        provider = OpenAIProvider()
        usage = provider.extract_usage(response)

        assert isinstance(usage, UsageData)
        assert usage.model == "gpt-4o"
        assert usage.input_tokens == 800
        assert usage.output_tokens == 443
        assert usage.provider == "openai"

    def test_zero_tokens_handled(self) -> None:
        response = make_openai_response(prompt_tokens=0, completion_tokens=0)
        provider = OpenAIProvider()
        usage = provider.extract_usage(response)
        assert usage.input_tokens == 0
        assert usage.output_tokens == 0

    def test_missing_usage_raises_value_error(self) -> None:
        response = SimpleNamespace(model="gpt-4o")  # no usage attribute
        provider = OpenAIProvider()
        with pytest.raises(ValueError, match="no usage data"):
            provider.extract_usage(response)

    def test_usage_none_raises_value_error(self) -> None:
        response = SimpleNamespace(model="gpt-4o", usage=None)
        provider = OpenAIProvider()
        with pytest.raises(ValueError, match="no usage data"):
            provider.extract_usage(response)

    def test_model_unknown_when_no_model_attr(self) -> None:
        response = SimpleNamespace(
            usage=SimpleNamespace(prompt_tokens=10, completion_tokens=5)
        )
        provider = OpenAIProvider()
        usage = provider.extract_usage(response)
        assert usage.model == "unknown"

    def test_gpt4o_mini_model_parsed(self) -> None:
        response = make_openai_response(model="gpt-4o-mini", prompt_tokens=200, completion_tokens=100)
        provider = OpenAIProvider()
        usage = provider.extract_usage(response)
        assert usage.model == "gpt-4o-mini"
        assert usage.input_tokens == 200
        assert usage.output_tokens == 100


# ---------------------------------------------------------------------------
# AnthropicProvider.extract_usage
# ---------------------------------------------------------------------------

class TestAnthropicProviderExtractUsage:
    def test_returns_correct_usage_data(self) -> None:
        response = make_anthropic_response(
            model="claude-3-5-sonnet-20241022",
            input_tokens=100,
            output_tokens=50,
        )
        provider = AnthropicProvider()
        usage = provider.extract_usage(response)

        assert isinstance(usage, UsageData)
        assert usage.model == "claude-3-5-sonnet-20241022"
        assert usage.input_tokens == 100
        assert usage.output_tokens == 50
        assert usage.provider == "anthropic"

    def test_zero_tokens_handled(self) -> None:
        response = make_anthropic_response(input_tokens=0, output_tokens=0)
        provider = AnthropicProvider()
        usage = provider.extract_usage(response)
        assert usage.input_tokens == 0
        assert usage.output_tokens == 0

    def test_missing_usage_raises_value_error(self) -> None:
        response = SimpleNamespace(model="claude-3-5-sonnet-20241022")  # no usage
        provider = AnthropicProvider()
        with pytest.raises(ValueError, match="no usage data"):
            provider.extract_usage(response)

    def test_usage_none_raises_value_error(self) -> None:
        response = SimpleNamespace(model="claude-3-5-sonnet-20241022", usage=None)
        provider = AnthropicProvider()
        with pytest.raises(ValueError, match="no usage data"):
            provider.extract_usage(response)

    def test_model_unknown_when_no_model_attr(self) -> None:
        response = SimpleNamespace(
            usage=SimpleNamespace(input_tokens=10, output_tokens=5)
        )
        provider = AnthropicProvider()
        usage = provider.extract_usage(response)
        assert usage.model == "unknown"

    def test_claude_opus_model_parsed(self) -> None:
        response = make_anthropic_response(model="claude-opus-4-6", input_tokens=500, output_tokens=300)
        provider = AnthropicProvider()
        usage = provider.extract_usage(response)
        assert usage.model == "claude-opus-4-6"
        assert usage.input_tokens == 500
        assert usage.output_tokens == 300


# ---------------------------------------------------------------------------
# GeminiProvider helpers
# ---------------------------------------------------------------------------

def make_gemini_response(
    model: str = "gemini-2.0-flash",
    model_version: str = "",
    prompt_token_count: int = 100,
    candidates_token_count: int = 50,
    include_model_attr: bool = True,
    include_model_version_attr: bool = False,
) -> SimpleNamespace:
    """Construct a minimal Gemini-style response via SimpleNamespace."""
    usage_metadata = SimpleNamespace(
        prompt_token_count=prompt_token_count,
        candidates_token_count=candidates_token_count,
    )
    kwargs: dict = {"usage_metadata": usage_metadata}
    if include_model_attr:
        kwargs["model"] = model
    if include_model_version_attr:
        kwargs["model_version"] = model_version
    return SimpleNamespace(**kwargs)


# ---------------------------------------------------------------------------
# detect_provider — Gemini fingerprint (step 5: usage_metadata)
# ---------------------------------------------------------------------------

class TestDetectProviderGeminiFingerprint:
    """Step 5: usage_metadata.prompt_token_count fingerprint."""

    def test_gemini_fingerprint_detected(self) -> None:
        response = make_gemini_response()
        provider = detect_provider(response)
        assert isinstance(provider, GeminiProvider)

    def test_gemini_fingerprint_without_model_attr(self) -> None:
        """No model attr but has usage_metadata — still detected as Gemini."""
        response = make_gemini_response(include_model_attr=False)
        provider = detect_provider(response)
        assert isinstance(provider, GeminiProvider)

    def test_usage_metadata_none_falls_through(self) -> None:
        """usage_metadata present but None — falls through to model prefix."""
        response = SimpleNamespace(model="gemini-2.0-flash", usage_metadata=None)
        provider = detect_provider(response)
        assert isinstance(provider, GeminiProvider)


# ---------------------------------------------------------------------------
# detect_provider — Gemini model prefix fallback (step 6)
# ---------------------------------------------------------------------------

class TestDetectProviderGeminiModelPrefix:
    def test_gemini_prefix_detected(self) -> None:
        response = SimpleNamespace(model="gemini-2.0-flash")
        provider = detect_provider(response)
        assert isinstance(provider, GeminiProvider)

    def test_gemini_25_pro_detected(self) -> None:
        response = SimpleNamespace(model="gemini-2.5-pro")
        provider = detect_provider(response)
        assert isinstance(provider, GeminiProvider)


# ---------------------------------------------------------------------------
# GeminiProvider.extract_usage
# ---------------------------------------------------------------------------

class TestGeminiProviderExtractUsage:
    def test_returns_correct_usage_data(self) -> None:
        response = make_gemini_response(
            model="gemini-2.0-flash",
            prompt_token_count=800,
            candidates_token_count=443,
        )
        provider = GeminiProvider()
        usage = provider.extract_usage(response)

        assert isinstance(usage, UsageData)
        assert usage.model == "gemini-2.0-flash"
        assert usage.input_tokens == 800
        assert usage.output_tokens == 443
        assert usage.provider == "gemini"

    def test_model_version_used_when_no_model_attr(self) -> None:
        """Older SDK: no model attr, but model_version present."""
        response = make_gemini_response(
            include_model_attr=False,
            include_model_version_attr=True,
            model_version="gemini-2.0-flash",
        )
        provider = GeminiProvider()
        usage = provider.extract_usage(response)
        assert usage.model == "gemini-2.0-flash"

    def test_fallback_to_gemini_when_no_model_info(self) -> None:
        """Oldest SDK: no model or model_version — falls back to 'gemini'."""
        response = make_gemini_response(include_model_attr=False, include_model_version_attr=False)
        provider = GeminiProvider()
        usage = provider.extract_usage(response)
        assert usage.model == "gemini"

    def test_zero_tokens_handled(self) -> None:
        response = make_gemini_response(prompt_token_count=0, candidates_token_count=0)
        provider = GeminiProvider()
        usage = provider.extract_usage(response)
        assert usage.input_tokens == 0
        assert usage.output_tokens == 0

    def test_missing_usage_metadata_raises_value_error(self) -> None:
        response = SimpleNamespace(model="gemini-2.0-flash")  # no usage_metadata
        provider = GeminiProvider()
        with pytest.raises(ValueError, match="no usage_metadata"):
            provider.extract_usage(response)

    def test_usage_metadata_none_raises_value_error(self) -> None:
        response = SimpleNamespace(model="gemini-2.0-flash", usage_metadata=None)
        provider = GeminiProvider()
        with pytest.raises(ValueError, match="no usage_metadata"):
            provider.extract_usage(response)

    def test_model_attr_takes_priority_over_model_version(self) -> None:
        """model attribute takes priority over model_version."""
        response = SimpleNamespace(
            model="gemini-2.5-pro",
            model_version="gemini-2.0-flash",
            usage_metadata=SimpleNamespace(prompt_token_count=10, candidates_token_count=5),
        )
        provider = GeminiProvider()
        usage = provider.extract_usage(response)
        assert usage.model == "gemini-2.5-pro"
