from pathlib import Path
import sys

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from server import (  # noqa: E402
    LATEST_SESSION_PAGES,
    best_consecutive,
    build_track_rankings,
    car_label,
    cache_session_key,
    combined_ranking,
    connected_now,
    fold_name,
    names_match,
    occupancy_from_stream,
    parse_cache_stem,
    parse_driver_laps,
    parse_sessionstat_rows,
    plan_current_event,
    plan_latest_sessions,
    pretty_track,
    resolve_pilot_id,
    select_latest_cache,
    server_number,
    unique_session_ids,
    apply_tyre_stats,
    compact_result,
    parse_results_rows,
    plan_results_event,
    result_track_id,
    tyre_stats,
)


def test_solo_baja_sesiones_que_faltan_de_la_tanda_reciente():
    to_fetch, latest = plan_latest_sessions(
        ["4250", "4249", "4249", "4101"],
        cached_ids={4250, 4249},
        max_new=160,
    )
    assert latest == ["4250", "4249", "4101"]
    assert to_fetch == ["4101"]


def test_no_pide_historial_aunque_el_cache_este_vacio():
    page = [str(n) for n in range(1015, 865, -1)]
    to_fetch, latest = plan_latest_sessions(page, cached_ids=set(), max_new=3)
    assert latest[0] == "1015"
    assert to_fetch == ["1015", "1014", "1013"]
    assert "866" not in to_fetch


def test_select_latest_cache_ignora_sesiones_viejas():
    folder = Path(__file__).resolve().parent / "_tmp_ac_cache"
    folder.mkdir(exist_ok=True)
    try:
        for stem in ("8771_4008", "8771_4250", "8771_4249", "8772_900"):
            (folder / f"{stem}.json").write_text('{"session_id": "x", "drivers": []}', encoding="utf-8")
        (folder / "basura.json").write_text("{}", encoding="utf-8")
        files = select_latest_cache(folder, per_port=2)
        stems = sorted(path.stem for path in files)
        assert stems == ["8771_4249", "8771_4250", "8772_900"]
        assert "8771_4008" not in stems
        only = select_latest_cache(folder, only_keys={"8771_4250", "no_existe"})
        assert [path.stem for path in only] == ["8771_4250"]
    finally:
        for path in folder.glob("*"):
            path.unlink()
        folder.rmdir()


def test_helpers_de_clave():
    assert cache_session_key("8771", "4250") == "8771_4250"
    assert parse_cache_stem("8771_4250") == ("8771", 4250)
    assert parse_cache_stem("roto") is None
    assert unique_session_ids(["2", "2", "1"]) == ["2", "1"]


def test_el_escaner_no_abre_paginas_viejas():
    assert LATEST_SESSION_PAGES == 1
    source = (ROOT / "server.py").read_text(encoding="utf-8")
    assert "while page < 80" not in source
    assert "sessionstat?page=0" in source
    assert "AC_REFRESH_SECONDS = 25" in source


def test_el_evento_actual_corta_cuando_cambia_el_circuito():
    rows = [
        {"id": "4250", "track": "d7fox_mid_ohio-no_chicane"},
        {"id": "4249", "track": "d7fox_mid_ohio-no_chicane"},
        {"id": "4244", "track": "grobnik"},
        {"id": "4243", "track": "grobnik"},
    ]
    to_fetch, live, latest = plan_current_event(rows, cached_ids={4250}, max_new=10)
    assert live == "d7fox_mid_ohio-no_chicane"
    assert latest == ["4250", "4249"]
    assert to_fetch == ["4250", "4249"]
    assert "4244" not in latest


def test_releen_la_sesion_en_curso_aunque_ya_este_en_cache():
    rows = [
        {"id": "4251", "track": "d7fox_mid_ohio-no_chicane"},
        {"id": "4250", "track": "d7fox_mid_ohio-no_chicane"},
        {"id": "4244", "track": "grobnik"},
    ]
    to_fetch, live, latest = plan_current_event(
        rows, cached_ids={4251, 4250, 4244}, max_new=10, refresh_latest=2
    )
    assert live == "d7fox_mid_ohio-no_chicane"
    assert to_fetch == ["4251", "4250"]
    assert "4244" not in to_fetch
    frozen, _, _ = plan_current_event(rows, cached_ids={4251, 4250}, max_new=10, refresh_latest=0)
    assert frozen == []


def test_parsea_filas_de_sessionstat():
    html = """
    <tr class="clickableRow" href="sessiondetails?sessionid=4250#">
      <td>d7fox_mid_ohio-no_chicane</td><td>Qualify</td><td>A</td><td></td><td></td><td>1</td>
      <td>2026-09-19 02:03</td>
    </tr>
    <tr class="clickableRow" href="sessiondetails?sessionid=4244#">
      <td>grobnik</td><td>Race</td><td>B</td><td></td><td></td><td>7</td>
      <td>2026-07-30 23:10</td>
    </tr>
    """
    rows = parse_sessionstat_rows(html)
    assert [row["id"] for row in rows] == ["4250", "4244"]
    assert rows[0]["track"] == "d7fox_mid_ohio-no_chicane"


def test_un_piloto_aparece_en_ohio_aunque_sea_mas_rapido_en_otro_sitio():
    folder = Path(__file__).resolve().parent / "_tmp_tracks"
    folder.mkdir(exist_ok=True)
    try:
        ohio = {
            "track": "d7fox_mid_ohio-no_chicane",
            "session_type": "Qualify",
            "date_text": "2026-09-19 00:20",
            "drivers": [
                {
                    "id": "1",
                    "name": "Andoni Garcia",
                    "car": "ks_mazda_787b",
                    "laps": [
                        {"time_ms": 78202, "valid": True, "splits": [0, 0, 0], "car": "ks_mazda_787b"},
                        {"time_ms": 78300, "valid": True, "splits": [0, 0, 0], "car": "ks_mazda_787b"},
                    ],
                }
            ],
        }
        grobnik = {
            "track": "grobnik",
            "session_type": "Race",
            "date_text": "2026-07-30 23:10",
            "drivers": [
                {
                    "id": "1",
                    "name": "Andoni Garcia",
                    "car": "otro",
                    "laps": [{"time_ms": 62000, "valid": True, "splits": [0, 0, 0], "car": "otro"}],
                }
            ],
        }
        (folder / "8771_1.json").write_text(__import__("json").dumps(ohio), encoding="utf-8")
        (folder / "8771_2.json").write_text(__import__("json").dumps(grobnik), encoding="utf-8")
        by_track = build_track_rankings(list(folder.glob("*.json")), {"d7fox_mid_ohio-no_chicane"})
        names = [row["name"] for row in by_track["d7fox_mid_ohio-no_chicane"]["ranking"]]
        assert names == ["Andoni Garcia"]
        assert by_track["d7fox_mid_ohio-no_chicane"]["ranking"][0]["best_ms"] == 78202
        assert "grobnik" not in by_track
    finally:
        for path in folder.glob("*"):
            path.unlink()
        folder.rmdir()


