"""The producer's cache in front of the race office's published series table.

Everything here is about what the standings card shows when something is wrong. A table that goes
blank on a bad fetch is worse than a stale one: an empty standings card is a statement about the
championship ("nobody has sailed"), while a table with its age on it is a statement about our
reading of it, which is the true one. No network — the reader is injected.
"""
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent.parent / "live"))
from live_feed_server import PublishedStandings, standings_binding_from_env  # noqa: E402

BINDING = {"rankingUrl": "https://race.example/ranking/", "displayName": "kSail"}


def _table(won=17):
    return {"stageId": None, "stageName": "Ranking Round Robin", "format": "team",
            "races": None, "scored": None, "complete": None, "note": "calculated for Round 2",
            "standings": [{"rank": 1, "teamId": None, "team": "DEF", "sailed": 21, "won": won,
                           "lost": 21 - won, "percent": 100.0 * won / 21}]}


class _Clock:
    def __init__(self) -> None:
        self.t = 1_000_000.0

    def __call__(self) -> float:
        return self.t


class _Reader:
    """Stands in for `ksail_public.read_ranking`. Counts calls, and can be told to fail."""

    def __init__(self) -> None:
        self.calls = 0
        self.fail = False
        self.won = 17
        self.labels_seen = []

    def __call__(self, url, *, team_labels=None, team_full_names=None):  # noqa: ANN001
        self.calls += 1
        self.labels_seen.append((team_labels, team_full_names))
        if self.fail:
            raise TimeoutError("the race office's site did not answer")
        return _table(self.won)


def _standings(reader, clock, **kw):
    return PublishedStandings(BINDING, read=reader, now=clock, **kw)


def test_a_producer_with_nothing_bound_says_so_instead_of_serving_an_empty_table():
    status, body = PublishedStandings({}).snapshot()
    assert status == 404
    assert "no standings source" in body["error"]
    assert "standings" not in body, "a configuration fault must not read as a fleet that never sailed"


def test_the_first_read_is_served_with_its_source_and_its_age():
    reader, clock = _Reader(), _Clock()
    status, body = _standings(reader, clock).snapshot()
    assert status == 200
    assert body["standings"][0]["team"] == "DEF"
    assert body["sourceName"] == "kSail", "the card attributes the table to whoever published it"
    assert body["ageS"] == 0.0
    assert body["fetchedUtcMs"] == int(clock.t * 1000)
    assert "staleReason" not in body


def test_the_binding_carries_both_name_maps_through_to_the_reader():
    """Which entry is called what, and how it is spelled out, are the event's — so both must reach
    the driver from the binding rather than from anything in the producer."""
    reader, clock = _Reader(), _Clock()
    PublishedStandings({**BINDING, "teamLabels": {"USA 1": "DEF"},
                        "teamFullNames": {"DEF": "Defending Champions"}},
                       read=reader, now=clock).snapshot()
    assert reader.labels_seen == [({"USA 1": "DEF"}, {"DEF": "Defending Champions"})]


def test_the_race_offices_site_is_asked_once_per_ttl_and_not_once_per_renderer():
    reader, clock = _Reader(), _Clock()
    cache = _standings(reader, clock, ttl_s=180.0)
    for _ in range(10):
        cache.snapshot()
    assert reader.calls == 1, "OBS, a partner's box and an operator's browser are one request"

    clock.t += 181.0
    cache.snapshot()
    assert reader.calls == 2


def test_the_age_is_the_age_of_the_reading_not_of_the_request():
    reader, clock = _Reader(), _Clock()
    cache = _standings(reader, clock, ttl_s=180.0)
    cache.snapshot()
    clock.t += 45.0
    _, body = cache.snapshot()
    assert body["ageS"] == 45.0 and reader.calls == 1


def test_a_failed_refresh_keeps_the_last_good_table_and_states_why():
    """The one that matters on air. An hour-old table is still the standings."""
    reader, clock = _Reader(), _Clock()
    cache = _standings(reader, clock, ttl_s=180.0)
    cache.snapshot()

    reader.fail = True
    clock.t += 3600.0
    status, body = cache.snapshot()
    assert status == 200
    assert body["standings"][0]["team"] == "DEF", "not blank, not zeroed"
    assert body["ageS"] == 3600.0, "and the card can say how old it is"
    assert "did not answer" in body["staleReason"]


def test_a_recovered_source_drops_the_stale_note_and_the_new_numbers_win():
    reader, clock = _Reader(), _Clock()
    cache = _standings(reader, clock, ttl_s=180.0)
    cache.snapshot()
    reader.fail = True
    clock.t += 200.0
    cache.snapshot()

    reader.fail = False
    reader.won = 18
    clock.t += 200.0
    _, body = cache.snapshot()
    assert body["standings"][0]["won"] == 18
    assert "staleReason" not in body and body["ageS"] == 0.0


def test_a_producer_that_never_managed_a_read_answers_an_error_rather_than_inventing_one():
    reader, clock = _Reader(), _Clock()
    reader.fail = True
    status, body = _standings(reader, clock).snapshot()
    assert status == 502
    assert "could not be read" in body["error"]
    assert "standings" not in body


def test_the_producer_adds_what_the_race_office_cannot_know():
    """The event's name and the venue's zone come from the header this producer already publishes —
    which is what lets the card draw a venue-local stamp while holding no feed connection."""
    reader, clock = _Reader(), _Clock()
    _, body = _standings(reader, clock).snapshot(
        {"eventName": "Team Racing World Championship 2026", "venueTimeZone": "Europe/Stockholm"})
    assert body["venueTimeZone"] == "Europe/Stockholm"
    assert body["eventName"].endswith("2026")


def test_an_empty_context_key_never_overwrites_the_tables_own_fields():
    reader, clock = _Reader(), _Clock()
    _, body = _standings(reader, clock).snapshot({"eventName": None, "venueTimeZone": ""})
    assert "venueTimeZone" not in body and "eventName" not in body


# --- the local override ---------------------------------------------------------------------------

def test_the_environment_override_is_shaped_like_the_events_own_binding():
    binding = standings_binding_from_env({
        "VR_STANDINGS_URL": "https://race.example/r/",
        "VR_STANDINGS_SOURCE": "Burgee",
        "VR_STANDINGS_TEAM_LABELS": '{"USA 1": "DEF"}'})
    assert binding == {"rankingUrl": "https://race.example/r/", "displayName": "Burgee",
                       "teamLabels": {"USA 1": "DEF"}}


def test_no_url_means_no_binding_at_all():
    assert standings_binding_from_env({"VR_STANDINGS_SOURCE": "Burgee"}) == {}


def test_unreadable_labels_do_not_take_the_whole_binding_down_with_them():
    """A mistyped JSON override loses the relabelling, not the standings — and the card then shows
    the race office's own names, which is the honest degradation."""
    binding = standings_binding_from_env({"VR_STANDINGS_URL": "https://race.example/r/",
                                          "VR_STANDINGS_TEAM_LABELS": "{not json"})
    assert binding["rankingUrl"] == "https://race.example/r/"
    assert "teamLabels" not in binding


@pytest.mark.parametrize("value", ['"USA 1"', "[1, 2]", "null"])
def test_labels_that_are_not_a_map_are_ignored(value):
    binding = standings_binding_from_env({"VR_STANDINGS_URL": "https://race.example/r/",
                                          "VR_STANDINGS_TEAM_LABELS": value})
    assert "teamLabels" not in binding
