"""The vendor-change tripwire.

Every published row is pinned by its exact feature wording AND its exact
rate string, so a transcription slip or a vendor republication breaks the
build rather than quietly moving a client-facing number. When Genesys
republishes, these pins are updated in the SAME commit as the anchor — see
the re-anchoring protocol in docs/Calculator_Pattern_V1-00.md.
"""

from __future__ import annotations

import pytest
from genesyscalc.meters import Confidence, LicenceModel, MeterBasis
from genesyscalc.ratecard import (
    ALLOWANCE_CARRIES_OVER,
    COPILOT_COVERS_SUMMARY_RULE,
    FREE_TOKENS_PER_MONTH,
    HIGHEST_TIER_RULE,
    LIST_PRICE_BY_CURRENCY,
    METERS,
    METERS_VERBATIM,
    RATE_CARD_SOURCE,
    RATE_CARD_SOURCE_DATE,
    VOICE_BOT_ROUNDUP_SECONDS,
    ZERO_RATED_VERBATIM,
    meter,
    rate_card_rows,
)

# The published table, as it reads on the page. Transcribed 2026-07-12.
PUBLISHED = {
    "Bots (Voice)": "17 minutes per token",
    "Bots (Digital)": "51 sessions per token",
    "Virtual Agent": "0.5 tokens per Virtual Agent interaction",
    "Agentic Virtual Agent": "1.2 tokens per interaction",
    "Agent Copilot [named]": "40 tokens per user",
    "Agent Copilot [concurrent]": "60 tokens per user",
    "AI Scoring": "20 evaluations per token",
    "AI Translate": "2 translations per token",
    "AI Summary and Insights": "50 summaries/insights per token",
    "Apple Messages for Business": "400 inbound or outbound messages per token",
    "Facebook Messenger": "400 messages per token",
    "Instagram Direct Messaging": "400 messages per token",
    "WhatsApp Messaging": "400 messages per token",
    "X Direct Messaging": "400 messages per token",
    "Genesys Cloud Social": "400 social post ingestions per channel per token",
    "Social Post Responses": "400 outbound messages per channel per token",
    "Predictive Routing": "17 routes per token",
    "Speech and Text Analytics [named]": "30 tokens per user",
    "Speech and Text Analytics [concurrent]": "45 tokens per user",
    "Genesys Cloud Copilot": "20 AI actions per token",
}


def test_published_meter_table_verbatim() -> None:
    assert {m.feature: m.published_rate for m in METERS_VERBATIM} == PUBLISHED


def test_meter_count() -> None:
    assert len(METERS_VERBATIM) == 20
    assert len(ZERO_RATED_VERBATIM) == 2


def test_source_pinned() -> None:
    """Bumping the date without updating the pins is the failure mode."""
    assert RATE_CARD_SOURCE_DATE == "2026-07-12"
    assert RATE_CARD_SOURCE == (
        "https://help.genesys.cloud/articles/genesys-cloud-tokens-model/"
    )


def test_every_confirmed_meter_carries_source_url_and_date() -> None:
    for m in METERS.values():
        if m.confidence is Confidence.CONFIRMED:
            assert m.source_url, f"{m.key} is 🟢 without a source URL"
            assert m.source_date, f"{m.key} is 🟢 without a source date"


def test_unsourced_confirmed_meter_cannot_be_constructed() -> None:
    from genesyscalc.meters import Meter

    with pytest.raises(ValueError, match="source URL and date"):
        Meter(
            key="made_up",
            feature="Made Up",
            published_rate="1 per token",
            basis=MeterBasis.UNITS_PER_TOKEN,
            rate=1.0,
        )


@pytest.mark.parametrize(
    ("key", "expected"),
    [
        ("bots_voice", 1 / 17),
        ("bots_digital", 1 / 51),
        ("virtual_agent", 0.5),
        ("agentic_virtual_agent", 1.2),
        ("ai_scoring", 1 / 20),
        ("ai_translate", 1 / 2),
        ("ai_summary_and_insights", 1 / 50),
        ("whatsapp_messaging", 1 / 400),
        ("genesys_cloud_social", 1 / 400),
        ("predictive_routing", 1 / 17),
        ("genesys_cloud_copilot", 1 / 20),
    ],
)
def test_numeric_rates_match_their_published_strings(key: str, expected: float) -> None:
    """Catches a float drifting away from the wording beside it."""
    assert meter(key).rate == pytest.approx(expected)


def test_per_user_rates() -> None:
    copilot = meter("agent_copilot_named")
    assert copilot.tokens_per_user_month(LicenceModel.NAMED) == 40.0
    assert copilot.tokens_per_user_month(LicenceModel.CONCURRENT) == 60.0

    sta = meter("speech_and_text_analytics_named")
    assert sta.tokens_per_user_month(LicenceModel.NAMED) == 30.0
    assert sta.tokens_per_user_month(LicenceModel.CONCURRENT) == 45.0


def test_free_allowance() -> None:
    assert FREE_TOKENS_PER_MONTH[LicenceModel.NAMED] == 250
    assert FREE_TOKENS_PER_MONTH[LicenceModel.CONCURRENT] == 350
    assert ALLOWANCE_CARRIES_OVER is False


def test_voice_roundup_granularity() -> None:
    assert VOICE_BOT_ROUNDUP_SECONDS == 15


def test_zero_rated_features() -> None:
    assert {m.key for m in ZERO_RATED_VERBATIM} == {
        "predictive_engagement",
        "knowledge_queries",
    }
    assert all(m.rate == 0.0 for m in ZERO_RATED_VERBATIM)
    assert all(m.tokens_for(1_000_000) == 0.0 for m in ZERO_RATED_VERBATIM)


def test_highest_tier_rule_text_verbatim() -> None:
    """It renders on stage, so the wording is part of the deliverable."""
    assert HIGHEST_TIER_RULE == (
        "In cases where an interaction uses multiple AI resources, such as bot "
        "flows, virtual agents, and agentic virtual agents, Genesys bases "
        "charges on the highest tier (price) resource that Genesys Cloud uses "
        "during the interaction."
    )


def test_copilot_covers_summary_rule_text_verbatim() -> None:
    assert COPILOT_COVERS_SUMMARY_RULE == (
        "if you enable Agent Copilot simultaneously, then Supervisor Copilot "
        "summaries and insights do not consume tokens"
    )


def test_list_price() -> None:
    assert LIST_PRICE_BY_CURRENCY["USD"] == 1.00
    assert LIST_PRICE_BY_CURRENCY["JPY"] == 120.0


def test_meter_lookup_error_is_useful() -> None:
    with pytest.raises(KeyError, match="not a published Genesys meter"):
        meter("nope")


def test_rate_card_rows_cover_every_published_line() -> None:
    rows = rate_card_rows()
    assert len(rows) == len(METERS_VERBATIM) + len(ZERO_RATED_VERBATIM)
    assert rows[0]["Feature"] == "Bots (Voice)"
    assert all(r["Confidence"] == "🟢" for r in rows)
    assert all(r["Source"] == RATE_CARD_SOURCE_DATE for r in rows)