def test_clasificatoria_suma_media2_de_los_dos_circuitos():
    by_track = {
        "ohio": {
            "ranking": [
                {"id": "ac:oliver", "name": "Oliver", "avg2_ms": 77200, "avg2_display": "1:17.200"},
                {"id": "ac:andoni", "name": "Andoni", "avg2_ms": 78400, "avg2_display": "1:18.400"},
            ]
        },
        "laguna": {
            "ranking": [
                {"id": "ac:oliver", "name": "Oliver", "avg2_ms": 79800, "avg2_display": "1:19.800"},
            ]
        },
    }
    rows = combined_ranking(by_track, ["ohio", "laguna"], "avg2")
    assert [row["name"] for row in rows] == ["Oliver"]
    assert rows[0]["total_ms"] == 77200 + 79800
    assert rows[0]["gap_display"] == "Lider"


def test_nombres_con_tilde_son_el_mismo_piloto():
    assert fold_name("Jose Luis Núñez") == "jose luis nunez"
    assert fold_name("Jose Luis Nunez") == "jose luis nunez"
    assert pretty_track("d7fox_mid_ohio-no_chicane") == "Mid Ohio"
    assert pretty_track("ks_laguna_seca") == "Laguna Seca"


def test_media2_exige_dos_validas_seguidas():
    solo_una = [
        {"time_ms": 78310, "valid": False},
        {"time_ms": 77707, "valid": True},
    ]
    assert best_consecutive(solo_una, 2) is None
    separadas = [
        {"time_ms": 78310, "valid": True},
        {"time_ms": 90000, "valid": False},
        {"time_ms": 77707, "valid": True},
    ]
    assert best_consecutive(separadas, 2) is None
    seguidas = [
        {"time_ms": 78310, "valid": True},
        {"time_ms": 77707, "valid": True},
    ]
    got = best_consecutive(seguidas, 2)
    assert got is not None
    assert got["laps"] == [78310, 77707]
    assert got["avg_ms"] == (78310 + 77707) // 2


def test_media2_trunca_como_el_server_no_redondea_al_alza():
    """Suma impar → media .5 ms. Python round() sube 1 ms; el server se queda abajo."""
    laps = [
        {"time_ms": 85005, "valid": True},
        {"time_ms": 85006, "valid": True},
    ]
    got = best_consecutive(laps, 2)
    assert got is not None
    assert got["avg_ms"] == 85005
    assert got["avg_ms"] == (85005 + 85006) // 2
    assert got["avg_ms"] != int(round((85005 + 85006) / 2))


def test_salvi_y_salvador_son_el_mismo_piloto():
    assert names_match("Salvador Vargas", "Salvi Vargas")
    assert names_match("Toño Martinez", "Tono Martinez")
    assert names_match("Jose Luis Núñez", "Jose Luis Nunez")
    assert names_match("torino arenas", "Torino Arenas")
    assert not names_match("Jordi Martinez", "Jose Martinez")
    assert not names_match("Andoni Garcia", "Diego Garcia")
    assert not names_match("Alvaro fernandez", "Oliver Fernandez")
    known: list[tuple[str, str]] = []
    first = resolve_pilot_id("Salvador Vargas", known)
    known.append((first, "Salvador Vargas"))
    assert resolve_pilot_id("Salvi Vargas", known) == first


def test_clasificatoria_une_salvi_y_salvador():
    by_track = {
        "ohio": {
            "ranking": [
                {"id": "ac:salvador vargas", "name": "Salvador Vargas", "avg2_ms": 78008, "avg2_display": "1:18.008"},
            ]
        },
        "laguna": {
            "ranking": [
                {"id": "ac:salvi vargas", "name": "Salvi Vargas", "avg2_ms": 80867, "avg2_display": "1:20.867"},
            ]
        },
    }
    rows = combined_ranking(by_track, ["ohio", "laguna"], "avg2")
    assert len(rows) == 1
    assert rows[0]["name"] == "Salvador Vargas"
    assert rows[0]["total_ms"] == 78008 + 80867
    assert rows[0]["parts_display"] == ["1:18.008", "1:20.867"]


def test_assetto_corsa_1_es_el_juego_por_defecto():
    html = (ROOT / "static" / "index.html").read_text(encoding="utf-8")
    assert 'let activeTab = "track:0";' in html
    assert "Clasificatoria" in html
    assert 'id="gameSel"' not in html
    combo = html.find('id: "combo"')
    evo = html.find('id: "evo"')
    assert combo != -1 and evo != -1
    assert combo < evo
    assert "function namesMatch" in html
    assert "<th>Vueltas</th><th>Mejor</th>" in html
    assert "servers_online || []).length" not in html
    assert "state.message || state.error" in html
    assert "state.error || state.message" not in html
    assert "function connectedNow" in html
    assert ">Road Atlanta<" not in html


def test_livemap_vacio_son_cero_pilotos_no_dos_servers():
    raw = (
        "<html><head></head><body>"
        '{&#34;class_data&#34;: &#34;\\n&#34;, &#34;session_data&#34;: &#34;d7fox_mid_ohio-no_chicaneQualify178:42\\n&#34;}'
        "</body></html>"
    )
    occ = occupancy_from_stream(raw)
    assert occ["connected_drivers"] == 0
    assert occ["drivers_now"] == []


def test_livemap_html_entities_reales_vacio():
    raw = (
        '<html><head></head><body>{&#34;alive_data&#34;: &#34;<circle></circle>\\n&#34;, '
        '&#34;class_data&#34;: &#34;\\n&#34;, &#34;state&#34;: &#34;ready&#34;, '
        '&#34;session_data&#34;: &#34;d7fox_mid_ohio-no_chicaneQualify168:32\\n&#34;}</body></html>'
    )
    occ = occupancy_from_stream(raw)
    assert occ["connected_drivers"] == 0
    assert occ["drivers_now"] == []


def test_livemap_html_entities_cuenta_piloto():
    raw = (
        '<html><head></head><body>{&#34;class_data&#34;: &#34;'
        "<tr><td>1</td><td>ks_mazda_787b</td><td>Jordi Martinez</td><td>01:19.171</td></tr>"
        '\\n&#34;, &#34;state&#34;: &#34;ready&#34;}</body></html>'
    )
    occ = occupancy_from_stream(raw)
    assert occ["connected_drivers"] == 1
    assert occ["drivers_now"] == ["Jordi Martinez"]


