"""The LINE-UP when the broadcast joins a race late.

A producer following a regatta puts the next race on air when the previous one has finished, which is
normally well inside the pre-start. The pre-start script used to be paced from the race's own zero
regardless, so on a followed race every beat of it — including the boat-by-boat presentation the
opening exists for — was scheduled in the past and never aired. Nobody was introduced.

`build_prestart_script(coverage_from_sec=…)` paces from where the coverage actually begins. Three
regimes, and the ordering rule between them is the point: **no boat is ever dropped**. The dwell
shrinks, the teaching lines go, the establishing shot goes — a fleet where one team is presented and
the other is not is worse than no line-up at all. Below the floor there is no line-up, and the
opening card and the clock carry it alone.

The joined-feed regimes are reached only on a join.  A feed shown from the top also uses the complete
course guide and line-up, whose timing is covered below.
"""
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "scoring"))

from commentary_i18n import Localizer  # noqa: E402
from score_race import (  # noqa: E402
    PRESENT_DWELL_MIN,
    PRESENT_TAIL_S,
    build_prestart_script,
)

DEVS = ["d1", "d2", "d3", "d4"]
TEAMS = [{"teamId": "irl", "name": "Ireland"}, {"teamId": "jpn", "name": "Japan"}]
TEAM_BOATS = {"irl": ["d1", "d2"], "jpn": ["d3", "d4"]}
SAIL_OF = {"d1": "6", "d2": "8", "d3": "2", "d4": "4"}
BOAT_TEAM = {"d1": "irl", "d2": "irl", "d3": "jpn", "d4": "jpn"}


def _script(gun_sec, coverage_from_sec=0, wind=True, programme=None):
    loc = Localizer({t["teamId"]: t["name"] for t in TEAMS}, SAIL_OF, BOAT_TEAM,
                    ["Start", "M1", "Finish"])
    return build_prestart_script(
        TEAMS, TEAM_BOATS, SAIL_OF, loc, gun_sec,
        {"windFromDeg": 200, "windKts": 9, **({"programme": programme} if programme else {})},
        wind, 9.0,
        venue_phrase=None, course_info=("windward_leeward", False, 1),
        coverage_from_sec=coverage_from_sec)


def _intros(script):
    return [e for e in script if e[1] == "boat_intro"]


def _devs(script):
    return [e[4]["deviceId"] for e in _intros(script)]


# --- a race shown from the top -------------------------------------------------------

def test_full_prestart_course_guide_completes_before_the_line_up():
    """The guide is one uninterrupted editorial sequence before the boat-by-boat line-up."""
    script, until, _explain, floor = _script(gun_sec=180)
    assert _devs(script) == DEVS                       # all four, in roster order
    guide = [e for e in script if e[4].get("courseGuide")]
    assert guide
    assert all(e[3] == "f" for e in guide), "one analyst owns the guided tour"
    # The renderer grants the final guide chapter nine seconds, followed by a five-second clean gap.
    first_intro_rsec = 180 - _intros(script)[0][0]
    last_guide_rsec = 180 - guide[-1][0]
    assert first_intro_rsec >= last_guide_rsec + 14
    assert until < _intros(script)[-1][0]
    assert floor == 60                                 # the long-standing drain floor
    assert any(e[4].get("cam") == "venue" for e in script)     # the establishing shot still asked for


def test_the_authored_welcome_is_a_structural_programme_anchor():
    """Live freshness may drop colour, but never the establishing welcome to the whole broadcast."""
    script, *_ = _script(gun_sec=180)
    welcome = next(e for e in script if e[4].get("programmeAnchor") == "welcome")
    assert welcome[1] == "phase"
    assert welcome[4]["cam"] == "venue"


def test_a_programme_resume_continues_after_the_required_welcome():
    """The scorer's venue line must not welcome the same first-race audience a second time."""
    loc = Localizer({t["teamId"]: t["name"] for t in TEAMS}, SAIL_OF, BOAT_TEAM,
                    ["Start", "M1", "Finish"])
    script, *_ = build_prestart_script(
        TEAMS, TEAM_BOATS, SAIL_OF, loc, 180,
        {"windFromDeg": 200, "windKts": 9,
         "programme": {"firstRace": True, "resumingFromWaiting": True}}, True, 9.0,
        venue_phrase={"en": "near Example Harbour", "sv": "nära Example Harbour"},
        course_info=("windward_leeward", False, 1))
    opening = next(e for e in script if e[4].get("programmeAnchor") == "welcome")
    assert opening[2]["en"].startswith("And what a place for it")
    assert "Welcome" not in opening[2]["en"]


