"""Tests for the SavingsTracker (real-time cost savings vs cloud APIs).

Validates:
- Per-request savings calculation
- Cloud pricing tiers
- Model-to-tier mapping
- Cumulative tracking
- Local electricity cost estimation
- Edge cases (zero tokens, unknown models)
"""

from __future__ import annotations

import pytest

from llmhosts.discovery.savings import SavingsTracker, _cloud_cost, _local_cost, _model_to_tier

# ---------------------------------------------------------------------------
# Model-to-tier mapping
# ---------------------------------------------------------------------------


@pytest.mark.unit
@pytest.mark.parametrize(
    "model_name,expected_tier",
    [
        ("qwen2.5:1.5b", "tiny"),
        ("llama3.2:3b", "small"),
        ("llama3.2:8b", "medium"),
        ("mistral:7b", "medium"),
        ("codellama:13b", "large"),
        ("mixtral-8x7b", "large"),
        ("qwen2.5:32b", "xl"),
        ("llama-70b-q4", "xl"),
        ("unknown-model", "medium"),  # default
    ],
)
def test_model_to_tier(model_name: str, expected_tier: str) -> None:
    """Models should map to correct cloud pricing tiers."""
    assert _model_to_tier(model_name) == expected_tier


@pytest.mark.unit
def test_model_to_tier_case_insensitive() -> None:
    """Tier mapping should be case-insensitive."""
    assert _model_to_tier("Llama3.2:8B") == "medium"
    assert _model_to_tier("QWEN2.5:1.5B") == "tiny"


# ---------------------------------------------------------------------------
# Cloud cost calculation
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_cloud_cost_medium_tier() -> None:
    """Medium tier: $1.00/M input, $3.00/M output."""
    cost = _cloud_cost("medium", input_tokens=1000, output_tokens=500)
    expected = (1000 * 1.00 + 500 * 3.00) / 1_000_000
    assert abs(cost - expected) < 1e-10


@pytest.mark.unit
def test_cloud_cost_xl_tier() -> None:
    """XL tier: $5.00/M input, $15.00/M output."""
    cost = _cloud_cost("xl", input_tokens=500, output_tokens=200)
    expected = (500 * 5.00 + 200 * 15.00) / 1_000_000
    assert abs(cost - expected) < 1e-10


@pytest.mark.unit
def test_cloud_cost_unknown_tier_defaults_to_medium() -> None:
    """Unknown tier should fall back to medium pricing."""
    cost = _cloud_cost("nonexistent", input_tokens=1000, output_tokens=500)
    medium_cost = _cloud_cost("medium", input_tokens=1000, output_tokens=500)
    assert cost == medium_cost


@pytest.mark.unit
def test_cloud_cost_zero_tokens() -> None:
    """Zero tokens should return zero cost."""
    assert _cloud_cost("medium", 0, 0) == 0.0


# ---------------------------------------------------------------------------
# Local cost estimation
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_local_cost_positive() -> None:
    """Local cost should be small but positive for non-zero tokens."""
    cost = _local_cost(1000)
    assert cost > 0
    assert cost < 0.01  # Should be very cheap


@pytest.mark.unit
def test_local_cost_zero_tokens() -> None:
    """Zero tokens should return zero local cost."""
    assert _local_cost(0) == 0.0
    assert _local_cost(-10) == 0.0


@pytest.mark.unit
def test_local_cost_scales_with_tokens() -> None:
    """More tokens = proportionally higher local cost."""
    cost_1k = _local_cost(1000)
    cost_10k = _local_cost(10000)
    assert abs(cost_10k / cost_1k - 10.0) < 1e-6


# ---------------------------------------------------------------------------
# SavingsTracker
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_tracker_initial_state() -> None:
    """Fresh tracker should have all zeros."""
    tracker = SavingsTracker()
    assert tracker.total_requests == 0
    assert tracker.total_tokens == 0
    assert tracker.total_savings == 0.0
    assert tracker.savings_percent == 0.0


@pytest.mark.unit
def test_tracker_record_request() -> None:
    """Recording a request should update all counters."""
    tracker = SavingsTracker()
    result = tracker.record_request("llama3.2:8b", input_tokens=500, output_tokens=200)

    assert tracker.total_requests == 1
    assert tracker.total_input_tokens == 500
    assert tracker.total_output_tokens == 200
    assert tracker.total_tokens == 700
    assert result["cloud_cost"] > 0
    assert result["local_cost"] >= 0
    assert result["savings"] >= 0
    assert 0 <= result["savings_percent"] <= 100


@pytest.mark.unit
def test_tracker_cumulative_savings() -> None:
    """Multiple requests should accumulate savings."""
    tracker = SavingsTracker()
    for _ in range(10):
        tracker.record_request("llama3.2:8b", input_tokens=1000, output_tokens=500)

    assert tracker.total_requests == 10
    assert tracker.total_input_tokens == 10000
    assert tracker.total_output_tokens == 5000
    assert tracker.total_savings > 0