def test_livemap_cuenta_filas_de_pilotos():
    raw = (
        '<html><body>{"class_data": "'
        "<tr><td>1</td><td>ks_mazda_787b</td><td>Jordi Martinez</td><td>01:19.171</td></tr>"
        "<tr><td>2</td><td>ks_mazda_787b</td><td>Oliver Fernandez</td><td>01:17.201</td></tr>"
        '", "state": "ready"}</body></html>'
    )
    occ = occupancy_from_stream(raw)
    assert occ["connected_drivers"] == 2
    assert occ["drivers_now"] == ["Jordi Martinez", "Oliver Fernandez"]


def test_connected_now_es_por_circuito():
    tracks = [
        {"id": "ohio", "connected_drivers": 0},
        {"id": "laguna", "connected_drivers": 1},
    ]
    assert connected_now(tracks) == 1
    assert connected_now(tracks, "ohio") == 0
    assert connected_now(tracks, "laguna") == 1


def test_valid_es_la_columna_quinta_no_el_fuel():
    fragment = """
    <tr>
      <td>1.</td><td>01:54.287</td><td>-</td><td>-</td>
      <td>no</td><td>2</td><td>0 (0)</td><td>-</td>
    </tr>
    <tr>
      <td>2.</td><td>01:19.171</td><td>-</td><td>-</td>
      <td>yes</td><td>0</td><td>0 (0)</td><td>-</td>
    </tr>
    """
    laps = parse_driver_laps(fragment)
    assert [(lap["time_ms"], lap["valid"]) for lap in laps] == [(114287, False), (79171, True)]


def test_server_1_y_2_salen_del_ultimo_numero_del_puerto():
    assert server_number("8771") == 1
    assert server_number("8772") == 2
    assert server_number(8772) == 2
    assert server_number("") is None


def test_coche_corto_porsche_o_mazda():
    assert car_label("ks_porsche_962c_shorttail") == "Porsche"
    assert car_label("ks_mazda_787b") == "Mazda"
    assert car_label("-") == "—"
    assert car_label("ks_ferrari_f40") == "Ferrari F40"


def test_cada_tiempo_lleva_su_coche_y_no_hay_media_3():
    import json
    folder = Path(__file__).resolve().parent / "_tmp_coches"
    folder.mkdir(exist_ok=True)
    try:
        s1 = {
            "track": "donington",
            "session_type": "Practice",
            "date_text": "2026-09-27 21:00",
            "drivers": [
                {
                    "id": "1",
                    "name": "Toni Perez",
                    "car": "ks_mazda_787b",
                    "laps": [
                        {"time_ms": 90000, "valid": True, "splits": [0, 0, 0], "car": "ks_mazda_787b"},
                        {"time_ms": 90100, "valid": True, "splits": [0, 0, 0], "car": "ks_mazda_787b"},
                    ],
                }
            ],
        }
        s2 = {
            "track": "donington",
            "session_type": "Practice",
            "date_text": "2026-09-28 21:00",
            "drivers": [
                {
                    "id": "1",
                    "name": "Toni Perez",
                    "car": "ks_porsche_962c_shorttail",
                    "laps": [
                        {"time_ms": 89500, "valid": True, "splits": [0, 0, 0], "car": "ks_porsche_962c_shorttail"},
                        {"time_ms": 95000, "valid": False, "splits": [0, 0, 0], "car": "ks_porsche_962c_shorttail"},
                        {"time_ms": 91000, "valid": True, "splits": [0, 0, 0], "car": "ks_porsche_962c_shorttail"},
                    ],
                }
            ],
        }
        (folder / "8771_1.json").write_text(json.dumps(s1), encoding="utf-8")
        (folder / "8771_2.json").write_text(json.dumps(s2), encoding="utf-8")
        ranking = build_track_rankings(sorted(folder.glob("*.json")))["donington"]["ranking"]
        # 30-sep: mismo piloto con dos coches = dos filas independientes.
        rows = {r["car_label"]: r for r in ranking}
        assert sorted(rows) == ["Mazda", "Porsche"]
        assert len({r["id"] for r in ranking}) == 2
        porsche, mazda = rows["Porsche"], rows["Mazda"]
        assert porsche["best_ms"] == 89500 and mazda["best_ms"] == 90000
        assert porsche["total_laps"] == 3 and porsche["valid_laps"] == 2
        assert mazda["total_laps"] == 2 and mazda["valid_laps"] == 2
        assert mazda["avg2_ms"] == 90050 and porsche["avg2_ms"] is None  # la vuelta sucia del Porsche corta la media
        assert [h["car_label"] for h in mazda["history"]] == ["Mazda"]
        assert [h["car_label"] for h in porsche["history"]] == ["Porsche"]
        assert [r["car_label"] for r in ranking] == ["Porsche", "Mazda"]  # ordenadas por mejor vuelta
        assert not any(key.startswith("avg3") for r in ranking for key in r)
        assert not any(key.startswith("avg3") for r in ranking for h in r["history"] for key in h)
    finally:
        for path in folder.glob("*"):
            path.unlink()
        folder.rmdir()


# --- Compuestos y lastre (resultados del Server Manager) ---

RESULTS_ROW_HTML = """
                            <tr class="row-link" data-href="/results/{file}">
                                <td>
                                    {date}
                                </td>
                                <td>

                                        {kind}

                                </td>
                                <td>
                                    {track}
                                </td>
                                <td>
                                    <small>Salvi Vargas</small>
                                </td>
"""


def _row(file, track, kind="Practice", date="28 Sep 26 14:37 CEST"):
    return RESULTS_ROW_HTML.format(file=file, track=track, kind=kind, date=date)


def test_lee_la_lista_de_resultados_del_server():
    html = _row("2026_9_28_14_37_PRACTICE", "Donington Park 2018 GP") + _row(
        "2026_9_27_20_2_QUALIFY", "Mid-Ohio (No Chicane)", "Qualifying"
    )
    rows = parse_results_rows(html)
    assert [r["file"] for r in rows] == ["2026_9_28_14_37_PRACTICE", "2026_9_27_20_2_QUALIFY"]
    assert rows[0]["track_text"] == "Donington Park 2018 GP"
    assert rows[1]["type"] == "Qualifying"