def test_an_ordinary_race_handover_goes_straight_to_every_boat():
    """Only the session opener repeats the venue and complete course presentation."""
    script, *_ = _script(180, programme={"firstRace": False})
    assert _devs(script) == DEVS
    assert [180 - row[0] for row in _intros(script)] == [8, 16, 24, 32]
    assert not any(row[4].get("programmeAnchor") == "welcome" for row in script)
    assert not any(row[4].get("courseGuide") for row in script)


def test_a_declared_course_refresher_uses_the_normal_paced_walk_before_the_line_up():
    """The cadence walk is chaptered and paced, without replaying the venue welcome."""
    script, *_ = _script(180, programme={"firstRace": False, "courseTour": True})
    assert _devs(script) == DEVS
    guide = [row for row in script if row[4].get("courseGuide")]
    assert guide
    assert guide[0][4].get("courseGuidePlan")
    assert 180 - guide[0][0] == 10
    assert 180 - _intros(script)[0][0] >= 180 - guide[-1][0] + 14
    assert not any(row[4].get("programmeAnchor") == "welcome" for row in script)


def test_start_explainer_is_scheduled_above_the_full_prestart_drain_floor():
    """A planned explainer must be reachable, and its microphone window must remain clear."""
    script, _until, explain_prestart, floor = _script(gun_sec=195)
    explainer = next(e for e in script if "don't begin from a standstill" in e[2].get("en", ""))
    assert explain_prestart is True
    assert explainer[0] > floor
    overlapping = [e for e in script
                   if e is not explainer and e[1] == "phase" and floor < e[0] <= explainer[0]]
    assert all("Out on the course" in e[2].get("en", "") for e in overlapping), \
        "only the compact measured-wind lead-in may share the reserved pre-explainer window"


def test_course_walk_names_each_actual_leg_and_keeps_it_in_its_own_chapter():
    """A course shape is only a headline: the walk must call every actual leg correctly.

    This guards the Digital-M's second beat.  It used to be absorbed into a generic figure-eight
    "run" chapter, which made the narration false and made the camera wait at M4 while speech moved
    on to M5.
    """
    loc = Localizer({t["teamId"]: t["name"] for t in TEAMS}, SAIL_OF, BOAT_TEAM,
                    ["Start", "M1", "M2", "M3", "M4", "M5", "M6", "Finish"])
    legs = [("upwind", 0, 1), ("reach", 1, 2), ("downwind", 2, 3),
            ("reach", 3, 4), ("upwind", 4, 5), ("reach", 5, 6),
            ("downwind", 6, 7)]
    script, *_ = build_prestart_script(
        TEAMS, TEAM_BOATS, SAIL_OF, loc, 240,
        {"windFromDeg": 200, "windKts": 9}, True, 9.0,
        venue_phrase=None, course_info=("figure8", False, 6), course_legs=legs)
    guide = [e for e in script if e[4].get("courseGuide")]
    m4_m5 = next(e for e in guide if e[4].get("courseFrac") == [0.5714, 0.7143])
    assert "a beat from fourth mark to fifth mark" in m4_m5[2]["en"]
    assert "a run from fourth mark" not in m4_m5[2]["en"]


