"""The finish call states the RACE OFFICE's result, and reads out the penalties it applied.

Two accounts of how a race ended reach the engine and they are not the same kind of claim. The
scorer's is derived from the tracks and is honest only as a preliminary — a tracker cannot see a
protest, a redress, or a penalty applied ashore after the boats are in. The race office's is the
decision, and on a REPLAY it was reached days before anyone watches the film.

So: with no official record in the feed, nothing changes — "the official result rests with the race
committee" is the truthful thing to say while the committee has not spoken. With one, the call states
that record with its score, and any penalty is read out after it, because a penalty changes the
series without changing the race and NOTHING else on the broadcast can show it.

Measured on TRWC 2026, which is why the disagreement case below is not hypothetical: the race
office's winner differs from the scorer's in 8 of 148 races.
"""
import math

import pytest
from conftest import make_course, make_feed
from official_record import as_number, official_result, scoreline
from score_race import enrich


def _en(event) -> str:
    text = event.get("i18n") or event.get("text")
    return (text.get("en", "") if isinstance(text, dict) else (text or ""))


def _finish_feed(official=None):
    """A race Denmark finishes and the USA does not — the scorer's winner is `den`."""
    course = make_course([(0.0, 100.0, "M1"), (60.0, 100.0, "M2")], finish_n=0.0)
    teams = [("den", "Denmark", [("D", "2")]), ("usa", "USA", [("U", "5")])]
    track = [(0.0, 0.0), (0.0, 100.0), (60.0, 100.0), (0.0, 0.0), (-6.0, -10.0)]
    walk, pos, i = [track[0]], track[0], 1
    while i < len(track):
        target = track[i]
        step = math.dist(pos, target)
        if step <= 4.0:
            pos, i = target, i + 1
        else:
            pos = (pos[0] + (target[0] - pos[0]) / step * 4.0,
                   pos[1] + (target[1] - pos[1]) / step * 4.0)
        walk.append(pos)
    frames = {s: {"D": (0.0, -3.0, 0), "U": (120.0, -3.0, 0)} for s in range(0, 6)}
    for k, (east, north) in enumerate(walk):
        frames[6 + k] = {"D": (east, north, 0), "U": (120.0, -3.0, 0)}
    feed = make_feed(course, teams, frames, wind=None, gun_sec=5, end_sec=6 + len(walk))
    if official is not None:
        feed["officialResult"] = official
    return feed


def _winner_calls(out):
    return [e for e in out["events"] if e["type"] == "winner"]


DEN_WINS = {
    "winnerTeamId": "den",
    "source": {"provider": "standings", "displayName": "kSail"},
    "teams": [{"teamId": "den", "team": "Denmark", "result": "win", "points": 3, "places": [1, 2]},
              {"teamId": "usa", "team": "USA", "result": "loss", "points": 7, "places": [3, 4]}],
}


# --- with no official record, nothing changes ----------------------------------------------------

def test_without_an_official_record_the_call_stays_preliminary():
    """The pre-existing behaviour, unchanged and deliberately so: a race the committee has not
    settled must not be presented as settled."""
    out = enrich(_finish_feed())
    calls = _winner_calls(out)
    assert calls and any(e.get("preliminary") for e in calls)
    assert "prelimin" in " ".join(_en(e) for e in calls).lower()
    assert (out["result"] or {}).get("preliminary") is True


# --- with one, the office's result is the result -------------------------------------------------

def test_the_official_result_replaces_the_preliminary_call():
    out = enrich(_finish_feed(DEN_WINS))
    calls = _winner_calls(out)
    spoken = " ".join(_en(e) for e in calls).lower()
    assert "official result" in spoken
    assert "prelimin" not in spoken, "a decided race is not announced as undecided"
    assert all(e.get("preliminary") is not True for e in calls)


def test_the_score_is_stated_because_a_team_race_is_decided_by_one():
    """1-4 against 2-3 is a two-point win that a finishing order does not explain on its own."""
    spoken = " ".join(_en(e) for e in _winner_calls(enrich(_finish_feed(DEN_WINS))))
    assert "3 points to 7" in spoken


def test_the_result_is_no_longer_flagged_preliminary():
    result = enrich(_finish_feed(DEN_WINS))["result"]
    assert result["preliminary"] is False
    assert result["winner"] == "den"
    assert result["official"]["points"] == {"den": 3.0, "usa": 7.0}


def test_a_margin_in_metres_is_not_attributed_to_the_race_office():
    """The margin is OUR measurement and the sentence is THEIR record. Mixing them would put our
    number behind their authority."""
    spoken = " ".join(_en(e) for e in _winner_calls(enrich(_finish_feed(DEN_WINS))))
    assert "metre" not in spoken.lower()


# --- when the two disagree, the office wins and the tracking is kept ------------------------------

def test_the_office_overrules_the_scorer_and_the_scorer_is_not_thrown_away():
    """8 races in 148 at TRWC 2026. The office's winner is the result; the tracked one survives as
    `trackedWinner`, because two accounts disagreeing is a fact about the tracking that somebody has
    to be able to see."""
    out = enrich(_finish_feed({**DEN_WINS, "winnerTeamId": "usa"}))
    result = out["result"]
    assert result["winner"] == "usa"          # the race office decided this
    assert result["trackedWinner"] == "den"   # …and the tracker read it the other way
    # Spelled out by the localizer, per the "a code is not a name to a viewer joining mid-programme"
    # rule — the call names the office's winner and not the scorer's, whichever spelling it uses.
    spoken = " ".join(_en(e) for e in _winner_calls(out))
    assert "United States" in spoken and "Denmark" not in spoken


