"""Volumes → tokens, with the three published rules that trip the naive math.

Every number below was computed by hand first, then pinned.
"""

from __future__ import annotations

import pytest
from genesyscalc.meters import LicenceModel, Tier
from genesyscalc.usage import (
    DeflectionMix,
    VoiceBotUsage,
    Volumes,
    apply_copilot_covers_summary,
    subscription_tokens,
    tier_interactions,
    virtual_agent_tokens,
    voice_bot_billable_minutes,
    voice_bot_roundup_uplift,
    voice_bot_tokens,
)

# ── 1 · the 15-second per-call round-up ──────────────────────────────────


def test_voice_bot_15_second_roundup() -> None:
    """1,000 calls at a 40s average.

    ceil(40/15) = 3 increments = 45s = 0.75 min/call → 750.0 min/month.
    The naive 40/60 × 1,000 = 666.67 min understates by 12.5%.
    """
    usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40)
    assert voice_bot_billable_minutes(usage) == pytest.approx(750.0)
    assert voice_bot_tokens(usage) == pytest.approx(750.0 / 17.0)
    assert voice_bot_tokens(usage) == pytest.approx(44.117647, rel=1e-6)


def test_roundup_uplift_is_reportable() -> None:
    """The stage-facing number: 12.5% more than the raw duration implies."""
    usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=40)
    assert voice_bot_roundup_uplift(usage) == pytest.approx(0.125)


def test_roundup_is_exact_on_increment_boundaries() -> None:
    """45s is exactly three increments — no inflation, uplift 0."""
    usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=45)
    assert voice_bot_billable_minutes(usage) == pytest.approx(750.0)
    assert voice_bot_roundup_uplift(usage) == pytest.approx(0.0)


def test_roundup_penalises_short_calls_hardest() -> None:
    """A 5-second bot greeting bills as 15 seconds — 3× its duration.

    1,000 calls × 5s = 83.33 raw minutes, billed as 250.0.
    """
    usage = VoiceBotUsage(calls_per_month=1_000, avg_bot_seconds_per_call=5)
    assert voice_bot_billable_minutes(usage) == pytest.approx(250.0)
    assert voice_bot_roundup_uplift(usage) == pytest.approx(2.0)


def test_zero_bot_volume_is_free_and_does_not_divide_by_zero() -> None:
    usage = VoiceBotUsage(calls_per_month=0, avg_bot_seconds_per_call=0)
    assert voice_bot_billable_minutes(usage) == 0.0
    assert voice_bot_roundup_uplift(usage) == 0.0
    assert voice_bot_tokens(usage) == 0.0


def test_negative_bot_usage_rejected() -> None:
    with pytest.raises(ValueError, match="calls_per_month"):
        VoiceBotUsage(calls_per_month=-1, avg_bot_seconds_per_call=30)


# ── 2 · the highest-tier rule as a partition ─────────────────────────────


def test_tier_shares_must_partition() -> None:
    """Shares summing past 1.0 would bill some interaction twice."""
    with pytest.raises(ValueError, match="sum to at most 1.0"):
        DeflectionMix(
            bot_only_share=0.5, virtual_agent_share=0.4, agentic_va_share=0.3
        )


def test_tier_share_bounds() -> None:
    with pytest.raises(ValueError, match="must be in"):
        DeflectionMix(virtual_agent_share=1.4)


def test_agent_handled_share_is_the_remainder() -> None:
    mix = DeflectionMix(
        bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05
    )
    assert mix.total_deflected_share == pytest.approx(0.50)
    assert mix.agent_handled_share == pytest.approx(0.50)