def test_resultados_solo_del_circuito_actual_y_relee_las_dos_ultimas():
    rows = parse_results_rows(
        _row("C", "Donington Park 2018 GP")
        + _row("B", "Donington Park 2018 GP")
        + _row("A", "Donington Park 2018 GP")
        + _row("Z", "Mid-Ohio (No Chicane)")
    )
    to_fetch, event, ended = plan_results_event(rows, cached={"C", "B", "A"})
    assert event == ["C", "B", "A"]
    assert ended is True
    assert to_fetch == ["C", "B"]  # la en curso y la anterior se releen; A ya está y no cambia
    to_fetch, _, _ = plan_results_event(rows, cached=set())
    assert to_fetch == ["C", "B", "A"]  # nunca baja la de Mid-Ohio


def test_si_la_pagina_no_cambia_de_circuito_hay_que_seguir_pasando_pagina():
    rows = parse_results_rows(_row("B", "Laguna Seca") + _row("A", "Laguna Seca"))
    _, event, ended = plan_results_event(rows, cached=set())
    assert event == ["B", "A"] and ended is False


def test_nombre_de_circuito_igual_que_el_stracker():
    assert result_track_id({"TrackName": "doningtonpark2018", "TrackConfig": "gp"}) == "doningtonpark2018-gp"
    assert result_track_id({"TrackName": "d7fox_mid_ohio", "TrackConfig": "no_chicane"}) == "d7fox_mid_ohio-no_chicane"
    assert result_track_id({"TrackName": "ks_laguna_seca", "TrackConfig": ""}) == "ks_laguna_seca"


def _lap(name, car, tyre, ms, cuts=0, ballast=0, ts=0):
    return {"DriverName": name, "CarModel": car, "Tyre": tyre, "LapTime": ms, "Cuts": cuts, "BallastKG": ballast, "Timestamp": ts}


def _result(date, laps, cars=()):
    return compact_result(
        {
            "TrackName": "doningtonpark2018",
            "TrackConfig": "gp",
            "Type": "PRACTICE",
            "Date": date,
            "Cars": [{"Driver": {"Name": c[0]}, "BallastKG": c[1], "Model": c[2] if len(c) > 2 else "ks_mazda_787b"} for c in cars],
            "Laps": laps,
        }
    )


MAZDA = "ks_mazda_787b"
PORSCHE = "ks_porsche_962c_shorttail"


def test_mejor_vuelta_limpia_por_coche_y_compuesto():
    s1 = _result(
        "2026-09-28T02:35:00+02:00",
        [
            _lap("Salvi Vargas", MAZDA, "S", 84000, ts=1),
            _lap("Salvi Vargas", MAZDA, "S", 83000, cuts=1, ts=2),  # cortada: no cuenta
            _lap("Salvi Vargas", MAZDA, "H", 85500, ts=3),
            _lap("Salvi Vargas", MAZDA, "H", 85100, ts=4),
            _lap("Salvi Vargas", PORSCHE, "S", 82900, ts=5),
        ],
    )
    stats = tyre_stats([s1])
    by = stats["drivers"]["Salvi Vargas"]["by_tyre"]
    assert by["ks_mazda_787b|S"]["ms"] == 84000
    assert by["ks_mazda_787b|H"]["ms"] == 85100
    assert by["ks_porsche_962c_shorttail|S"]["ms"] == 82900
    assert len(by) == 3
    labels = {c["key"]: c["label"] for c in stats["compounds"]}
    assert labels == {"ks_mazda_787b|S": "Mazda S", "ks_mazda_787b|H": "Mazda H", "ks_porsche_962c_shorttail|S": "Porsche S"}


def test_con_un_solo_coche_la_etiqueta_es_solo_el_compuesto():
    stats = tyre_stats([_result("2026-09-28", [_lap("A B", MAZDA, "S", 84000), _lap("A B", MAZDA, "US", 86000)])])
    assert sorted(c["label"] for c in stats["compounds"]) == ["S", "US"]


def test_cinco_compuestos_por_coche_caben_todos():
    tyres = ["XS", "S", "M", "H", "W"]
    laps = [_lap("A B", car, t, 80000 + i) for car in (MAZDA, PORSCHE) for i, t in enumerate(tyres)]
    stats = tyre_stats([_result("2026-09-28", laps)])
    assert len(stats["compounds"]) == 10
    assert len(stats["drivers"]["A B"]["by_tyre"]) == 10


def test_el_lastre_es_el_de_la_sesion_mas_reciente():
    old = _result("2026-09-27T23:35:00+02:00", [_lap("Salvi Vargas", MAZDA, "S", 84000, ballast=20, ts=10)])
    new = _result("2026-09-28T14:37:00+02:00", [_lap("Salvi Vargas", MAZDA, "S", 84500, ballast=5, ts=20)])
    assert tyre_stats([new, old])["drivers"]["Salvi Vargas"]["ballast_kg"] == 5
    # conectado en la última sesión sin dar vuelta: manda la lista de coches de esa sesión
    only_connected = _result("2026-09-28T18:00:00+02:00", [], cars=[("Salvi Vargas", 0)])
    assert tyre_stats([only_connected, old])["drivers"]["Salvi Vargas"]["ballast_kg"] == 0
    # la hora de vuelta vuelve a cero en cada sesión: una vuelta vieja "tardía" no pisa a la nueva
    late_old = _result("2026-09-27T23:35:00+02:00", [_lap("Salvi Vargas", MAZDA, "S", 84000, ballast=20, ts=5000000)])
    early_new = _result("2026-09-28T14:37:00+02:00", [_lap("Salvi Vargas", MAZDA, "S", 84500, ballast=5, ts=1000)])
    assert tyre_stats([late_old, early_new])["drivers"]["Salvi Vargas"]["ballast_kg"] == 5


def test_se_pega_a_la_fila_del_stracker_aunque_el_nombre_cambie():
    stats = tyre_stats([_result("2026-09-28", [_lap("Salvi Vargas", MAZDA, "S", 83634, ballast=10)])])
    block = {"ranking": [{"name": "Salvador Vargas", "car": MAZDA, "best_ms": 83634}, {"name": "Primo Isra", "car": MAZDA, "best_ms": 83898}]}
    apply_tyre_stats(block, stats)
    salvi, primo = block["ranking"]
    assert salvi["by_tyre"]["ks_mazda_787b|S"]["display"] == "1:23.634"
    assert salvi["ballast_kg"] == 10
    assert primo["by_tyre"] == {} and primo["ballast_kg"] is None
    assert block["compounds"][0]["key"] == "ks_mazda_787b|S"


