"""
Unit tests for roboco.billing.pricing — calculate_cost().

Covers:
- Each model tier (opus, sonnet, haiku) with all 4 token types.
- Unknown model name returns 0.0 without raising.
- Empty model string returns 0.0 without raising.
- Substring match correctness: longer fragment wins
  (e.g. 'claude-sonnet-4-6' matches 'claude-sonnet-4' not bare 'sonnet').
"""

from __future__ import annotations

import pytest
from roboco.billing.pricing import _is_anthropic_model, calculate_cost

# ---------------------------------------------------------------------------
# Named constants (ruff PLR2004: magic values in comparisons must be named).
# ---------------------------------------------------------------------------

# Token counts
_M = 1_000_000  # 1 million tokens

# Pricing — per-1M USD, matches the _PRICING table in pricing.py
_OPUS_INPUT = 5.00
_OPUS_OUTPUT = 25.00
_OPUS_CACHE_READ = 0.50
_OPUS_CACHE_WRITE = 6.25

_SONNET_INPUT = 3.00
_SONNET_OUTPUT = 15.00
_SONNET_CACHE_READ = 0.30
_SONNET_CACHE_WRITE = 0.75

_HAIKU_INPUT = 1.00
_HAIKU_OUTPUT = 5.00
_HAIKU_CACHE_READ = 0.10
_HAIKU_CACHE_WRITE = 1.25

_HAIKU3_INPUT = 0.25  # claude-haiku-3 is cheaper than haiku-3-5 / haiku-4

# xAI Grok — priced non-Anthropic (per the xAI API)
_GROK_INPUT = 1.00
_GROK_OUTPUT = 2.00
_GROK_CACHE_READ = 0.20
_GROK_CACHE_WRITE = 1.00

# Tolerance for floating-point comparisons
_TOL = 1e-4


# ---------------------------------------------------------------------------
# Opus tier
# ---------------------------------------------------------------------------


class TestOpusTier:
    """claude-opus-4 family pricing."""

    def test_input_only(self) -> None:
        cost = calculate_cost("claude-opus-4-5", tokens_input=_M, tokens_output=0)
        assert abs(cost - _OPUS_INPUT) < _TOL

    def test_output_only(self) -> None:
        cost = calculate_cost("claude-opus-4-5", tokens_input=0, tokens_output=_M)
        assert abs(cost - _OPUS_OUTPUT) < _TOL

    def test_cache_read_only(self) -> None:
        cost = calculate_cost(
            "claude-opus-4-5",
            tokens_input=0,
            tokens_output=0,
            tokens_cache_read=_M,
        )
        assert abs(cost - _OPUS_CACHE_READ) < _TOL

    def test_cache_write_only(self) -> None:
        cost = calculate_cost(
            "claude-opus-4-5",
            tokens_input=0,
            tokens_output=0,
            tokens_cache_write=_M,
        )
        assert abs(cost - _OPUS_CACHE_WRITE) < _TOL

    def test_all_token_types(self) -> None:
        cost = calculate_cost(
            "claude-opus-4-5",
            tokens_input=_M,
            tokens_output=_M,
            tokens_cache_read=_M,
            tokens_cache_write=_M,
        )
        expected = _OPUS_INPUT + _OPUS_OUTPUT + _OPUS_CACHE_READ + _OPUS_CACHE_WRITE
        assert abs(cost - expected) < _TOL

    def test_short_alias(self) -> None:
        """Bare 'opus' alias resolves to the opus tier."""
        cost = calculate_cost("opus", tokens_input=_M, tokens_output=0)
        assert abs(cost - _OPUS_INPUT) < _TOL

    def test_returns_float(self) -> None:
        cost = calculate_cost("claude-opus-4", tokens_input=100, tokens_output=50)
        assert isinstance(cost, float)


# ---------------------------------------------------------------------------
# Sonnet tier
# ---------------------------------------------------------------------------


class TestSonnetTier:
    """claude-sonnet-4 family pricing."""

    def test_input_only(self) -> None:
        cost = calculate_cost("claude-sonnet-4-6", tokens_input=_M, tokens_output=0)
        assert abs(cost - _SONNET_INPUT) < _TOL

    def test_output_only(self) -> None:
        cost = calculate_cost("claude-sonnet-4-6", tokens_input=0, tokens_output=_M)
        assert abs(cost - _SONNET_OUTPUT) < _TOL

    def test_cache_read_only(self) -> None:
        cost = calculate_cost(
            "claude-sonnet-4-6",
            tokens_input=0,
            tokens_output=0,
            tokens_cache_read=_M,
        )
        assert abs(cost - _SONNET_CACHE_READ) < _TOL

    def test_cache_write_only(self) -> None:
        cost = calculate_cost(
            "claude-sonnet-4-6",
            tokens_input=0,
            tokens_output=0,
            tokens_cache_write=_M,
        )
        assert abs(cost - _SONNET_CACHE_WRITE) < _TOL

    def test_all_token_types(self) -> None:
        cost = calculate_cost(
            "claude-sonnet-4-6",
            tokens_input=_M,
            tokens_output=_M,
            tokens_cache_read=_M,
            tokens_cache_write=_M,
        )
        expected = (
            _SONNET_INPUT + _SONNET_OUTPUT + _SONNET_CACHE_READ + _SONNET_CACHE_WRITE
        )
        assert abs(cost - expected) < _TOL

    def test_short_alias(self) -> None:
        """Bare 'sonnet' alias resolves to the sonnet tier."""
        cost = calculate_cost("sonnet", tokens_input=_M, tokens_output=0)
        assert abs(cost - _SONNET_INPUT) < _TOL

    def test_35_variant(self) -> None:
        """claude-3-5-sonnet resolves to sonnet tier."""
        cost = calculate_cost(
            "claude-3-5-sonnet-20241022", tokens_input=_M, tokens_output=0
        )
        assert abs(cost - _SONNET_INPUT) < _TOL