def test_a_winner_the_picture_does_not_carry_is_refused():
    """TRWC's two American entries are bound to swapped team ids across its own store. A record
    naming a team this race does not have would draw a winner with no flag, no name and no boats —
    so the whole record is refused and the honest preliminary call stands."""
    out = enrich(_finish_feed({**DEN_WINS, "winnerTeamId": "racemgmt-team-99"}))
    assert (out["result"] or {}).get("preliminary") is True
    assert "prelimin" in " ".join(_en(e) for e in _winner_calls(out)).lower()


# --- the penalties, which nothing else on the broadcast can show ----------------------------------

def test_a_penalty_is_read_out_after_the_result():
    out = enrich(_finish_feed({
        **DEN_WINS,
        "penalties": [{"teamId": "usa", "team": "USA", "wins": 0.5,
                       "headline": "USA penalised 0.50 wins",
                       "reason": "Boat 9 deducted 0.5 race win for Level B damage"}]}))
    calls = _winner_calls(out)
    penalty = [e for e in calls if e.get("penalty")]
    assert len(penalty) == 1
    said = _en(penalty[0])
    assert "penalised" in said and "Level B damage" in said
    assert said != _en(calls[0]), "the penalty is its own beat, not appended to the result"


def test_the_office_s_own_sentence_is_quoted_and_not_paraphrased():
    """A jury's finding is not ours to reword — and it is the only description of the decision that
    exists anywhere."""
    reason = "Breach of SI12.2 - report to the Bosun for damage inspection"
    out = enrich(_finish_feed({**DEN_WINS,
                               "penalties": [{"teamId": "usa", "reason": reason}]}))
    assert reason in " ".join(_en(e) for e in _winner_calls(out))


def test_two_penalties_are_two_beats_and_never_overlap():
    """A one-second cadence can put several facts at one instant; a voice cannot."""
    out = enrich(_finish_feed({
        **DEN_WINS,
        "penalties": [{"teamId": "den", "reason": "one race win penalty for Level C damage"},
                      {"teamId": "usa", "reason": "half a race win for Level B damage"}]}))
    beats = [e for e in _winner_calls(out) if e.get("penalty")]
    assert len(beats) == 2
    assert len({e["tSec"] for e in beats}) == 2


def test_a_penalty_with_no_words_is_not_announced():
    """There is nothing to read out, and "a penalty was applied" without saying what it was tells a
    viewer only that something happened."""
    out = enrich(_finish_feed({**DEN_WINS, "penalties": [{"teamId": "usa", "wins": 1}]}))
    assert not [e for e in _winner_calls(out) if e.get("penalty")]


# --- the wrap-up must not contradict the finish call ---------------------------------------------

def test_the_wrap_up_stops_qualifying_a_decided_result():
    """The production layer summarises the race a few seconds after the finish call. If it kept
    saying "subject to Race Officer confirmation" the film would contradict itself about the one fact
    the viewer came for."""
    from programme import race_close_beats

    out = enrich(_finish_feed(DEN_WINS))
    spoken = " ".join(_en(b) for b in race_close_beats(out))
    assert "official" in spoken.lower()
    assert "subject to Race Officer confirmation" not in spoken


def test_the_wrap_up_still_qualifies_an_undecided_one():
    from programme import race_close_beats

    out = enrich(_finish_feed())
    spoken = " ".join(_en(b) for b in race_close_beats(out))
    assert "subject to Race Officer confirmation" in spoken


def test_the_sailing_summary_is_dropped_when_the_office_names_another_winner():
    """"Their last boat came home 9 metres ahead" is the scorer's sentence about the team the tracker
    had winning. Said of the office's winner it describes somebody else's race."""
    from programme import race_close_beats

    out = enrich(_finish_feed({**DEN_WINS, "winnerTeamId": "usa"}))
    spoken = " ".join(_en(b) for b in race_close_beats(out))
    assert "metres ahead" not in spoken
    assert "official" in spoken.lower()


# --- the pure record reader -----------------------------------------------------------------------

def test_a_record_with_no_winner_is_no_record():
    assert official_result({"officialResult": {"teams": []}}, ["den", "usa"]) is None
    assert official_result({}, ["den", "usa"]) is None


@pytest.mark.parametrize("points", [{"den": 3}, {"usa": 7}, {}])
def test_half_a_scoreline_is_no_scoreline(points):
    """"three points to" is worse than no score, and a missing number must never become a zero — a
    team that scores nothing has won every place there is."""
    record = {"winner": "den", "loser": "usa", "points": points}
    assert scoreline(record) is None


def test_a_score_is_spoken_as_a_whole_number_when_it_is_one():
    assert as_number(3.0) == 3
    assert as_number(0.5) == 0.5


def test_a_non_numeric_score_never_reaches_the_picture():
    """Feed values arrive from JSON and from other people's parsers; `None`, `""` and `NaN` all get
    here and none of them may become a number on screen."""
    record = official_result({"officialResult": {
        "winnerTeamId": "den",
        "teams": [{"teamId": "den", "points": "not a number"},
                  {"teamId": "usa", "points": float("nan")}]}}, ["den", "usa"])
    assert record["points"] == {}
    assert scoreline(record) is None