def test_server_apagado_no_asoma_mientras_se_repasa():
    import json
    import shutil
    from server import AcScanner, STRACKER_BASES

    folder = Path(__file__).resolve().parent / "_tmp_pestanas"
    folder.mkdir(exist_ok=True)
    try:
        for port, track in (("8771", "doningtonpark2018-gp"), ("8772", "ks_laguna_seca")):
            (folder / f"{cache_session_key(port, '100')}.json").write_text(json.dumps({"track": track}), encoding="utf-8")
        scanner = AcScanner()
        scanner.cache_dir = folder
        scanner.state["servers_online"] = [STRACKER_BASES[0]]
        assert [t["id"] for t in scanner.detect_live_tracks()] == ["doningtonpark2018-gp"]
        scanner.state["servers_online"] = []  # sin saber aún quién responde: se enseña lo guardado
        assert [t["id"] for t in scanner.detect_live_tracks()] == ["doningtonpark2018-gp", "ks_laguna_seca"]
    finally:
        shutil.rmtree(folder, ignore_errors=True)


def _stream_real(filas: str) -> str:
    # Copia literal de lo que manda el stracker (28-sep): la tabla llega sin <tr>/<td>.
    return (
        r'<html><head></head><body>{&#34;chat_data&#34;: &#34;&#34;, &#34;state&#34;: &#34;ready&#34;, '
        r'&#34;session_data&#34;: &#34;doningtonpark2018-gpPractice77:40\n&#34;, &#34;class_data&#34;: &#34;\n            '
        + filas
        + r'&#34;, &#34;alive_data&#34;: &#34;<circle r="\&#34;6\&#34;"></circle>\n&#34;}</body></html>'
    )


FILA_MANU = (
    r"\n                <b>•</b> 1.\n                    ks_mazda_787b\n\n"
    r"                Manu Sanjuán\n                01:26.727\n                5\n                -\n            "
)
FILA_JORDI = (
    r"\n                <b>•</b> 2.\n                    ks_mazda_787b\n\n"
    r"                Jordi Martinez\n                01:27.101\n                3\n                +0.374\n            "
)


def test_livemap_real_sin_tabla_cuenta_un_piloto():
    occ = occupancy_from_stream(_stream_real(FILA_MANU + r"\n"))
    assert occ["connected_drivers"] == 1
    assert occ["drivers_now"] == ["Manu Sanjuán"]


def test_livemap_real_sin_tabla_cuenta_dos_pilotos():
    occ = occupancy_from_stream(_stream_real(FILA_MANU + FILA_JORDI + r"\n"))
    assert occ["connected_drivers"] == 2
    assert occ["drivers_now"] == ["Manu Sanjuán", "Jordi Martinez"]


def test_livemap_real_vacio_sigue_en_cero():
    occ = occupancy_from_stream(_stream_real(r"\n"))
    assert occ["connected_drivers"] == 0


# --- 29-sep: una tabla por server, compuesto en directo, contador de pista que no parpadea ---

DIRECTO = Path(__file__).resolve().parent / "directo_8771_29sep.json"


def _foto_real():
    import json

    return json.loads(DIRECTO.read_text(encoding="utf-8"))["Message"]


def test_directo_real_cuenta_pilotos_y_da_compuesto_al_momento():
    from server import live_from_snapshot

    live = live_from_snapshot(_foto_real())
    assert live["track"] == "doningtonpark2018-gp"
    assert live["connected_drivers"] == 2
    assert sorted(live["drivers_now"]) == ["Daniel Espejo", "Jon Rojo"]
    jon = [lap for lap in live["session"]["laps"] if lap["driver"] == "Jon Rojo"]
    assert [(lap["ms"], lap["tyre"]) for lap in jon] == [(84040, "M")]
    # Daniel está dentro pero sin vuelta limpia: no se inventa un tiempo
    assert not [lap for lap in live["session"]["laps"] if lap["driver"] == "Daniel Espejo"]


def test_directo_sin_vuelta_limpia_no_es_un_tiempo():
    from server import live_from_snapshot

    def drv(name, best_ns, tyre="S"):
        return {"CarInfo": {"DriverName": name}, "Ballast": 15, "Cars": {MAZDA: {"BestLap": best_ns, "TyreBestLap": tyre}}}

    snap = {
        "SessionInfo": {"Track": "doningtonpark2018", "TrackConfig": "gp"},
        "ConnectedDrivers": {
            "Drivers": {"1": drv("Cero", 0), "2": drv("Nueves", 999999999 * 1_000_000), "3": drv("Bueno", 83500 * 1_000_000)}
        },
        "DisconnectedDrivers": {"Drivers": {"4": drv("Ido", 84000 * 1_000_000, "M")}},
    }
    live = live_from_snapshot(snap)
    assert live["connected_drivers"] == 3  # el que se fue no cuenta como en pista
    got = sorted((lap["driver"], lap["ms"], lap["tyre"]) for lap in live["session"]["laps"])
    assert got == [("Bueno", 83500, "S"), ("Ido", 84000, "M")]
    assert live["session"]["ballast"]["Bueno"] == 15


def _trama(payload, opcode=1, mask=None):
    import struct

    n = len(payload)
    head = bytes([0x80 | opcode])
    bit = 0x80 if mask else 0
    if n < 126:
        head += bytes([bit | n])
    elif n < 65536:
        head += bytes([bit | 126]) + struct.pack(">H", n)
    else:
        head += bytes([bit | 127]) + struct.pack(">Q", n)
    if mask:
        payload = bytes(c ^ mask[i % 4] for i, c in enumerate(payload))
        head += mask
    return head + payload


def test_tramas_del_directo_largas_troceadas_y_con_ping():
    from server import parse_ws_frames

    corto, medio, largo = b'{"a":1}', b"x" * 300, b"y" * 70000
    todo = _trama(corto) + _trama(b"", opcode=9) + _trama(medio, mask=b"\x01\x02\x03\x04") + _trama(largo)
    # llega a trozos, como por la red
    msgs, rest = [], b""
    for i in range(0, len(todo), 999):
        got, rest = parse_ws_frames(rest + todo[i : i + 999])
        msgs += got
    assert msgs == [corto, medio, largo] and rest == b""
    got, rest = parse_ws_frames(_trama(medio)[:-1])
    assert got == [] and len(rest) == len(_trama(medio)) - 1


def test_compuesto_del_directo_solo_si_el_stracker_da_la_vuelta_por_valida():
    from server import checked_live_session

    session = {
        "laps": [
            {"driver": "Jon Rojo", "car": MAZDA, "ms": 84040, "tyre": "M"},
            {"driver": "JL Nuñez", "car": MAZDA, "ms": 82734, "tyre": "S"},
            {"driver": "Otro Piloto", "car": MAZDA, "ms": 85000, "tyre": "S"},
            {"driver": "Jon Rojo", "car": PORSCHE, "ms": 84041, "tyre": "M"},  # limpia con el Mazda, no con este
        ]
    }
    valid = {("Jon Rojo", MAZDA): {84041}, ("Jose Luis Nuñez", MAZDA): {82734}, ("Otro Piloto", MAZDA): {85100}}
    out = checked_live_session(session, valid)
    assert [lap["cuts"] for lap in out["laps"]] == [0, 0, 1, 1]


