""" Spillerens per-hull-historikk (ADR-072). To lag med tester: 1. Rene enhetstester av summarize_hole_history() -- ingen db, bevis at GIR%/fairway%/gjennomsnitt regnes riktig og hopper over rader uten data. 2. Integrasjonstester mot ekte router-funksjoner (samme mønster som test_tournament_hole_stats.py/test_concurrency_version_check.py) -- beviser selve bane-broen: samme fysiske (teeoff-)bane spilt både som frittstående runde OG i en org-turnering i en ANNEN organisasjon enn spilleren selv eier noe i, skal telles sammen. Custom-baner og gjestespillere skal BEVISST ekskluderes (bruker-bekreftet scope). """ import uuid from datetime import date from app.auth import CurrentUser from app.hole_history import ( HoleInstance, hole_history_for_user, resolve_personal_round_course_key, summarize_hole_history, ) from app.routers.individual_tournaments import ( HoleUpdate as TournamentHoleUpdate, update_hole as tournament_update_hole, ) from app.routers.rounds import HoleUpdate as RoundHoleUpdate, get_hole_history, update_hole as round_update_hole from app.routers.individual_tournaments import get_round_participant_hole_history from tests.conftest import ( add_membership, create_hole, create_org, create_org_hole, create_course, create_participant, create_player, create_round, create_tee, create_tournament, create_tournament_participant, create_tournament_round, create_tournament_round_participant, create_user, ) import app.db as app_db # --------------------------------------------------------------------------- # 1. summarize_hole_history() -- ren funksjon, ingen db. # --------------------------------------------------------------------------- def _instance(**overrides) -> HoleInstance: defaults = dict( source="personal", played_at=date(2026, 1, 1), label="Runde", score=4, par=4, putts=None, tee_shot_result=None, approach_result=None, chip_count=None, bunker_shot_count=None, penalty_strokes=None, first_putt_distance_bucket=None, ) defaults.update(overrides) return HoleInstance(**defaults) def test_summarize_empty_returns_none(): assert summarize_hole_history([]) is None def test_summarize_counts_and_averages(): instances = [ _instance(source="personal", score=4, par=4), _instance(source="tournament", score=6, par=4), ] out = summarize_hole_history(instances) assert out.times_played == 2 assert out.times_personal == 1 assert out.times_tournament == 1 assert out.average_score == 5.0 assert out.average_score_vs_par == 1.0 assert out.best_score == 4 assert out.worst_score == 6 def test_summarize_gir_percent_only_over_rows_with_putts(): instances = [ _instance(score=4, par=4, putts=2), # 4-2=2 <= 4-2=2 -> GIR _instance(score=5, par=4, putts=3), # 5-3=2 <= 2 -> GIR _instance(score=6, par=4, putts=None), # ingen putter registrert -- ekskludert fra GIR% ] out = summarize_hole_history(instances) assert out.gir_percent == 100.0 assert out.average_putts == 2.5 # snitt kun over de to MED putter registrert def test_summarize_gir_percent_none_when_no_putts_data_anywhere(): instances = [_instance(score=4, par=4, putts=None)] out = summarize_hole_history(instances) assert out.gir_percent is None assert out.average_putts is None def test_summarize_fairway_percent_excludes_par_3_and_missing_data(): instances = [ _instance(par=4, tee_shot_result="fairway"), _instance(par=4, tee_shot_result="left"), _instance(par=3, tee_shot_result="fairway"), # par 3 -- ekskludert _instance(par=5, tee_shot_result=None), # ingen utslagsdata -- ekskludert ] out = summarize_hole_history(instances) assert out.fairway_hit_percent == 50.0 # 1 av 2 par-4/5-rader MED data def test_summarize_orders_most_recent_first_and_undated_last(): instances = [ _instance(played_at=date(2026, 1, 1), score=4), _instance(played_at=date(2026, 6, 1), score=5), _instance(played_at=None, score=6), ] out = summarize_hole_history(instances) assert [i.score for i in out.instances] == [5, 4, 6] # --------------------------------------------------------------------------- # 2. Integrasjon: bane-bro + eksklusjoner. # --------------------------------------------------------------------------- async def _score_personal_hole(round_id, participant_id, user_id, hole_number, score, putts=None): user = CurrentUser(user_id=user_id) await round_update_hole( round_id, participant_id, hole_number, RoundHoleUpdate(score=score, putts=putts, expected_version=None), user=user, ) async def _score_tournament_hole(tournament_id, round_id, rp_id, org_id, user_id, hole_number, score, putts=None): user = CurrentUser(user_id=user_id) await tournament_update_hole( tournament_id, round_id, rp_id, hole_number, TournamentHoleUpdate(gross_strokes=score, putts=putts, expected_version=None), organization_id=org_id, user=user, ) async def test_teeoff_bridge_combines_personal_and_tournament_history(pool): """Samme spiller, samme fysiske (teeoff-)bane -- én frittstående runde OG én turneringsrunde i en organisasjon spilleren IKKE eier noe i selv (kun har en player-rad koblet til app_user der). Historikken må vise BEGGE, uansett hvilken side spørringen kommer fra.""" user_id = await create_user() # Frittstående runde -- create_round() sin faste teeoff-bane-fixture # (facility='test-facility', course='test-course'). async with app_db.plain_connection() as conn: round_id = await create_round(conn, user_id) participant_id = await create_participant(conn, round_id, user_id=user_id, is_owner=True) await create_hole(conn, participant_id, hole_number=7, par=4, stroke_index=9) await _score_personal_hole(round_id, participant_id, user_id, hole_number=7, score=4, putts=2) # Org-turnering på en bane importert med SAMME teeoff-identitet. org_id = await create_org() await add_membership(org_id, user_id, role="member") tournament_id = await create_tournament(org_id) course_id = await create_course(org_id) async with app_db.org_connection(org_id) as conn: await conn.execute( "UPDATE course SET source = 'official', external_course_ref = 'test-facility:test-course' WHERE id = $1", course_id, ) for n in range(1, 19): await create_org_hole(org_id, course_id, hole_number=n, par=4 if n != 7 else 4, stroke_index=n) tee_id = await create_tee(org_id, course_id) tournament_round_id = await create_tournament_round(org_id, tournament_id, course_id) player_id = await create_player(org_id, user_id=user_id) tp_id = await create_tournament_participant(org_id, tournament_id, player_id) rp_id = await create_tournament_round_participant(org_id, tournament_round_id, tp_id, tee_id) await _score_tournament_hole(tournament_id, tournament_round_id, rp_id, org_id, user_id, hole_number=7, score=5, putts=1) key = ("teeoff", "test-facility", "test-course") combined = await hole_history_for_user(user_id, key, 7) assert combined.times_played == 2 assert combined.times_personal == 1 assert combined.times_tournament == 1 assert sorted(i.score for i in combined.instances) == [4, 5] # Begge endepunktene skal returnere SAMME kombinerte historikk. from_personal_endpoint = await get_hole_history( round_id, participant_id, 7, user=CurrentUser(user_id=user_id) ) assert from_personal_endpoint.times_played == 2 from_tournament_endpoint = await get_round_participant_hole_history( tournament_id, tournament_round_id, rp_id, 7, organization_id=org_id ) assert from_tournament_endpoint.times_played == 2 async def test_custom_personal_course_is_excluded(pool): """Håndlaget bane (course_source='custom', ingen GolfAPI-kobling) -- resolve_personal_round_course_key skal returnere None, IKKE en snublende match mot noe annet.""" user_id = await create_user() async with app_db.plain_connection() as conn: personal_course_id = str(uuid.uuid4()) await conn.execute( "INSERT INTO personal_course (id, created_by_user_id, name) VALUES ($1, $2, 'Hjemmebanen')", personal_course_id, user_id, ) for n in range(1, 19): await conn.execute( "INSERT INTO personal_course_hole (personal_course_id, hole_number, par, stroke_index) " "VALUES ($1, $2, 4, $3)", personal_course_id, n, n, ) round_id = str(uuid.uuid4()) await conn.execute( """ INSERT INTO round (id, owner_user_id, course_source, personal_course_id, course_name_snapshot, tee_name_snapshot, played_at) VALUES ($1, $2, 'custom', $3, 'Hjemmebanen', 'Gul', CURRENT_DATE) """, round_id, user_id, personal_course_id, ) key = await resolve_personal_round_course_key(conn, round_id) assert key is None async def test_guest_tournament_participant_has_no_history(pool): """player.user_id IS NULL (gjest, aldri registrert/lenket) -- historikk- endepunktet skal svare None (ikke feile), samme "skjul panelet"- kontrakt som en custom bane.""" org_id = await create_org() tournament_id = await create_tournament(org_id) course_id = await create_course(org_id) 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) tournament_round_id = await create_tournament_round(org_id, tournament_id, course_id) # user_id utelates bevisst -- create_player() sin default er NULL. player_id = await create_player(org_id) tp_id = await create_tournament_participant(org_id, tournament_id, player_id) rp_id = await create_tournament_round_participant(org_id, tournament_round_id, tp_id, tee_id) out = await get_round_participant_hole_history( tournament_id, tournament_round_id, rp_id, 1, organization_id=org_id ) assert out is None