# ---------------------------------------------------------------------------
# Haiku tier
# ---------------------------------------------------------------------------


class TestHaikuTier:
    """claude-haiku family pricing."""

    def test_input_only(self) -> None:
        cost = calculate_cost("claude-haiku-4-5", tokens_input=_M, tokens_output=0)
        assert abs(cost - _HAIKU_INPUT) < _TOL

    def test_output_only(self) -> None:
        cost = calculate_cost("claude-haiku-4-5", tokens_input=0, tokens_output=_M)
        assert abs(cost - _HAIKU_OUTPUT) < _TOL

    def test_cache_read_only(self) -> None:
        cost = calculate_cost(
            "claude-haiku-4-5",
            tokens_input=0,
            tokens_output=0,
            tokens_cache_read=_M,
        )
        assert abs(cost - _HAIKU_CACHE_READ) < _TOL

    def test_cache_write_only(self) -> None:
        cost = calculate_cost(
            "claude-haiku-4-5",
            tokens_input=0,
            tokens_output=0,
            tokens_cache_write=_M,
        )
        assert abs(cost - _HAIKU_CACHE_WRITE) < _TOL

    def test_all_token_types(self) -> None:
        cost = calculate_cost(
            "claude-haiku-4-5",
            tokens_input=_M,
            tokens_output=_M,
            tokens_cache_read=_M,
            tokens_cache_write=_M,
        )
        expected = _HAIKU_INPUT + _HAIKU_OUTPUT + _HAIKU_CACHE_READ + _HAIKU_CACHE_WRITE
        assert abs(cost - expected) < _TOL

    def test_short_alias(self) -> None:
        """Bare 'haiku' alias resolves to the haiku tier."""
        cost = calculate_cost("haiku", tokens_input=_M, tokens_output=0)
        assert abs(cost - _HAIKU_INPUT) < _TOL

    def test_haiku3_variant(self) -> None:
        """claude-haiku-3 has lower pricing than haiku-3-5."""
        cost = calculate_cost("claude-haiku-3", tokens_input=_M, tokens_output=0)
        assert abs(cost - _HAIKU3_INPUT) < _TOL


# ---------------------------------------------------------------------------
# Grok tier (xAI — priced non-Anthropic)
# ---------------------------------------------------------------------------


class TestGrokTier:
    """grok-build-0.1 pricing — a non-Anthropic model that IS billed per token."""

    def test_input_only(self) -> None:
        cost = calculate_cost("grok-build-0.1", tokens_input=_M, tokens_output=0)
        assert abs(cost - _GROK_INPUT) < _TOL

    def test_output_only(self) -> None:
        cost = calculate_cost("grok-build-0.1", tokens_input=0, tokens_output=_M)
        assert abs(cost - _GROK_OUTPUT) < _TOL

    def test_cached_input(self) -> None:
        cost = calculate_cost(
            "grok-build-0.1", tokens_input=0, tokens_output=0, tokens_cache_read=_M
        )
        assert abs(cost - _GROK_CACHE_READ) < _TOL

    def test_all_token_types(self) -> None:
        cost = calculate_cost(
            "grok-build-0.1",
            tokens_input=_M,
            tokens_output=_M,
            tokens_cache_read=_M,
            tokens_cache_write=_M,
        )
        expected = _GROK_INPUT + _GROK_OUTPUT + _GROK_CACHE_READ + _GROK_CACHE_WRITE
        assert abs(cost - expected) < _TOL

    def test_grok_is_not_treated_as_anthropic(self) -> None:
        """Priced, but not an Anthropic model (no warn-on-unpriced path)."""
        assert _is_anthropic_model("grok-build-0.1") is False
        # Still resolves to a real (non-zero) per-token cost.
        assert calculate_cost("grok-build-0.1", tokens_input=_M, tokens_output=0) > 0.0


# ---------------------------------------------------------------------------
# Unknown / edge cases — must return 0.0 without raising
# ---------------------------------------------------------------------------