@pytest.mark.unit
def test_tracker_savings_always_non_negative() -> None:
    """Savings should never go negative."""
    tracker = SavingsTracker()
    tracker.record_request("qwen2.5:1.5b", input_tokens=1, output_tokens=1)

    assert tracker.total_savings >= 0
    assert tracker.savings_percent >= 0


@pytest.mark.unit
def test_tracker_savings_percent_capped() -> None:
    """Savings percent should be between 0 and 100."""
    tracker = SavingsTracker()
    for _ in range(100):
        tracker.record_request("qwen2.5:32b", input_tokens=5000, output_tokens=2000)

    assert 0 <= tracker.savings_percent <= 100


@pytest.mark.unit
def test_tracker_summary_format() -> None:
    """Summary should return all expected keys with correct types."""
    tracker = SavingsTracker()
    tracker.record_request("llama3.2:8b", input_tokens=500, output_tokens=200)

    summary = tracker.summary()

    assert isinstance(summary["requests"], int)
    assert isinstance(summary["tokens_processed"], int)
    assert summary["cloud_equivalent"].startswith("$")
    assert summary["actual_cost"].startswith("$")
    assert summary["savings"].startswith("$")
    assert summary["savings_percent"].endswith("%")
    assert isinstance(summary["session_hours"], float)


@pytest.mark.unit
def test_tracker_format_display() -> None:
    """format_display should return a human-readable string."""
    tracker = SavingsTracker()
    tracker.record_request("llama3.2:8b", input_tokens=500, output_tokens=200)

    display = tracker.format_display()

    assert "Tokens:" in display
    assert "Cloud:" in display
    assert "Saved:" in display


@pytest.mark.unit
def test_tracker_xl_model_higher_savings() -> None:
    """Larger models should show higher cloud savings per request."""
    tracker_small = SavingsTracker()
    tracker_large = SavingsTracker()

    result_small = tracker_small.record_request("qwen2.5:1.5b", input_tokens=1000, output_tokens=500)
    result_large = tracker_large.record_request("llama-70b", input_tokens=1000, output_tokens=500)

    assert result_large["cloud_cost"] > result_small["cloud_cost"]
    assert result_large["savings"] > result_small["savings"]


@pytest.mark.unit
def test_tracker_session_hours() -> None:
    """Session hours should be near zero for a fresh tracker."""
    tracker = SavingsTracker()
    assert tracker.session_hours < 0.01  # Less than 36 seconds


@pytest.mark.unit
def test_record_request_returns_per_request_info() -> None:
    """record_request should return a dict with per-request cost breakdown."""
    tracker = SavingsTracker()
    result = tracker.record_request("llama3.2:8b", input_tokens=500, output_tokens=200)

    assert "cloud_cost" in result
    assert "local_cost" in result
    assert "savings" in result
    assert "savings_percent" in result
    assert result["savings"] == round(result["cloud_cost"] - result["local_cost"], 6) or result["savings"] >= 0


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


@pytest.mark.unit
def test_tracker_zero_tokens_request() -> None:
    """Recording a request with zero tokens should not crash."""
    tracker = SavingsTracker()
    result = tracker.record_request("llama3.2:8b", input_tokens=0, output_tokens=0)

    assert result["cloud_cost"] == 0.0
    assert result["local_cost"] == 0.0
    assert result["savings"] == 0.0


@pytest.mark.unit
def test_tracker_unknown_model_uses_medium_tier() -> None:
    """Unknown models should default to medium tier pricing."""
    tracker = SavingsTracker()
    result_unknown = tracker.record_request("my-custom-model", input_tokens=1000, output_tokens=500)

    tracker2 = SavingsTracker()
    result_medium = tracker2.record_request("some-7b-model", input_tokens=1000, output_tokens=500)

    # Both should use medium tier
    assert result_unknown["cloud_cost"] == result_medium["cloud_cost"]


# ---------------------------------------------------------------------------
# Substring collision tests (bug #6 from scenario analysis)
# ---------------------------------------------------------------------------


@pytest.mark.unit
@pytest.mark.parametrize(
    "model_name,expected_tier",
    [
        ("mixtral-8x22b", "xl"),  # "22b" not "2b"
        ("gemma-2-27b", "xl"),  # "27b" not "7b"
        ("model-12b", "large"),  # "12b" not "2b"
        ("qwen2.5:14b", "large"),  # "14b" not "4b"
        ("llama-2-7b", "medium"),  # "7b" — the "2" in "llama-2" should not match "2b"
        ("phi-2", "medium"),  # No "b" suffix — defaults to medium
        ("qwen2.5:2b", "tiny"),  # "2b" standalone
        ("gemma-2b-it", "tiny"),  # "2b" with suffix
    ],
)
def test_model_to_tier_no_substring_collision(model_name: str, expected_tier: str) -> None:
    """Tier matching must not collide across different parameter sizes."""
    assert _model_to_tier(model_name) == expected_tier


@pytest.mark.unit
def test_cloud_cost_negative_tokens_clamped() -> None:
    """Negative tokens should be clamped to zero, not produce negative costs."""
    assert _cloud_cost("medium", -100, -50) == 0.0
    assert _cloud_cost("xl", -1, 100) >= 0.0