def test_iniciales_son_el_mismo_piloto():
    assert names_match("JL Nuñez", "Jose Luis Nuñez")
    assert names_match("J.L. Nunez", "Jose Luis Nuñez")
    assert not names_match("JM Nuñez", "Jose Luis Nuñez")
    assert not names_match("JL Garcia", "Jose Luis Nuñez")
    assert not names_match("J Nuñez", "Jose Luis Nuñez")
    assert not names_match("J Nuñez", "Jose Nuñez")  # una sola inicial es poco para darlo por el mismo


def test_mismo_piloto_escrito_distinto_junta_compuestos_y_el_lastre_mas_nuevo():
    viejo = _result("2026-09-28T10:00:00+02:00", [_lap("jordi martinez", MAZDA, "S", 83534, ballast=10, ts=99999)])
    nuevo = _result(
        "2026-09-29T10:00:00+02:00",
        [_lap("Jordi Martinez", MAZDA, "M", 84363, ballast=5, ts=1), _lap("Jordi Martinez", MAZDA, "S", 83900, ballast=5, ts=1)],
    )
    block = {"ranking": [{"name": "Jordi Martinez", "car": MAZDA, "best_ms": 83534}]}
    apply_tyre_stats(block, tyre_stats([viejo, nuevo]))
    row = block["ranking"][0]
    assert {k: v["ms"] for k, v in row["by_tyre"].items()} == {"ks_mazda_787b|M": 84363, "ks_mazda_787b|S": 83534}
    assert row["ballast_kg"] == 5


def _sesion_stracker(sid, track, drivers):
    return {
        "session_id": sid,
        "track": track,
        "session_type": "Practice",
        "date_text": "2026-09-29 20:00",
        "drivers": [
            {"id": str(i), "name": n, "car": MAZDA, "laps": [{"time_ms": ms, "valid": True, "splits": [0, 0, 0], "car": MAZDA}]}
            for i, (n, ms) in enumerate(drivers)
        ],
    }


def test_cada_server_su_tabla_aunque_carguen_el_mismo_circuito():
    import json
    import shutil
    from server import AcScanner, live_from_snapshot

    track = "doningtonpark2018-gp"
    folder = Path(__file__).resolve().parent / "_tmp_por_server"
    shutil.rmtree(folder, ignore_errors=True)
    (folder / "ac").mkdir(parents=True)
    (folder / "results").mkdir()
    try:
        uno_ac = _sesion_stracker("10", track, [("Jon Rojo", 84040), ("Salvi Vargas", 83600)])
        dos_ac = _sesion_stracker("20", track, [("Jose Luis Nuñez", 82734)])
        (folder / "ac" / "8771_10.json").write_text(json.dumps(uno_ac), encoding="utf-8")
        (folder / "ac" / "8772_20.json").write_text(json.dumps(dos_ac), encoding="utf-8")
        res1 = _result("2026-09-29T19:00:00+02:00", [_lap("Salvi Vargas", MAZDA, "H", 83600)])
        res2 = _result("2026-09-29T19:00:00+02:00", [_lap("JL Nuñez", MAZDA, "S", 82734), _lap("Salvi Vargas", MAZDA, "W", 90000)])
        (folder / "results" / "8771_x.json").write_text(json.dumps(res1), encoding="utf-8")
        (folder / "results" / "8772_y.json").write_text(json.dumps(res2), encoding="utf-8")
        scanner = AcScanner()
        scanner.cache_dir, scanner.results_dir = folder / "ac", folder / "results"
        scanner.live_by_port["8771"] = live_from_snapshot(_foto_real())
        boards = scanner.build_boards(
            [
                {"id": f"8771:{track}", "track": track, "port": "8771"},
                {"id": f"8772:{track}", "track": track, "port": "8772"},
            ]
        )
        uno, dos = boards[f"8771:{track}"], boards[f"8772:{track}"]
        assert sorted(r["name"] for r in uno["ranking"]) == ["Jon Rojo", "Salvi Vargas"]
        assert [r["name"] for r in dos["ranking"]] == ["Jose Luis Nuñez"]
        tyres = {r["name"]: sorted(r["by_tyre"]) for r in uno["ranking"]}
        assert tyres == {"Jon Rojo": ["ks_mazda_787b|M"], "Salvi Vargas": ["ks_mazda_787b|H"]}  # Jon: sesión sin acabar, sale del directo
        assert sorted(dos["ranking"][0]["by_tyre"]) == ["ks_mazda_787b|S"]
    finally:
        shutil.rmtree(folder, ignore_errors=True)


def test_contador_de_pista_aguanta_un_fallo_suelto_y_cae_a_cero_si_sigue():
    import server
    from server import AcScanner

    base = "http://2.139.243.255:8771/stracker"
    scanner = AcScanner()
    scanner.state["live_tracks"] = [{"id": "8771:doningtonpark2018-gp", "track": "doningtonpark2018-gp", "port": "8771"}]
    reloj = [1000.0]
    resp = {"live": None, "mapa": None}
    scanner.fetch_live = lambda b: resp["live"]
    scanner.fetch_occupancy = lambda b: resp["mapa"]
    real_time = server.time.time
    server.time.time = lambda: reloj[0]

    def cuenta():
        return scanner.state["live"]["connected_drivers"]

    try:
        resp["live"] = {"connected_drivers": 2, "drivers_now": ["A", "B"], "track": "x", "session": {}}
        scanner.refresh_live([base])
        assert cuenta() == 2
        resp["live"] = None  # fallan el directo y el mapa: se mantiene lo último
        reloj[0] += 60
        scanner.refresh_live([base])
        assert cuenta() == 2
        resp["mapa"] = {"connected_drivers": 1, "drivers_now": ["A"]}  # responde el plan B
        scanner.refresh_live([base])
        assert cuenta() == 1
        resp["mapa"] = None
        reloj[0] += 91
        scanner.refresh_live([base])
        assert cuenta() == 0
        resp["live"] = {"connected_drivers": 0, "drivers_now": [], "track": "x", "session": {}}
        resp["mapa"] = {"connected_drivers": 3, "drivers_now": ["A", "B", "C"]}
        scanner.refresh_live([base])
        assert cuenta() == 0  # un cero del propio server es un cero de verdad
    finally:
        server.time.time = real_time


