"""Tests for the per-model prompt-token calibration."""

from __future__ import annotations

import pytest

from navi.core.token_calibration import (
    DEFAULT_CHARS_PER_TOKEN,
    TokenCalibration,
    calibration_for,
    observe_prompt,
    reset_calibration,
)


@pytest.fixture(autouse=True)
def _clean_registry():
    reset_calibration()
    yield
    reset_calibration()


class TestDefault:
    def test_without_observations_uses_the_conservative_default(self):
        cal = TokenCalibration()
        # chars // 3 would say 1000; the default is deliberately higher because
        # chars // 3 was measured ~1.8x below the real prompt size on prod.
        assert cal.tokens_for(1800) == int(1800 / DEFAULT_CHARS_PER_TOKEN)
        assert cal.tokens_for(1800) > 1800 // 3

    def test_one_observation_is_not_enough_for_a_line(self):
        cal = TokenCalibration()
        cal.observe(100_000, 60_000)
        assert cal._line() is None
        assert cal.tokens_for(50_000) == int(50_000 / DEFAULT_CHARS_PER_TOKEN)


class TestLine:
    def test_two_observations_pin_overhead_and_slope(self):
        cal = TokenCalibration()
        # overhead 10k tokens, 2 chars per token
        cal.observe(100_000, 60_000)
        cal.observe(40_000, 30_000)
        intercept, slope = cal._line()
        assert intercept == pytest.approx(10_000, rel=0.01)
        assert slope == pytest.approx(0.5, rel=0.01)
        assert cal.tokens_for(200_000) == pytest.approx(110_000, rel=0.01)

    def test_predicts_a_smaller_kept_region_in_real_units(self):
        """The point of the line: a hypothetical post-compression context has to
        be priced in real tokens, overhead included. A pure ratio would call
        10k chars '5.6k tokens' where the real prompt is 15k."""
        cal = TokenCalibration()
        cal.observe(100_000, 60_000)
        cal.observe(40_000, 30_000)
        assert cal.tokens_for(10_000) == pytest.approx(15_000, rel=0.01)

    def test_delta_pricing_drops_the_fixed_overhead(self):
        """Appended content is priced at the slope: the 10k overhead is already
        inside the real baseline count the delta is added to."""
        cal = TokenCalibration()
        cal.observe(100_000, 60_000)
        cal.observe(40_000, 30_000)
        assert cal.delta_for(10_000) == pytest.approx(5_000, rel=0.01)
        assert cal.delta_for(10_000) < cal.tokens_for(10_000)

    def test_delta_without_a_line_uses_the_default_ratio(self):
        cal = TokenCalibration()
        assert cal.delta_for(1800) == int(1800 / DEFAULT_CHARS_PER_TOKEN)

    def test_observations_with_equal_size_do_not_fit(self):
        cal = TokenCalibration()
        cal.observe(50_000, 30_000)
        cal.observe(51_000, 30_500)  # 2% apart — slope would be noise
        assert cal._line() is None

    def test_an_implausible_slope_is_rejected(self):
        cal = TokenCalibration()
        # 10 tokens per char can only mean one prompt was truncated server-side.
        cal.observe(10_000, 100_000)
        cal.observe(20_000, 200_000)
        assert cal._line() is None
        assert cal.tokens_for(1800) == int(1800 / DEFAULT_CHARS_PER_TOKEN)

    def test_a_negative_intercept_is_clamped_to_zero(self):
        cal = TokenCalibration()
        cal.observe(100_000, 40_000)
        cal.observe(50_000, 20_500)
        intercept, _ = cal._line()
        assert intercept >= 0

    def test_only_the_last_observations_are_kept(self):
        cal = TokenCalibration()
        for i in range(20):
            cal.observe(10_000 + i * 10_000, 5_000 + i * 5_000)
        assert cal.observations == 8


class TestRegistry:
    def test_calibration_is_per_model(self):
        observe_prompt("glm", 100_000, 60_000)
        observe_prompt("glm", 40_000, 30_000)
        assert calibration_for("glm").observations == 2
        assert calibration_for("gemma").observations == 0

    def test_model_may_be_the_profile_priority_list(self):
        observe_prompt(["glm-5.3-flash:cloud", "gemma4:31b-cloud"], 1000, 500)
        assert calibration_for("GLM-5.3-FLASH:cloud").observations == 1

    def test_reset_drops_every_model(self):
        observe_prompt("glm", 1000, 500)
        reset_calibration()
        assert calibration_for("glm").observations == 0