def test_highest_tier_charges_each_interaction_once() -> None:
    """100,000 interactions: 30% bot, 15% VA, 5% agentic VA.

    VA  = 100,000 × 0.15 × 0.5 tokens = 7,500
    AVA = 100,000 × 0.05 × 1.2 tokens = 6,000
                                        ------
                                        13,500

    Applying every tier to the whole volume — the additive reading — would
    give 50,000 + 120,000 = far more, billing the same interaction at
    several tiers at once.
    """
    mix = DeflectionMix(
        bot_only_share=0.30, virtual_agent_share=0.15, agentic_va_share=0.05
    )
    tokens = virtual_agent_tokens(100_000, mix)
    assert tokens["virtual_agent"] == pytest.approx(7_500.0)
    assert tokens["agentic_virtual_agent"] == pytest.approx(6_000.0)
    assert sum(tokens.values()) == pytest.approx(13_500.0)


def test_tier_interactions_never_exceed_the_volume() -> None:
    mix = DeflectionMix(
        bot_only_share=0.40, virtual_agent_share=0.35, agentic_va_share=0.25
    )
    split = tier_interactions(100_000, mix)
    assert sum(split.values()) == pytest.approx(100_000.0)
    assert split[Tier.BOT] == pytest.approx(40_000.0)


def test_bot_tier_is_not_priced_per_interaction() -> None:
    """Voice bots bill in minutes with a round-up, so they are absent here."""
    mix = DeflectionMix(bot_only_share=1.0)
    assert virtual_agent_tokens(100_000, mix) == {
        "virtual_agent": 0.0,
        "agentic_virtual_agent": 0.0,
    }


# ── 3 · Copilot covers Supervisor summaries ──────────────────────────────


def test_summary_billed_when_copilot_off() -> None:
    usage = apply_copilot_covers_summary(
        {"ai_summary_and_insights": 120_000.0}, copilot_enabled=False
    )
    assert usage.get("ai_summary_and_insights") == pytest.approx(120_000.0)
    assert usage.notes == ()


def test_copilot_covers_summary_zeroes_the_line() -> None:
    """The published exclusion, enforced — and reported, not silent."""
    usage = apply_copilot_covers_summary(
        {"ai_summary_and_insights": 120_000.0}, copilot_enabled=True
    )
    assert usage.get("ai_summary_and_insights") == 0.0
    assert len(usage.notes) == 1
    assert "Agent Copilot is enabled" in usage.notes[0]


def test_copilot_exclusion_leaves_other_meters_alone() -> None:
    usage = apply_copilot_covers_summary(
        {"ai_summary_and_insights": 100.0, "ai_scoring": 500.0}, copilot_enabled=True
    )
    assert usage.get("ai_scoring") == pytest.approx(500.0)


def test_copilot_exclusion_is_quiet_when_there_is_nothing_to_zero() -> None:
    usage = apply_copilot_covers_summary(
        {"ai_summary_and_insights": 0.0}, copilot_enabled=True
    )
    assert usage.notes == ()


# ── subscription tokens ──────────────────────────────────────────────────


def test_per_user_tokens_are_exact_and_licence_sensitive() -> None:
    """500 users × 40 = 20,000 named; × 60 = 30,000 concurrent."""
    keys = frozenset({"agent_copilot_named"})
    assert subscription_tokens(500, keys, LicenceModel.NAMED) == {
        "agent_copilot_named": 20_000.0
    }
    assert subscription_tokens(500, keys, LicenceModel.CONCURRENT) == {
        "agent_copilot_named": 30_000.0
    }


def test_subscription_rejects_a_consumption_meter() -> None:
    with pytest.raises(ValueError, match="not a per-user meter"):
        subscription_tokens(10, frozenset({"ai_scoring"}), LicenceModel.NAMED)


# ── volumes ──────────────────────────────────────────────────────────────


def test_volume_totals() -> None:
    v = Volumes(
        voice_inbound_monthly=100_000,
        voice_outbound_monthly=20_000,
        digital_sessions_monthly=8_000,
        email_monthly=12_000,
    )
    assert v.voice_total_monthly == pytest.approx(120_000.0)
    assert v.all_interactions_monthly == pytest.approx(140_000.0)


def test_negative_volume_rejected() -> None:
    with pytest.raises(ValueError, match="voice_inbound_monthly"):
        Volumes(voice_inbound_monthly=-1)