# --- 30-sep: una fila por piloto+coche, compuestos de cada coche y todas las vueltas con rueda y lastre ---


def _stracker_dos_coches(date_text, laps_mazda, laps_porsche):
    def laps(car, items):
        return [{"time_ms": ms, "valid": ok, "splits": [0, 0, 0], "car": car} for ms, ok in items]

    return {
        "track": "doningtonpark2018-gp",
        "session_type": "Practice",
        "date_text": date_text,
        "drivers": [
            {"id": "7", "name": "Jose Luis Nuñez", "car": PORSCHE, "laps": laps(MAZDA, laps_mazda) + laps(PORSCHE, laps_porsche)}
        ],
    }


def _tablero(folder, stracker, results, live=None, captured=None):
    import json
    import shutil
    from server import AcScanner

    shutil.rmtree(folder, ignore_errors=True)
    (folder / "ac").mkdir(parents=True)
    (folder / "results").mkdir()
    for i, s in enumerate(stracker):
        (folder / "ac" / f"8772_{i + 1}.json").write_text(json.dumps(s), encoding="utf-8")
    for i, r in enumerate(results):
        (folder / "results" / f"8772_r{i}.json").write_text(json.dumps(r), encoding="utf-8")
    scanner = AcScanner()
    scanner.cache_dir, scanner.results_dir = folder / "ac", folder / "results"
    if live is not None:
        scanner.live_by_port["8772"] = live
    if captured is not None:
        scanner.captured_by_port["8772"] = captured
    track = "doningtonpark2018-gp"
    return scanner.build_boards([{"id": f"8772:{track}", "track": track, "port": "8772"}])[f"8772:{track}"]


def test_mismo_piloto_con_dos_coches_son_dos_filas_con_sus_compuestos_y_su_lastre():
    import shutil

    folder = Path(__file__).resolve().parent / "_tmp_dos_coches"
    try:
        st = _stracker_dos_coches("2026-09-30 20:00", [(86188, True), (87065, True)], [(92582, True), (97838, True)])
        res = _result(
            "2026-09-30T20:40:00+02:00",
            [
                _lap("JL Nuñez", MAZDA, "M", 86188, ballast=30, ts=1),
                _lap("JL Nuñez", MAZDA, "S", 87065, ballast=30, ts=2),
                _lap("JL Nuñez", PORSCHE, "S", 92582, ballast=0, ts=3),
                _lap("JL Nuñez", PORSCHE, "M", 97838, ballast=0, ts=4),
            ],
        )
        block = _tablero(folder, [st], [res])
        rows = {r["car_label"]: r for r in block["ranking"]}
        assert sorted(rows) == ["Mazda", "Porsche"]
        mazda, porsche = rows["Mazda"], rows["Porsche"]
        assert {k: v["display"] for k, v in mazda["by_tyre"].items()} == {"ks_mazda_787b|M": "1:26.188", "ks_mazda_787b|S": "1:27.065"}
        assert {k: v["display"] for k, v in porsche["by_tyre"].items()} == {"ks_porsche_962c_shorttail|S": "1:32.582", "ks_porsche_962c_shorttail|M": "1:37.838"}
        assert mazda["ballast_kg"] == 30 and porsche["ballast_kg"] == 0  # el lastre de CADA coche, no el último del piloto
        assert mazda["best_ms"] == 86188 and porsche["best_ms"] == 92582
        # cada vuelta de la lista lleva su rueda y su lastre
        vueltas = [(l["ms"], l["tyre"], l["ballast"]) for l in porsche["lap_log"][0]["laps"]]
        assert vueltas == [(92582, "S", 0), (97838, "M", 0)]
    finally:
        shutil.rmtree(folder, ignore_errors=True)


def test_lista_de_vueltas_mas_nueva_primero_y_empareja_por_tiempo_y_fecha():
    from server import tag_lap_log

    log = [
        {"date_iso": "2026-09-29 20:00", "session_type": "Practice", "laps": [{"ms": 90000, "valid": True}, {"ms": 91000, "valid": False}]},
        {"date_iso": "2026-09-30 20:00", "session_type": "Qualify", "laps": [{"ms": 90001, "valid": True}, {"ms": 95000, "valid": True}]},
    ]
    known = [
        {"ms": 90000, "tyre": "S", "ballast": 5, "date": "2026-09-29T20:30:00+02:00"},
        {"ms": 90000, "tyre": "H", "ballast": 10, "date": "2026-09-30T20:30:00+02:00"},
        {"ms": 91001, "tyre": "M", "ballast": 5, "date": "2026-09-29T20:30:00+02:00"},
    ]
    out = tag_lap_log(log, known)
    assert [s["date_iso"] for s in out] == ["2026-09-30 20:00", "2026-09-29 20:00"]
    nuevo, viejo = out
    assert [(l["n"], l["display"], l["tyre"], l["ballast"]) for l in nuevo["laps"]] == [(1, "1:30.001", "H", 10), (2, "1:35.000", None, None)]
    assert [(l["n"], l["tyre"], l["ballast"], l["valid"]) for l in viejo["laps"]] == [(1, "S", 5, True), (2, "M", 5, False)]


def test_lista_de_vueltas_no_se_manda_en_el_estado_sino_aparte():
    from server import AcScanner

    scanner = AcScanner()
    row = {"id": "7|" + MAZDA, "name": "JL", "best_ms": 1, "lap_log": [{"date_iso": "x", "laps": [{"ms": 1}]}]}
    scanner.state["by_track"] = {"8772:t": {"ranking": [row]}}
    scanner.state["ranking"] = [row]
    snap = scanner.snapshot()
    assert "lap_log" not in snap["by_track"]["8772:t"]["ranking"][0]
    assert "lap_log" not in snap["ranking"][0]
    assert scanner.lap_log("8772:t", "7|" + MAZDA) == [{"date_iso": "x", "laps": [{"ms": 1}]}]
    assert scanner.lap_log("8772:t", "7|otro") == [] and scanner.lap_log("no", "7|" + MAZDA) == []


def test_directo_da_marcas_para_guardar_cada_vuelta():
    from server import live_from_snapshot

    marks = live_from_snapshot(_foto_real())["marks"]
    jon = [m for m in marks if m["driver"] == "Jon Rojo"]
    assert jon == [{"driver": "Jon Rojo", "car": MAZDA, "num_laps": 6, "last_ms": 84340, "tyre": "M", "ballast": 0}]


