""" Visningsmodus for netto-scoring ("Jakter Jesper", migrasjon 092, 2026-08-23). IKKE en ny scoring_method -- sluttallet er matematisk identisk med stroke_net sitt netto-til-par (`net_total - par_played`). Det eneste som faktisk er nytt er den LØPENDE visningen: standard "tjener inn" handicap-slag gradvis per slagindeks etter hvert som hull spilles, mens 'hunt' trekker fra HELE spillehandicapet med én gang og viser brutto-til-par-differansen løpende. Se `_attach_stroke_play_columns` i individual_tournaments.py. """ from fastapi import HTTPException import pytest from app.routers.individual_tournaments import ( HoleUpdate as TournamentHoleUpdate, individual_leaderboard, update_hole as tournament_update_hole, ) from app.routers.tournaments import TournamentUpdate, update_tournament from app.auth import CurrentUser from tests.conftest import ( add_membership, create_org, create_org_hole, create_course, create_player, create_tee, create_tournament, create_tournament_participant, create_tournament_round, create_tournament_round_participant, create_user, ) import app.db as app_db async def _score_holes(tournament_id, round_id, rp_id, org_id, user_id, scores: list[int]) -> None: user = CurrentUser(user_id=user_id) for n, gross in enumerate(scores, start=1): await tournament_update_hole( tournament_id, round_id, rp_id, n, TournamentHoleUpdate(gross_strokes=gross, expected_version=None), organization_id=org_id, user=user, ) async def _setup_stroke_net_round(org_id, method="stroke_net"): tournament_id = await create_tournament(org_id, name="Jakter Jesper-turnering") course_id = await create_course(org_id, name="Hunt Links") for n in range(1, 19): await create_org_hole(org_id, course_id, hole_number=n, par=4, stroke_index=n) tee_id = await create_tee(org_id, course_id) async with app_db.org_connection(org_id) as conn: await conn.execute( "UPDATE tournament SET format_type = 'individual', scoring_method = $2 WHERE id = $1", tournament_id, method, ) round_id = await create_tournament_round(org_id, tournament_id, course_id, sequence=1) return tournament_id, course_id, tee_id, round_id async def _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, name, playing_handicap): player_id = await create_player(org_id, display_name=name) tp_id = await create_tournament_participant(org_id, tournament_id, player_id) rp_id = await create_tournament_round_participant(org_id, round_id, tp_id, tee_id) async with app_db.org_connection(org_id) as conn: await conn.execute( "UPDATE tournament_round_participant SET course_handicap = $1, playing_handicap = $1 WHERE id = $2", playing_handicap, rp_id, ) return tp_id, rp_id async def test_hunt_differs_mid_round_but_converges_at_completion(pool): org_id = await create_org() owner_id = await create_user() await add_membership(org_id, owner_id, role="owner") tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id) p1, rp1 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P1", 12) # Hull 1, par -- stroke_index 1 (<=12) -> mottar 1 slag under standard. await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4]) entries = await individual_leaderboard(tournament_id, organization_id=org_id) by_id = {e.tournament_participant_id: e for e in entries} # Standard (default): netto = 4-1=3, til par = 3-4 = -1 -- slaget er # kun DELVIS "tjent inn" ennå (1 av 12 spilte hull). assert by_id[p1].total_label == "-1" await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id) entries = await individual_leaderboard(tournament_id, organization_id=org_id) by_id = {e.tournament_participant_id: e for e in entries} # Hunt: gross(4) - FULLT spillehandicap(12) = -8, til par = -8-4 = -12 # -- hele handicapet trukket fra med én gang, mye "dypere" tall. assert by_id[p1].total_label == "-12" # Fullfør runden med par på alle resterende hull. await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4] * 17) entries = await individual_leaderboard(tournament_id, organization_id=org_id) by_id = {e.tournament_participant_id: e for e in entries} # Fortsatt -12 under hunt -- flatt hele veien når hvert hull er par. assert by_id[p1].total_label == "-12" # Bytt tilbake til standard nå som runden er fullført -- KONVERGERER # til nøyaktig samme tall som hunt (selve beviset på ekvivalensen). await update_tournament(tournament_id, TournamentUpdate(net_display_style="standard"), organization_id=org_id) entries = await individual_leaderboard(tournament_id, organization_id=org_id) by_id = {e.tournament_participant_id: e for e in entries} assert by_id[p1].total_label == "-12" async def test_hunt_display_style_rejected_for_non_stroke_net(pool): org_id = await create_org() owner_id = await create_user() await add_membership(org_id, owner_id, role="owner") tournament_id = await create_tournament(org_id) async with app_db.org_connection(org_id) as conn: await conn.execute( "UPDATE tournament SET format_type = 'individual', scoring_method = 'stableford' WHERE id = $1", tournament_id, ) with pytest.raises(HTTPException) as exc_info: await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id) assert exc_info.value.status_code == 400 async def test_switching_away_from_stroke_net_rejected_while_hunt_active(pool): org_id = await create_org() owner_id = await create_user() await add_membership(org_id, owner_id, role="owner") tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id) await update_tournament(tournament_id, TournamentUpdate(net_display_style="hunt"), organization_id=org_id) with pytest.raises(HTTPException) as exc_info: await update_tournament( tournament_id, TournamentUpdate(scoring_method="stableford"), organization_id=org_id ) assert exc_info.value.status_code == 400 # Men lov når begge felt endres sammen i samme kall. out = await update_tournament( tournament_id, TournamentUpdate(scoring_method="stableford", net_display_style="standard"), organization_id=org_id, ) assert out.scoring_method == "stableford" assert out.net_display_style == "standard" async def test_lowest_playing_hcp_tiebreak_resolves_field_tie(pool): """Speiler `test_lowest_hcp_resolves_field_tie_but_not_winner` (test_tiebreak.py) sitt oppsett nøyaktig, men med `stroke_gross` (handicap påvirker IKKE selve rangeringen der -- kun tiebreaken -- slik at identiske bruttoscorer faktisk BLIR en reell uavgjort å løse). `stroke_net` ville gjort dette meningsløst: ulikt spillehandicap med identisk brutto gir ulik netto, altså ingen uavgjort i det hele tatt å teste tiebreaken på.""" org_id = await create_org() owner_id = await create_user() await add_membership(org_id, owner_id, role="owner") tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id, method="stroke_gross") await update_tournament( tournament_id, TournamentUpdate(tiebreak_winner_method="none", tiebreak_field_method="lowest_playing_hcp"), organization_id=org_id, ) leader, rp_leader = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "Leader", 5) p2, rp2 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P2", 8) p3, rp3 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P3", 20) await _score_holes(tournament_id, round_id, rp_leader, org_id, owner_id, [3] * 18) # klar leder await _score_holes(tournament_id, round_id, rp2, org_id, owner_id, [4] * 18) # uavgjort brutto med p3 await _score_holes(tournament_id, round_id, rp3, org_id, owner_id, [4] * 18) # uavgjort brutto med p2 entries = await individual_leaderboard(tournament_id, organization_id=org_id) by_id = {e.tournament_participant_id: e for e in entries} assert by_id[leader].position == "1" assert by_id[p2].position == "2" # lavere spillehandicap (8 < 20) -> foran assert by_id[p3].position == "3" async def test_lowest_playing_hcp_missing_value_sorts_last(pool): """Speiler `lowest_hcp` sin regel: manglende data kan ALDRI vinne en tiebreak ved å mangle -- sorteres sist. `stroke_gross`, samme begrunnelse som testen over -- spillehandicap er ikke påkrevd for brutto-scoring (`_SCORING_METHODS_REQUIRING_HANDICAP` gjelder ikke her), så et reelt NULL-scenario for tiebreak-metrikken kan faktisk oppstå (ulikt stroke_net, der manglende handicap gjør at netto- tallet aldri blir beregnet i det hele tatt -- deltakeren havner da helt utenfor rangeringen, ikke i en tiebreak-gruppe).""" org_id = await create_org() owner_id = await create_user() await add_membership(org_id, owner_id, role="owner") tournament_id, course_id, tee_id, round_id = await _setup_stroke_net_round(org_id, method="stroke_gross") # Kun to deltakere, begge uavgjort for 1.-plass -- tiebreak_winner_method # er den relevante innstillingen her (rank==1), ikke feltmetoden. await update_tournament( tournament_id, TournamentUpdate(tiebreak_winner_method="lowest_playing_hcp", tiebreak_field_method="none"), organization_id=org_id, ) p1, rp1 = await _add_participant_with_playing_hcp(org_id, tournament_id, round_id, tee_id, "P1", 10) # P2 -- ingen spillehandicap beregnet (NULL, samme som "aldri kjørt # gjennom _compute_round_participant_handicap"). player2 = await create_player(org_id, display_name="P2") p2 = await create_tournament_participant(org_id, tournament_id, player2) rp2 = await create_tournament_round_participant(org_id, round_id, p2, tee_id) await _score_holes(tournament_id, round_id, rp1, org_id, owner_id, [4] * 18) await _score_holes(tournament_id, round_id, rp2, org_id, owner_id, [4] * 18) entries = await individual_leaderboard(tournament_id, organization_id=org_id) by_id = {e.tournament_participant_id: e for e in entries} assert by_id[p1].position == "1" # har en verdi -> vinner uavgjortheten assert by_id[p2].position == "2" # mangler verdi -> sist