def test_first_course_event_carries_the_complete_continuous_visual_plan():
    """A live renderer must know the next leg before that leg's spoken event reaches the stream."""
    loc = Localizer({t["teamId"]: t["name"] for t in TEAMS}, SAIL_OF, BOAT_TEAM,
                    ["Start", "M1", "M2", "M3", "M4", "M5", "M6", "Finish"])
    legs = [("upwind", 0, 1), ("reach", 1, 2), ("downwind", 2, 3),
            ("reach", 3, 4), ("upwind", 4, 5), ("reach", 5, 6),
            ("downwind", 6, 7)]
    script, *_ = build_prestart_script(
        TEAMS, TEAM_BOATS, SAIL_OF, loc, 240,
        {"windFromDeg": 200, "windKts": 9}, True, 9.0,
        venue_phrase=None, course_info=("figure8", False, 6), course_legs=legs)
    first = next(e for e in script if e[4].get("cam") == "course")
    plan = first[4].get("courseGuidePlan")
    assert len(plan) == len(legs)
    assert plan[0]["courseFrac"][0] == 0.0
    assert plan[-1]["courseFrac"][1] == 1.0
    for chapter, following in zip(plan, plan[1:], strict=False):
        assert chapter["offsetS"] + chapter["durationS"] == following["offsetS"], \
            "visual chapters must meet exactly, with no stop at the rounding"
    assert first[4]["courseGuideEndOffsetS"] >= plan[-1]["offsetS"] + plan[-1]["durationS"]


def test_a_short_pre_start_shown_from_the_top_is_also_unchanged():
    """The other original branch — a synthetic feed with no real pre-start. Its beats never drain
    (the 60 s floor is above the whole feed), and that stays true."""
    script, _until, _explain, floor = _script(gun_sec=30)
    assert floor == 60
    assert _devs(script) == DEVS


# --- joined late: every boat still presented ------------------------------------------------------

def test_every_boat_is_presented_when_the_broadcast_joins_late():
    script, _until, _explain, _floor = _script(gun_sec=202, coverage_from_sec=150)
    assert _devs(script) == DEVS, "a joined race must still introduce every boat, in roster order"


def test_the_line_up_compresses_rather_than_dropping_a_boat():
    """Less time ⇒ shorter dwell, never a shorter fleet."""
    roomy = _script(gun_sec=202, coverage_from_sec=100)[0]
    tight = _script(gun_sec=202, coverage_from_sec=155)[0]
    assert _devs(roomy) == _devs(tight) == DEVS
    roomy_gaps = [a[0] - b[0] for a, b in zip(_intros(roomy), _intros(roomy)[1:], strict=False)]
    tight_gaps = [a[0] - b[0] for a, b in zip(_intros(tight), _intros(tight)[1:], strict=False)]
    assert min(tight_gaps) < min(roomy_gaps)
    assert min(tight_gaps) >= PRESENT_DWELL_MIN, "a presentation shorter than this is a flicker"


def test_the_presentations_finish_clear_of_the_gun():
    """The final-minute countdown, the favoured-end call and the time-and-distance read own the last
    seconds; a compressed line-up must not still be running through them."""
    script, _until, _explain, _floor = _script(gun_sec=202, coverage_from_sec=155)
    assert all(e[0] > PRESENT_TAIL_S for e in _intros(script))


def test_the_whole_script_lands_after_the_coverage_starts():
    """The bug this exists to fix: a beat scheduled before the join is a beat that never airs."""
    script, _until, _explain, _floor = _script(gun_sec=202, coverage_from_sec=150)
    avail = 202 - 150
    assert script, "a joined race with room must schedule something"
    assert all(e[0] <= avail for e in script), "nothing may be scheduled before the coverage begins"


def test_the_drain_floor_clears_the_last_scheduled_beat():
    """A line-up that is built and then never spoken is the same as no line-up. The floor moves with
    the script; leaving it at 60 is what silenced a race joined inside the last minute."""
    script, _until, _explain, floor = _script(gun_sec=202, coverage_from_sec=155)
    assert all(e[0] > floor for e in script)


def test_with_no_room_the_card_carries_the_line_up_alone():
    script, until, _explain, _floor = _script(gun_sec=202, coverage_from_sec=196)
    assert _intros(script) == [], "seconds from the gun, nothing is rushed onto the air"
    assert until == 202, "…and the intro window is closed, so reactive commentary starts at once"


def test_a_joined_race_asks_for_no_establishing_shot():
    """The drone has just flown the wide shot itself, across the hand-over. A second `cam:'venue'`
    here would cancel its descent onto the new race."""
    script, _until, _explain, _floor = _script(gun_sec=202, coverage_from_sec=150)
    assert not [e for e in script if e[4].get("cam")]