def _marca(driver, car, n, ms, tyre="M", ballast=0):
    return {"driver": driver, "car": car, "num_laps": n, "last_ms": ms, "tyre": tyre, "ballast": ballast}


def _directo(date, marks, track="doningtonpark2018-gp"):
    return {"track": track, "session": {"date": date}, "marks": marks}


def test_se_guarda_cada_vuelta_del_directo_al_subir_el_contador():
    from server import capture_live_laps

    store = {}
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 3, 97000)]))
    assert store["laps"] == []  # la primera vez no se sabe si esa vuelta ya estaba
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 3, 97000)]))
    assert store["laps"] == []  # sin vuelta nueva
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 3, 97000, "M", 20)]))  # en boxes: lastre nuevo
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 4, 97838, "M", 20)]))
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 4, 97838, "S", 0)]))  # en boxes: rueda y lastre
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 5, 999999999, "S")]))  # sin tiempo: no cuenta
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 6, 92582, "S")]))
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 6, 92582, "S"), _marca("JL", MAZDA, 9, 86000, "")]))
    capture_live_laps(store, _directo("A", [_marca("JL", PORSCHE, 6, 92582, "S"), _marca("JL", MAZDA, 10, 86100, "")]))
    # la del Mazda no tiene rueda conocida (no es el coche que lleva ahora): no se inventa
    assert [(l["car"], l["ms"], l["tyre"], l["ballast"]) for l in store["laps"]] == [(PORSCHE, 97838, "M", 20), (PORSCHE, 92582, "S", 0)]
    assert [l["ts"] for l in store["laps"]] == [1, 2]
    capture_live_laps(store, _directo("B", [_marca("JL", PORSCHE, 1, 99000)]))
    assert store["laps"] == []  # sesión nueva: se empieza de cero
    capture_live_laps(store, _directo("B", [_marca("JL", PORSCHE, 2, 99000)], track="otro"))
    assert store["laps"] == []  # otro circuito, también


def test_vuelta_guardada_del_directo_da_el_compuesto_que_la_mejor_no_da():
    import shutil
    from server import capture_live_laps

    folder = Path(__file__).resolve().parent / "_tmp_captura"
    try:
        st = _stracker_dos_coches("2026-09-30 21:00", [], [(92582, True), (97838, True), (98000, False)])
        best_only = {
            "track": "doningtonpark2018-gp",
            "session": {
                "track": "doningtonpark2018-gp",
                "date": "2026-09-30T21:00:00+02:00",
                "type": "live",
                "ballast": {"JL Nuñez": 0},
                "laps": [{"driver": "JL Nuñez", "car": PORSCHE, "tyre": "S", "ballast": 0, "ms": 92582, "cuts": 0, "ts": 0}],
            },
        }
        store = {}
        for n, ms, tyre in ((1, 92582, "S"), (1, 92582, "M"), (2, 97838, "M"), (3, 98000, "M")):
            live = {**best_only, "marks": [_marca("JL Nuñez", PORSCHE, n, ms, tyre)]}
            capture_live_laps(store, live)
        block = _tablero(folder, [st], [], live=best_only, captured=store)
        row = block["ranking"][0]
        assert {k: v["display"] for k, v in row["by_tyre"].items()} == {"ks_porsche_962c_shorttail|S": "1:32.582", "ks_porsche_962c_shorttail|M": "1:37.838"}
        # la sucia no cuenta para el compuesto pero sí sale en la lista con su rueda
        assert [(l["ms"], l["tyre"], l["valid"]) for l in row["lap_log"][0]["laps"]] == [(92582, "S", True), (97838, "M", True), (98000, "M", False)]
        # vueltas de otra sesión guardadas no se cuelan
        viejo = {**store, "key": ["doningtonpark2018-gp", "otra"]}
        block = _tablero(folder, [st], [], live=best_only, captured=viejo)
        assert sorted(block["ranking"][0]["by_tyre"]) == ["ks_porsche_962c_shorttail|S"]
    finally:
        shutil.rmtree(folder, ignore_errors=True)



def test_lastre_por_coche_se_queda_con_el_mas_nuevo_y_cada_vuelta_se_empareja_una_vez():
    viejo = _result("2026-09-28T10:00:00+02:00", [_lap("JL Nuñez", MAZDA, "S", 90000, ballast=50, ts=9999)])
    nuevo = _result(
        "2026-09-30T10:00:00+02:00",
        [_lap("JL Nuñez", MAZDA, "S", 91000, ballast=20, ts=1), _lap("JL Nuñez", PORSCHE, "M", 88000, ballast=0, ts=1)],
    )
    log = [
        {
            "date_iso": "2026-09-30 10:00",
            "session_type": "Practice",
            "laps": [{"ms": 91000, "valid": True}, {"ms": 91000, "valid": True}, {"ms": 88000, "valid": True}],
        }
    ]
    block = {"ranking": [{"name": "Jose Luis Nuñez", "car": MAZDA, "best_ms": 88000, "lap_log": log}]}
    apply_tyre_stats(block, tyre_stats([nuevo, viejo]))
    row = block["ranking"][0]
    assert row["ballast_kg"] == 20
    # una vuelta de los resultados no se pega a dos vueltas; la del Porsche no es de esta fila
    assert [(l["tyre"], l["ballast"]) for l in row["lap_log"][0]["laps"]] == [("S", 20), (None, None), (None, None)]


def test_rueda_del_directo_solo_para_el_coche_que_lleva_puesto():
    from server import live_from_snapshot

    drv = {
        "CarInfo": {"DriverName": "JL", "CarModel": PORSCHE, "Tyres": "M"},
        "Ballast": 0,
        "Cars": {MAZDA: {"NumLaps": 4, "LastLap": 86000 * 1_000_000}, PORSCHE: {"NumLaps": 2, "LastLap": 97838 * 1_000_000}},
    }
    marks = live_from_snapshot({"SessionInfo": {}, "ConnectedDrivers": {"Drivers": {"1": drv}}})["marks"]
    assert {m["car"]: m["tyre"] for m in marks} == {MAZDA: "", PORSCHE: "M"}


def test_sin_vueltas_con_ese_coche_el_lastre_sale_de_la_lista_de_inscritos_mas_nueva():
    viejo = _result("2026-09-28T10:00:00+02:00", [], cars=[("Toni Perez", 40)])
    nuevo = _result("2026-09-30T10:00:00+02:00", [], cars=[("toni perez", 15)])
    block = {"ranking": [{"name": "Toni Perez", "car": MAZDA, "best_ms": 90000}]}
    apply_tyre_stats(block, tyre_stats([viejo, nuevo]))
    assert block["ranking"][0]["ballast_kg"] == 15