class TestUnknownModels:
    def test_unknown_model_name_returns_zero(self) -> None:
        cost = calculate_cost("gpt-4o", tokens_input=_M, tokens_output=_M)
        assert cost == 0.0

    def test_empty_string_returns_zero(self) -> None:
        cost = calculate_cost("", tokens_input=_M, tokens_output=_M)
        assert cost == 0.0

    def test_gibberish_returns_zero(self) -> None:
        cost = calculate_cost(
            "totally-unknown-model-xyz", tokens_input=100, tokens_output=100
        )
        assert cost == 0.0

    def test_zero_tokens_with_unknown_model_returns_zero(self) -> None:
        cost = calculate_cost("unknown", tokens_input=0, tokens_output=0)
        assert cost == 0.0

    def test_does_not_raise_on_unknown_model(self) -> None:
        """Must not raise regardless of token counts."""
        try:
            calculate_cost(
                "not-a-claude-model",
                tokens_input=999_999,
                tokens_output=999_999,
            )
        except Exception as exc:
            pytest.fail(f"calculate_cost raised unexpectedly: {exc}")


# ---------------------------------------------------------------------------
# Substring match correctness
# ---------------------------------------------------------------------------

# Named constants for the comparison floor/ceiling used in these tests.
_ZERO_COST = 0.0
_SONNET_CHEAPER_THAN_OPUS = True  # structural assertion in the test below


class TestSubstringMatchPriority:
    def test_claude_sonnet_4_resolves_non_zero(self) -> None:
        """'claude-sonnet-4-6' must find a match (non-zero cost)."""
        cost = calculate_cost("claude-sonnet-4-6", tokens_input=_M, tokens_output=0)
        assert cost > _ZERO_COST

    def test_haiku3_cheaper_than_haiku4(self) -> None:
        """claude-haiku-3 is cheaper than claude-haiku-4 — longest-match wins."""
        haiku3_cost = calculate_cost("claude-haiku-3", tokens_input=_M, tokens_output=0)
        haiku4_cost = calculate_cost("claude-haiku-4", tokens_input=_M, tokens_output=0)
        # haiku-3 ($0.25/1M) < haiku-4 ($1.00/1M)
        assert haiku3_cost < haiku4_cost

    def test_non_claude_model_returns_zero(self) -> None:
        """A random non-Claude model must not match any Claude pricing entry."""
        non_opus_cost = calculate_cost("llama-3-70b", tokens_input=_M, tokens_output=0)
        assert non_opus_cost == _ZERO_COST

    def test_opus_model_non_zero(self) -> None:
        """Claude opus model resolves to non-zero cost."""
        opus_cost = calculate_cost("claude-opus-4", tokens_input=_M, tokens_output=0)
        assert opus_cost > _ZERO_COST

    def test_zero_tokens_returns_zero_for_known_model(self) -> None:
        """Known model with 0 tokens has 0 cost."""
        cost = calculate_cost("claude-opus-4", tokens_input=0, tokens_output=0)
        assert cost == _ZERO_COST

    def test_case_insensitive_matching(self) -> None:
        """Model name matching is case-insensitive."""
        lower_cost = calculate_cost(
            "claude-sonnet-4-6", tokens_input=1000, tokens_output=1000
        )
        upper_cost = calculate_cost(
            "CLAUDE-SONNET-4-6", tokens_input=1000, tokens_output=1000
        )
        assert lower_cost == upper_cost
        assert lower_cost > _ZERO_COST


# ---------------------------------------------------------------------------
# Provider awareness — non-Anthropic models have no per-token cost
# ---------------------------------------------------------------------------


class TestProviderAwareness:
    """Non-Anthropic models (local Ollama / Ollama Cloud) cost 0.0 per token."""

    def test_ollama_prefixed_model_returns_zero(self) -> None:
        """Self-hosted Ollama models (``ollama/`` prefix) have no API cost."""
        cost = calculate_cost("ollama/llama3", tokens_input=_M, tokens_output=_M)
        assert cost == _ZERO_COST

    def test_ollama_cloud_model_returns_zero(self) -> None:
        """Ollama Cloud (``:cloud`` tag) is subscription-billed, not per token."""
        cost = calculate_cost("glm-5:cloud", tokens_input=_M, tokens_output=_M)
        assert cost == _ZERO_COST

    def test_bare_local_model_returns_zero(self) -> None:
        """A bare local embedding model has no per-token cost."""
        cost = calculate_cost("qwen3-embedding:0.6b", tokens_input=_M, tokens_output=0)
        assert cost == _ZERO_COST

    def test_is_anthropic_model_true_for_claude_names(self) -> None:
        for name in ("claude-opus-4-6", "claude-fable-5", "opus", "sonnet", "haiku"):
            assert _is_anthropic_model(name) is True, name

    def test_is_anthropic_model_false_for_non_claude_names(self) -> None:
        for name in ("ollama/llama3", "glm-5:cloud", "qwen3-embedding", "gpt-4o"):
            assert _is_anthropic_model(name) is False, name
