"""
Tester for TeeCup handicap-motor.

Fasitverdiene er hentet fra R&A Rules of Handicapping, Appendix C, der det er
mulig, slik at motoren kan verifiseres mot en autoritativ kilde uavhengig av
resten av systemet (ADR-005).

Kjør:  python -m pytest test_handicap_engine.py -v
   ev.  python test_handicap_engine.py   (kjører en enkel selvsjekk uten pytest)
"""

from datetime import date

from handicap_engine import (
    Format,
    HoleResult,
    Player,
    PerPlayerPercentage,
    CombinedPercentage,
    WeightedLowHigh,
    RankedSplit,
    TeamAverage,
    DEFAULT_MATCHPLAY_ALLOWANCES,
    adjusted_gross_score,
    allocate_strokes_by_index,
    allocate_over_played_holes,
    apply_index_caps,
    compute_match_state,
    course_handicap,
    course_handicap_9,
    course_handicap_9_raw,
    course_handicap_raw,
    handicap_index_from_differentials,
    low_handicap_index,
    match_play_strokes,
    net_stroke_play_margin,
    compute_skins,
    compute_skins_detail,
    copenhagen_points_for_hole,
    compute_copenhagen,
    compute_copenhagen_detail,
    bbb_points_for_hole,
    compute_bbb,
    FlagResult,
    flag_result,
    flag_lap_and_hole,
    EclecticHoleValue,
    EclecticHolePick,
    eclectic_best_per_hole,
    shamble_hole_score,
    money_ball_hole_score,
    high_low_high_points_for_hole,
    high_low_high_running_score,
    order_of_merit_points_for_position,
    order_of_merit_aggregate,
    max_hole_score_for_handicap,
    net_par,
    round_counts_for_handicap,
    round_half_up,
    round_half_up_decimal,
    score_differential,
    stableford_points_for_hole,
    stableford_total,
    stroke_play_gross_total,
    stroke_play_net_total,
    unit_playing_handicap,
)


# ---------------------------------------------------------------------------
# Avrunding
# ---------------------------------------------------------------------------

def test_round_half_up_positive():
    assert round_half_up(16.2) == 16
    assert round_half_up(15.3) == 15
    assert round_half_up(26.1) == 26
    assert round_half_up(0.5) == 1        # 0,5 alltid opp (ikke banker's)
    assert round_half_up(2.5) == 3
    assert round_half_up(1.5) == 2


def test_round_half_up_negative():
    # Minus-handicap (plusspillere)
    assert round_half_up(-2.5) == -2
    assert round_half_up(-0.5) == 0


# ---------------------------------------------------------------------------
# Course Handicap
# ---------------------------------------------------------------------------

def test_course_handicap_formula():
    # Index 18.0, Slope 113 (nøytral), CR == Par -> nøyaktig 18
    assert course_handicap_raw(18.0, 113, 72.0, 72) == 18.0
    # Slope 130, CR 71.5, Par 72
    raw = course_handicap_raw(10.0, 130, 71.5, 72)
    assert abs(raw - (10.0 * 130 / 113 + (71.5 - 72))) < 1e-9
    assert course_handicap(10.0, 130, 71.5, 72) == round_half_up(raw)


# ---------------------------------------------------------------------------
# Singles match play (R&A Appendix C, Eksempel 2): 100 %
#   A spiller av 0, B mottar 8 slag.
# ---------------------------------------------------------------------------

def test_singles_match_play_appendix_c_example_2():
    strat = DEFAULT_MATCHPLAY_ALLOWANCES[Format.SINGLES]
    # To spillere med course handicap som skiller 8 (100 % allowance)
    a = Player("A", 8.0, 113, 72.0, 72)   # CH 8
    b = Player("B", 16.0, 113, 72.0, 72)  # CH 16
    ph_a = unit_playing_handicap([a], strat)
    ph_b = unit_playing_handicap([b], strat)
    strokes = match_play_strokes([ph_a, ph_b])
    assert strokes == [0, 8]


# ---------------------------------------------------------------------------
# Four-ball match play (R&A Appendix C, Eksempel 3): 90 % per spiller
#   Course handicaps 10 / 18 / 27 / 39  ->  0 / 7 / 15 / 26
# ---------------------------------------------------------------------------

def test_fourball_match_play_appendix_c_example_3():
    strat = DEFAULT_MATCHPLAY_ALLOWANCES[Format.FOURBALL]
    chs = [10, 18, 27, 39]
    players = [Player(f"P{i}", ch, 113, 72.0, 72) for i, ch in enumerate(chs)]
    phs = [unit_playing_handicap([p], strat) for p in players]
    # 90 % avrundet: 9, 16, 24, 35
    assert phs == [9, 16, 24, 35]
    strokes = match_play_strokes(phs)
    assert strokes == [0, 7, 15, 26]


# ---------------------------------------------------------------------------
# Foursomes match play (R&A Appendix C, Eksempel 4): 50 % av differansen
#   mellom lagenes samlede course handicap. Team 2 mottar 19.
#   -> lagenes samlede CH skiller 38.
# ---------------------------------------------------------------------------

def test_foursome_match_play_appendix_c_example_4():
    strat = DEFAULT_MATCHPLAY_ALLOWANCES[Format.FOURSOME]
    # Team 1 samlet CH = 20, Team 2 samlet CH = 58 -> differanse 38
    team1 = [Player("A", 8.0, 113, 72.0, 72), Player("B", 12.0, 113, 72.0, 72)]   # sum 20
    team2 = [Player("C", 28.0, 113, 72.0, 72), Player("D", 30.0, 113, 72.0, 72)]  # sum 58
    ph1 = unit_playing_handicap(team1, strat)  # 50 % av 20 = 10
    ph2 = unit_playing_handicap(team2, strat)  # 50 % av 58 = 29
    assert ph1 == 10 and ph2 == 29
    strokes = match_play_strokes([ph1, ph2])
    assert strokes == [0, 19]


# ---------------------------------------------------------------------------
# Greensomes: 60 % laveste + 40 % høyeste
# ---------------------------------------------------------------------------

def test_greensome_weighted_allowance():
    strat = WeightedLowHigh(0.60, 0.40)
    # CH 12 og 20 -> 0,6*12 + 0,4*20 = 7,2 + 8,0 = 15,2 -> 15
    p_low = Player("L", 12.0, 113, 72.0, 72)
    p_high = Player("H", 20.0, 113, 72.0, 72)
    assert unit_playing_handicap([p_low, p_high], strat) == 15
    # rekkefølge skal ikke spille noen rolle
    assert unit_playing_handicap([p_high, p_low], strat) == 15


# ---------------------------------------------------------------------------
# Scramble: rangert splitt
# ---------------------------------------------------------------------------

def test_scramble_4_ranked_split():
    strat = RankedSplit((0.25, 0.20, 0.15, 0.10))
    # CH 4, 10, 16, 24 -> 0,25*4 + 0,20*10 + 0,15*16 + 0,10*24
    #                   = 1,0 + 2,0 + 2,4 + 2,4 = 7,8 -> 8
    players = [Player(f"P{i}", ch, 113, 72.0, 72) for i, ch in enumerate([24, 4, 16, 10])]
    assert unit_playing_handicap(players, strat) == 8  # rekkefølge irrelevant


def test_scramble_2_ranked_split():
    strat = RankedSplit((0.35, 0.15))
    # CH 6 og 18 -> 0,35*6 + 0,15*18 = 2,1 + 2,7 = 4,8 -> 5
    players = [Player("A", 18.0, 113, 72.0, 72), Player("B", 6.0, 113, 72.0, 72)]
    assert unit_playing_handicap(players, strat) == 5


def test_team_average_two_players():
    # CH 6 og 18 -> (6+18)/2 = 12
    players = [Player("A", 18.0, 113, 72.0, 72), Player("B", 6.0, 113, 72.0, 72)]
    assert unit_playing_handicap(players, TeamAverage()) == 12


def test_team_average_three_players_rounds_half_up():
    # CH 8, 14, 20 -> (8+14+20)/3 = 14,0 -> 14
    players = [Player(f"P{i}", ch, 113, 72.0, 72) for i, ch in enumerate([8, 14, 20])]
    assert unit_playing_handicap(players, TeamAverage()) == 14


def test_team_average_five_players_rounds_up_at_half():
    # CH 5, 6, 7, 8, 9 -> sum 35 / 5 = 7,0 -> 7. Rekkefølge irrelevant.
    players = [Player(f"P{i}", ch, 113, 72.0, 72) for i, ch in enumerate([9, 5, 8, 6, 7])]
    assert unit_playing_handicap(players, TeamAverage()) == 7


def test_team_average_rounding_half_up_boundary():
    # CH 4 og 5 -> 4,5 -> 5 (halvveis alltid opp, samme regel som round_half_up)
    players = [Player("A", 4.0, 113, 72.0, 72), Player("B", 5.0, 113, 72.0, 72)]
    assert unit_playing_handicap(players, TeamAverage()) == 5


def test_team_average_rejects_single_player():
    try:
        TeamAverage().playing_handicap([10.0])
    except ValueError:
        pass
    else:
        raise AssertionError("TeamAverage skulle avvist én spiller")


def test_allowance_is_configurable_not_hardcoded():
    """ADR-005: motoren skal godta en overstyrt allowance (f.eks. 75 %/3/4)."""
    strat_75 = PerPlayerPercentage(0.75)
    p = Player("X", 20.0, 113, 72.0, 72)  # CH 20
    assert unit_playing_handicap([p], strat_75) == 15  # 0,75*20


# ---------------------------------------------------------------------------
# Slagfordeling på Stroke Index
# ---------------------------------------------------------------------------

def test_allocate_strokes_basic():
    si = list(range(1, 19))  # SI 1..18
    # 5 slag -> ett slag på SI 1..5, null ellers
    alloc = allocate_strokes_by_index(5, si)
    assert sum(alloc) == 5
    assert alloc[0] == 1 and alloc[4] == 1 and alloc[5] == 0


def test_allocate_strokes_high_handicap_double():
    si = list(range(1, 19))
    # 20 slag -> alle hull minst 1, SI 1 og 2 får 2
    alloc = allocate_strokes_by_index(20, si)
    assert sum(alloc) == 20
    assert alloc[0] == 2 and alloc[1] == 2 and alloc[2] == 1


def test_allocate_strokes_zero():
    si = list(range(1, 19))
    assert allocate_strokes_by_index(0, si) == [0] * 18


def test_allocate_strokes_plus_handicap_gives_back():
    si = list(range(1, 19))
    # -2 slag: gir tilbake på de to letteste hullene (SI 18 og 17)
    alloc = allocate_strokes_by_index(-2, si)
    assert sum(alloc) == -2
    # SI 18 er indeks 17, SI 17 er indeks 16
    assert alloc[17] == -1 and alloc[16] == -1
    assert alloc[0] == 0


def test_allocate_strokes_respects_scorecard_order():
    # Hullenes SI i kortrekkefølge (ikke sortert): fordelingen skal følge SI-verdien
    si = [5, 1, 12, 3, 18, 7, 9, 11, 15, 2, 4, 6, 8, 10, 13, 14, 16, 17]
    alloc = allocate_strokes_by_index(3, si)
    assert sum(alloc) == 3
    # Slag skal ligge på hull med SI 1, 2, 3
    for hole_si, strokes in zip(si, alloc):
        assert strokes == (1 if hole_si <= 3 else 0)


# ---------------------------------------------------------------------------
# 9-hulls-fordeling (front/back) — "slagene faller på 18-hulls-kortet"
# ---------------------------------------------------------------------------

# Standard 18-hulls stroke index i hullrekkefølge: hull 1 har SI 1, hull 2 SI 3, ...
# Odde SI på front-9, par SI på back-9.
_FRONT_ODD_SI = [1, 3, 5, 7, 9, 11, 13, 15, 17]
_BACK_EVEN_SI = [2, 4, 6, 8, 10, 12, 14, 16, 18]
_ALL18_SI = _FRONT_ODD_SI + _BACK_EVEN_SI          # hull 1..18
_FRONT_HOLES = list(range(1, 10))                  # hull 1..9
_BACK_HOLES = list(range(10, 19))                  # hull 10..18


def test_nine_hole_three_strokes_back_vs_front():
    # 3 mottatte slag: faller på SI 1, 2, 3.
    back = allocate_over_played_holes(3, _ALL18_SI, _BACK_HOLES)
    front = allocate_over_played_holes(3, _ALL18_SI, _FRONT_HOLES)
    assert sum(back) == 1    # kun SI 2 på back-9
    assert sum(front) == 2   # SI 1 og 3 på front-9


def test_nine_hole_twelve_strokes_is_six_not_ten():
    # Kjernetesten: 12 slag på back-9 skal bli 6, ikke 10 (den naive feilen).
    back = allocate_over_played_holes(12, _ALL18_SI, _BACK_HOLES)
    assert sum(back) == 6
    # SI 2,4,6,8,10,12 får slag; SI 14,16,18 får ikke.
    assert back == [1, 1, 1, 1, 1, 1, 0, 0, 0]


def test_nine_hole_matches_naive_only_when_low():
    # Metodene sammenfaller så lenge totalen ikke overstiger antall spilte hull
    # (her 9): opp til 8 er base-slaget i den naive varianten fortsatt 0.
    for total in range(0, 9):
        correct = sum(allocate_over_played_holes(total, _ALL18_SI, _BACK_HOLES))
        naive = sum(allocate_strokes_by_index(total, _BACK_EVEN_SI))
        assert correct == naive
    # Fra og med 9 spriker de:
    assert sum(allocate_over_played_holes(12, _ALL18_SI, _BACK_HOLES)) \
        != sum(allocate_strokes_by_index(12, _BACK_EVEN_SI))


# ---------------------------------------------------------------------------
# Match-status
# ---------------------------------------------------------------------------

def test_match_state_all_square():
    results = [HoleResult.SIDE_A, HoleResult.SIDE_B, HoleResult.HALVED]
    state = compute_match_state(results, total_holes=18)
    assert state.lead == 0
    assert state.describe() == "AS"


def test_match_state_two_up():
    results = [HoleResult.SIDE_A, HoleResult.SIDE_A, HoleResult.HALVED]
    state = compute_match_state(results, total_holes=18)
    assert state.lead == 2
    assert state.describe() == "2 UP (A)"


def test_match_state_dormie():
    # A leder med 2, og det gjenstår nøyaktig 2 hull
    results = [HoleResult.SIDE_A] * 2 + [HoleResult.HALVED] * 14
    state = compute_match_state(results, total_holes=18)
    assert state.holes_remaining == 2
    assert state.is_dormie is True
    assert state.describe() == "dormie 2 (A)"


def test_match_state_closed_3_and_2():
    # A leder med 3 etter 16 hull -> 2 gjenstår -> avgjort "3&2"
    results = [HoleResult.SIDE_A] * 3 + [HoleResult.HALVED] * 13
    state = compute_match_state(results, total_holes=18)
    assert state.is_closed is True
    assert state.describe() == "3&2 (A)"


def test_match_state_won_on_last_hole():
    # A leder med 1 etter 18 hull -> vunnet "1 UP"
    results = [HoleResult.SIDE_A] + [HoleResult.HALVED] * 17
    state = compute_match_state(results, total_holes=18)
    assert state.holes_remaining == 0
    assert state.describe() == "1 UP (A)"


def test_match_state_side_b_leads():
    results = [HoleResult.SIDE_B, HoleResult.SIDE_B, HoleResult.SIDE_A]
    state = compute_match_state(results, total_holes=18)
    assert state.lead == -1
    assert state.describe() == "1 UP (B)"


# ---------------------------------------------------------------------------
# Netto-slagspill-sammenligning mellom to sider (scramble_solo) -- SØSKEN
# til match-status over, ikke en del av den: ren totalsum-sammenligning,
# ingen hull-for-hull-tilstand.
# ---------------------------------------------------------------------------

def test_net_stroke_play_margin_a_wins():
    # A har lavest nettosum -> A leder.
    assert net_stroke_play_margin(net_total_a=68, net_total_b=72) == 4


def test_net_stroke_play_margin_b_wins():
    assert net_stroke_play_margin(net_total_a=75, net_total_b=70) == -5


def test_net_stroke_play_margin_tie():
    assert net_stroke_play_margin(net_total_a=71, net_total_b=71) == 0


# ---------------------------------------------------------------------------
# HCP-indeksberegning fra spilte runder (ADR-033)
#
# Fasitverdiene under er, der mulig, hentet direkte fra egne diagrammer og
# tallregneeksempler i "WHS Rules of Handicapping" (effektiv januar 2024,
# USGA/R&A) — samme prinsipp som resten av filen (ADR-005): verifiser mot en
# autoritativ kilde, ikke bare intern konsistens.
# ---------------------------------------------------------------------------

def test_round_half_up_decimal_rule_5_1c_examples():
    # Rule 5.1c sine tre eksplisitte eksempler, inkl. negative verdier.
    assert round_half_up_decimal(-1.54, 1) == -1.5
    assert round_half_up_decimal(-1.55, 1) == -1.5
    assert round_half_up_decimal(-1.56, 1) == -1.6
    assert round_half_up_decimal(15.25, 1) == 15.3


def test_net_par():
    # Net Par (Rule 3.2b/2) -- par + mottatte handicapslag.
    assert net_par(par=4, strokes_received=1) == 5
    assert net_par(par=4, strokes_received=0) == 4


def test_max_hole_score_for_handicap():
    # Net Double Bogey (Rule 3.1b): par + 2 + mottatte slag.
    assert max_hole_score_for_handicap(4, 1) == 7
    # Diagram 3.1b sitt eksempel: par 4, 1 mottatt slag -> maks 7.
    assert max_hole_score_for_handicap(par=4, strokes_received=1) == 7
    # Før indeks etablert (Rule 3.1a): par + 5, uavhengig av slag.
    assert max_hole_score_for_handicap(5, strokes_received=0, index_established=False) == 10


def test_max_hole_score_for_handicap_high_course_handicap_exception_rule_3_1b():
    # Rule 3.1b, ordrett sitert fra WHS Rules of Handicapping 2024, side 37
    # (verifisert direkte mot PDF-en, ikke bare kodens egen gjenfortelling):
    # "Where a Course Handicap is calculated at more than 54 and a player
    # receives 4 or more strokes on a hole, the maximum hole score is
    # par + 5 for handicap purposes." Dette OVERSTYRER den vanlige
    # par+2+slag-cappen -- funnet som et reelt hull i motoren (var verken
    # implementert, testet, eller nevnt i noen ADR før denne testen).

    # Unntaket slår inn: banehandicap 55 (> 54) OG 4 mottatte slag ->
    # par + 5, IKKE par + 2 + 4 (som ville gitt 10).
    assert max_hole_score_for_handicap(4, strokes_received=4, course_handicap=55) == 9

    # Grense A -- eksakt 54 er IKKE "mer enn 54": vanlig cap gjelder fortsatt.
    assert max_hole_score_for_handicap(4, strokes_received=4, course_handicap=54) == 4 + 2 + 4

    # Grense B -- 3 mottatte slag er IKKE "4 eller flere", selv med høyt
    # banehandicap: vanlig cap gjelder fortsatt.
    assert max_hole_score_for_handicap(4, strokes_received=3, course_handicap=60) == 4 + 2 + 3

    # Flere mottatte slag enn 4 (f.eks. et par-3-hull med 5+ slag ved svært
    # høyt banehandicap) rammes også -- fortsatt par + 5, ikke par+2+5.
    assert max_hole_score_for_handicap(3, strokes_received=6, course_handicap=70) == 8

    # course_handicap ikke oppgitt (standard None) -- uendret oppførsel fra
    # FØR denne fiksen, selv med 4+ slag. Bakoverkompatibilitet for alle
    # eksisterende kallsteder som ikke (ennå) sender parameteren.
    assert max_hole_score_for_handicap(4, strokes_received=4) == 4 + 2 + 4


def test_adjusted_gross_score_applies_high_course_handicap_exception():
    # Samme unntak (Rule 3.1b) verifisert på hele adjusted_gross_score-veien
    # (course_handicap videreført til max_hole_score_for_handicap per hull),
    # ikke bare på selve cap-funksjonen isolert.
    pars = [4] * 18
    stroke_index = list(range(1, 19))
    strokes_received = allocate_strokes_by_index(55, stroke_index)  # banehandicap 55
    # 55 = 3*18 + 1 (base=3, extra=1) -- KUN hull med SI 1 mottar 4 slag,
    # alle 17 andre hull mottar 3 slag hver (se allocate_strokes_by_index).
    assert strokes_received[0] == 4  # SI 1 -- det ENESTE hullet med 4+ slag
    assert strokes_received[9] == 3  # SI 10 -- representativt for resten
    assert strokes_received.count(4) == 1 and strokes_received.count(3) == 17
    scores = [12] * 18  # høyt nok til å treffe enhver rimelig cap på hvert hull

    ags_with_hcp = adjusted_gross_score(scores, pars, strokes_received, course_handicap=55)
    ags_without_hcp = adjusted_gross_score(scores, pars, strokes_received)
    # Med course_handicap=55: hullet med 4 slag (SI 1) capped til par+5=9
    # via unntaket. De 17 andre hullene (3 slag) capped til par+2+3=9 --
    # samme tall, men via den VANLIGE regelen, uendret av unntaket.
    assert ags_with_hcp == 1 * 9 + 17 * 9  # = 162
    # Uten course_handicap (gammel oppførsel, unntaket slår aldri inn):
    # SI 1-hullet capped til par+2+4=10 i stedet for 9 -- de 17 andre
    # hullene er uendret (unntaket gjaldt dem uansett aldri, siden de har
    # under 4 mottatte slag).
    assert ags_without_hcp == 1 * 10 + 17 * 9  # = 163
    assert ags_with_hcp == ags_without_hcp - 1


def test_adjusted_gross_score_diagram_3_1b_worked_example():
    # John Smith, HCP 16, Diagram 3.1b. Front-9 (par/SI/score) er lest
    # tydelig og eksakt fra diagrammet (Out = 35 par / 43 gross, begge
    # verifisert ved summering) -- brukt uendret her.
    #
    # Back-9: par/SI-radene er like eksakte (In = 35 par, verifisert), MEN
    # selve score-tallrekken i bilde-utsnittet er for utydelig til å stole
    # blindt på hver enkelt siffer -- summen min (46) stemte ikke med
    # diagrammets oppgitte "In 45". Eneste back-9-tallet jeg er HELT sikker
    # på (egen, tydelig uthevet boks i diagrammet, med piler): hull 17
    # (par 4, SI 6, mottar 1 slag ved HCP 16) hadde bruttoscore 9, capped
    # til Net Double Bogey 7. De ANDRE åtte back-9-scorene under er derfor
    # egenkomponerte (ikke hentet fra diagrammet) -- valgt lavt nok til at
    # ingen av dem selv trigger en cap, slik at testen isolert kan
    # verifisere nøyaktig den ene, kildebelagte capping-hendelsen.
    front9_pars = [4, 4, 3, 4, 5, 4, 3, 4, 4]
    front9_si = [7, 13, 3, 15, 11, 1, 17, 5, 9]
    front9_scores = [5, 5, 6, 4, 5, 5, 3, 5, 5]
    assert sum(front9_pars) == 35 and sum(front9_scores) == 43  # Out, begge fra diagrammet

    back9_pars = [3, 4, 5, 3, 4, 5, 3, 4, 4]
    back9_si = [18, 12, 4, 14, 8, 2, 16, 6, 10]
    assert sum(back9_pars) == 35  # In (par), fra diagrammet
    # Egenkomponerte back-9-scorer (se kommentar over) -- hull 17 (indeks 7,
    # par 4, SI 6) er det ENESTE kildebelagte tallet i denne rekken: gross 9.
    back9_scores = [3, 3, 3, 3, 3, 3, 3, 9, 3]  # index 7 = hull 17

    pars = front9_pars + back9_pars
    stroke_index = front9_si + back9_si
    scores = front9_scores + back9_scores
    strokes_received = allocate_strokes_by_index(16, stroke_index)  # HCP 16

    hole17_index = len(front9_pars) + back9_si.index(6)  # hole 17 = 0-indexert 16
    assert hole17_index == 16
    assert pars[hole17_index] == 4 and stroke_index[hole17_index] == 6
    assert strokes_received[hole17_index] == 1  # SI 6 <= 16 -> mottar slag
    assert max_hole_score_for_handicap(4, 1) == 7  # Net Double Bogey, diagrammets "Max 7"

    ags = adjusted_gross_score(scores, pars, strokes_received)
    # Eneste hull som overskrider sin cap er hull 17 (gross 9 -> capped 7):
    # adjusted = gross_total - (9 - 7).
    assert ags == sum(scores) - 2


def test_adjusted_gross_score_unplayed_holes_use_net_par():
    # 9 spilte hull (front) + 9 uspilte (None) -- uspilte fylles med Net Par
    # (TeeCups kildebelagte erstatning for WHS sin upubliserte Expected Score,
    # se ADR-033 og moduldoc i handicap_engine.py).
    pars = [4] * 18
    stroke_index = list(range(1, 19))
    strokes_received = allocate_strokes_by_index(9, stroke_index)  # 9 slag -> 1 på SI 1-9
    scores = [4] * 9 + [None] * 9
    ags = adjusted_gross_score(scores, pars, strokes_received)
    # Spilte 9: par 4, ingen over cap -> 9*4 = 36.
    # Uspilte 9 (SI 10-18, 0 mottatte slag hver) -> Net Par = par + 0 = 4 hver -> 36.
    assert ags == 72


def test_score_differential_formula():
    # Egen tallsjekk mot selve formelen (Rule 5.1a), ingen offisiell
    # diagram-fasit for akkurat denne kombinasjonen -- verifiserer
    # utregningen, ikke bare at funksjonen kjører.
    diff = score_differential(adjusted_gross_score_value=90, course_rating=71.5, slope_rating=128)
    expected = round_half_up_decimal((113 / 128) * (90 - 71.5), 1)
    assert diff == expected == 16.3


def test_handicap_index_initial_three_scores_rule_5_2a_example_1():
    # Rule 5.2a klargjøring: tre differensialer 15,3 / 15,2 / 16,6
    # -> laveste 1 (15,2), justering -2,0 -> initial indeks 13,2.
    idx = handicap_index_from_differentials([15.3, 15.2, 16.6])
    assert idx == 13.2


def test_handicap_index_initial_three_scores_rule_5_2a_example_2():
    # Rule 5.2a klargjøring, andre eksempel: 40,7 / 42,4 / 36,1
    # -> laveste 1 (36,1), justering -2,0 -> initial indeks 34,1.
    idx = handicap_index_from_differentials([40.7, 42.4, 36.1])
    assert idx == 34.1


def test_handicap_index_six_scores_rule_5_2a_example_2_continued():
    # Samme klargjøring, spilleren legger til tre nye: 45,9 / 43,6 / 45,0.
    # Seks totalt -> snitt av laveste 2 (36,1 og 40,7 = 38,4), justering
    # -1,0 -> indeks 37,4.
    idx = handicap_index_from_differentials([40.7, 42.4, 36.1, 45.9, 43.6, 45.0])
    assert idx == 37.4


def test_handicap_index_fewer_than_three_returns_none():
    assert handicap_index_from_differentials([]) is None
    assert handicap_index_from_differentials([20.0, 21.0]) is None


def test_handicap_index_twenty_scores_uses_lowest_eight():
    # Rule 5.2b: 20 differensialer -> snitt av laveste 8, ingen justering.
    diffs = [float(v) for v in range(1, 21)]  # 1..20
    idx = handicap_index_from_differentials(diffs)
    # Laveste 8: 1..8 -> snitt 4,5
    assert idx == 4.5


def test_low_handicap_index_within_window():
    history = [
        (date(2025, 1, 1), 18.0),
        (date(2025, 6, 1), 15.0),
        (date(2025, 12, 1), 20.0),
    ]
    # Vindu 365 dager tilbake fra 2025-12-15 dekker alle tre -> laveste 15.0.
    assert low_handicap_index(history, date(2025, 12, 15)) == 15.0


def test_low_handicap_index_excludes_old_entries():
    history = [
        (date(2023, 1, 1), 5.0),   # for gammel, faller utenfor 365-dagersvinduet
        (date(2025, 11, 1), 15.0),
    ]
    assert low_handicap_index(history, date(2025, 12, 15)) == 15.0


def test_low_handicap_index_empty_returns_none():
    assert low_handicap_index([], date(2025, 1, 1)) is None


def test_apply_index_caps_diagram_5_8():
    # Diagram 5.8: Low Handicap Index-referanse 20,0. Soft cap-trigger +3
    # (23,0), hard cap-tak +5 (25,0).
    assert apply_index_caps(new_index=22.0, low_handicap_index_value=20.0) == 22.0  # under soft cap
    assert apply_index_caps(new_index=23.0, low_handicap_index_value=20.0) == 23.0  # akkurat på triggeren
    # Økning 4,0 -> 3,0 uendret + halvparten av resten (1,0) = 3,5 -> 23,5
    assert apply_index_caps(new_index=24.0, low_handicap_index_value=20.0) == 23.5
    # Stor økning -> hard cap-taket, aldri over 25,0
    assert apply_index_caps(new_index=40.0, low_handicap_index_value=20.0) == 25.0


def test_apply_index_caps_no_lower_limit():
    # Ingen nedre grense -- indeksen kan synke fritt, ingen cap i den retningen.
    assert apply_index_caps(new_index=5.0, low_handicap_index_value=20.0) == 5.0


def test_round_counts_for_handicap_18_hole_intent():
    # Rule 2.2a: intensjon 18 hull, minst 10 av 18 må være spilt.
    assert round_counts_for_handicap(18, holes_planned=18) is True
    assert round_counts_for_handicap(10, holes_planned=18) is True
    assert round_counts_for_handicap(9, holes_planned=18) is False


def test_round_counts_for_handicap_9_hole_intent():
    # Rule 2.2b: intensjon 9 hull, ALLE 9 må spilles -- ikke "minst 9".
    assert round_counts_for_handicap(9, holes_planned=9) is True
    assert round_counts_for_handicap(8, holes_planned=9) is False
    # Færre enn 9 er alltid ugyldig, uansett intensjon.
    assert round_counts_for_handicap(7, holes_planned=18) is False


def test_course_handicap_9_halves_index_rule_6_1b():
    # Rule 6.1b: indeksen HALVERES før den ganges med 9-hulls slope/113 --
    # dette AVVIKER fra 18-hulls-formelen (course_handicap_raw), som IKKE
    # halverer. Verifiserer nettopp dette avviket, siden ingen tallmessig
    # offisiell fasit ble gitt for akkurat denne kombinasjonen i kilden.
    idx, slope9, rating9, par9 = 20.0, 132, 36.1, 36
    ch9 = course_handicap_9_raw(idx, slope9, rating9, par9)
    # Skal IKKE være det samme som å bruke full indeks (ville gitt et annet tall).
    full_index_variant = course_handicap_raw(idx, slope9, rating9, par9)
    assert ch9 != full_index_variant
    expected = (idx / 2.0) * (slope9 / 113.0) + (rating9 - par9)
    assert abs(ch9 - expected) < 1e-9
    assert course_handicap_9(idx, slope9, rating9, par9) == round_half_up(expected)


# ---------------------------------------------------------------------------
# Skins (ADR-039 Beslutning D)
# ---------------------------------------------------------------------------

def test_skins_outright_win_every_hole():
    scores = [[("a", 4), ("b", 5), ("c", 6)], [("a", 3), ("b", 3), ("c", 5)]]
    result = compute_skins(scores, tie_handling="carry")
    # Hull 1: a vinner (laveste 4) -> 1 skin. Hull 2: a/b uavgjort (3) -> carry.
    assert result == {"a": 1.0}, result


def test_skins_carry_over_then_win():
    scores = [
        [("a", 4), ("b", 4), ("c", 5)],  # uavgjort a/b -> carry (pott=1)
        [("a", 5), ("b", 5), ("c", 4)],  # c vinner alene -> tar pott (1+1=2)
    ]
    result = compute_skins(scores, tie_handling="carry")
    assert result == {"c": 2.0}, result


def test_skins_three_way_tie_carries_whole_pot():
    scores = [
        [("a", 4), ("b", 4), ("c", 4)],  # alle like -> carry (pott=1)
        [("a", 5), ("b", 5), ("c", 3)],  # c vinner alene -> tar hele pott (2)
    ]
    result = compute_skins(scores, tie_handling="carry")
    assert result == {"c": 2.0}, result


def test_skins_split_tie_instead_of_carry():
    scores = [[("a", 4), ("b", 4), ("c", 5)]]
    result = compute_skins(scores, tie_handling="split")
    assert result == {"a": 0.5, "b": 0.5}, result


def test_skins_split_after_prior_carry_splits_whole_pot():
    scores = [
        [("a", 5), ("b", 4), ("c", 6)],  # b vinner alene -> tar pott (1)
        [("a", 4), ("b", 4), ("c", 5)],  # a/b uavgjort -> splittes (pott=1 denne runden)
    ]
    result = compute_skins(scores, tie_handling="split")
    assert result == {"b": 1.0 + 0.5, "a": 0.5}, result


def test_skins_empty_hole_skipped_not_an_error():
    scores = [[], [("a", 4), ("b", 5)]]
    result = compute_skins(scores, tie_handling="carry")
    assert result == {"a": 1.0}, result


def test_skins_no_winner_if_never_decided():
    scores = [[("a", 4), ("b", 4)], [("a", 5), ("b", 5)]]
    result = compute_skins(scores, tie_handling="carry")
    assert result == {}, result


# ---------------------------------------------------------------------------
# compute_skins_detail (hull-for-hull-forløp, samme totaler som compute_skins)
# ---------------------------------------------------------------------------

def test_skins_detail_matches_compute_skins_totals():
    scores = [
        [("a", 4), ("b", 4), ("c", 5)],
        [("a", 5), ("b", 5), ("c", 4)],
    ]
    winnings = compute_skins(scores, tie_handling="carry")
    detail_winnings, _log = compute_skins_detail(scores, tie_handling="carry")
    assert detail_winnings == winnings == {"c": 2.0}


def test_skins_detail_outright_win_log():
    scores = [[("a", 4), ("b", 5), ("c", 6)], [("a", 3), ("b", 3), ("c", 5)]]
    _winnings, log = compute_skins_detail(scores, tie_handling="carry")
    assert len(log) == 2
    assert log[0].values == {"a": 4, "b": 5, "c": 6}
    assert log[0].pot_before == 0.0
    assert log[0].awarded == {"a": 1.0}
    assert log[0].carried is False
    # Hull 2: a/b uavgjort -> carried, ingen tildeling, potten ruller (blir 1.0 for neste).
    assert log[1].awarded == {}
    assert log[1].carried is True


def test_skins_detail_carry_then_win_log():
    scores = [
        [("a", 4), ("b", 4), ("c", 5)],  # uavgjort -> carry
        [("a", 5), ("b", 5), ("c", 4)],  # c vinner -> tar hele potten (2)
    ]
    _winnings, log = compute_skins_detail(scores, tie_handling="carry")
    assert log[0].pot_before == 0.0
    assert log[0].carried is True
    assert log[0].awarded == {}
    assert log[1].pot_before == 1.0
    assert log[1].carried is False
    assert log[1].awarded == {"c": 2.0}


def test_skins_detail_split_log():
    scores = [[("a", 4), ("b", 4), ("c", 5)]]
    _winnings, log = compute_skins_detail(scores, tie_handling="split")
    assert log[0].carried is False
    assert log[0].awarded == {"a": 0.5, "b": 0.5}


def test_skins_detail_empty_hole_logged_but_inert():
    scores = [[], [("a", 4), ("b", 5)]]
    _winnings, log = compute_skins_detail(scores, tie_handling="carry")
    assert log[0].values == {}
    assert log[0].pot_before == 0.0
    assert log[0].awarded == {}
    assert log[0].carried is False
    assert log[1].awarded == {"a": 1.0}


# ---------------------------------------------------------------------------
# Kobenhavner/Copenhagen (2026-07-30) -- 6 poeng per hull mellom 3 spillere.
# Fasitverdiene er verbatim fra spilletyper-og-spilleformer-2023.pdf s.4,
# de fire rangeringsmonstrene (4-2-0/4-1-1/3-3-0/2-2-2).
# ---------------------------------------------------------------------------

def test_copenhagen_all_distinct_gives_4_2_0():
    # Lavest slag vinner (higher_is_better=False, standard) -- a=4 slag best.
    points = copenhagen_points_for_hole([("a", 4), ("b", 5), ("c", 6)])
    assert points == {"a": 4, "b": 2, "c": 0}
    assert sum(points.values()) == 6


def test_copenhagen_winner_alone_other_two_tied_gives_4_1_1():
    points = copenhagen_points_for_hole([("a", 4), ("b", 5), ("c", 5)])
    assert points == {"a": 4, "b": 1, "c": 1}
    assert sum(points.values()) == 6


def test_copenhagen_two_tied_best_one_worst_gives_3_3_0():
    points = copenhagen_points_for_hole([("a", 4), ("b", 4), ("c", 5)])
    assert points == {"a": 3, "b": 3, "c": 0}
    assert sum(points.values()) == 6


def test_copenhagen_all_tied_gives_2_2_2():
    points = copenhagen_points_for_hole([("a", 4), ("b", 4), ("c", 4)])
    assert points == {"a": 2, "b": 2, "c": 2}
    assert sum(points.values()) == 6


def test_copenhagen_higher_is_better_for_stableford_points():
    # Stableford: HØYEST poengsum vinner -- a=5 poeng best her.
    points = copenhagen_points_for_hole([("a", 5), ("b", 3), ("c", 2)], higher_is_better=True)
    assert points == {"a": 4, "b": 2, "c": 0}


def test_copenhagen_rejects_wrong_player_count():
    try:
        copenhagen_points_for_hole([("a", 4), ("b", 5)])
        assert False, "skulle kastet ValueError"
    except ValueError:
        pass


def test_copenhagen_total_matches_hand_computed_sum():
    scores = [
        [("a", 4), ("b", 5), ("c", 6)],  # a: 4, b: 2, c: 0
        [("a", 5), ("b", 5), ("c", 4)],  # c alene best -> c:4, a/b delt sist -> 1/1
        [("a", 4), ("b", 4), ("c", 4)],  # alle likt -> 2/2/2
    ]
    totals = compute_copenhagen(scores)
    assert totals == {"a": 4 + 1 + 2, "b": 2 + 1 + 2, "c": 0 + 4 + 2}
    assert totals == {"a": 7, "b": 5, "c": 6}
    assert sum(totals.values()) == 18  # 3 hull * 6 poeng


def test_copenhagen_incomplete_hole_skipped_not_an_error():
    scores = [[("a", 4), ("b", 5)], [("a", 4), ("b", 5), ("c", 6)]]
    totals = compute_copenhagen(scores)
    assert totals == {"a": 4, "b": 2, "c": 0}


def test_copenhagen_detail_log_matches_totals():
    scores = [[("a", 4), ("b", 5), ("c", 6)]]
    totals, log = compute_copenhagen_detail(scores)
    assert totals == {"a": 4, "b": 2, "c": 0}
    assert len(log) == 1
    assert log[0].values == {"a": 4, "b": 5, "c": 6}
    assert log[0].points == {"a": 4, "b": 2, "c": 0}


# ---------------------------------------------------------------------------
# Bingo Bango Bongo (2026-07-30)
# ---------------------------------------------------------------------------

def test_bbb_points_for_hole_three_different_winners():
    points = bbb_points_for_hole("a", "b", "c")
    assert points == {"a": 1, "b": 1, "c": 1}


def test_bbb_points_for_hole_one_winner_all_three():
    points = bbb_points_for_hole("a", "a", "a")
    assert points == {"a": 3}


def test_bbb_points_for_hole_sweep_bonus_gives_six():
    points = bbb_points_for_hole("a", "a", "a", sweep_bonus_enabled=True)
    assert points == {"a": 6}


def test_bbb_points_for_hole_sweep_bonus_off_gives_three():
    points = bbb_points_for_hole("a", "a", "a", sweep_bonus_enabled=False)
    assert points == {"a": 3}


def test_bbb_points_for_hole_none_categories_award_nothing():
    points = bbb_points_for_hole(None, "b", None)
    assert points == {"b": 1}


def test_bbb_points_for_hole_all_none_is_empty():
    assert bbb_points_for_hole(None, None, None) == {}


def test_compute_bbb_totals_across_holes():
    hole_log = [
        ("a", "b", "c"),   # a:1 b:1 c:1
        ("a", "a", "a"),   # a:3 (uten sweep-bonus)
        (None, "b", "b"),  # b:2
    ]
    totals = compute_bbb(hole_log)
    assert totals == {"a": 1 + 3, "b": 1 + 2, "c": 1}
    assert totals == {"a": 4, "b": 3, "c": 1}


def test_compute_bbb_with_sweep_bonus():
    hole_log = [("a", "a", "a"), ("b", "c", "b")]
    totals = compute_bbb(hole_log, sweep_bonus_enabled=True)
    # Hull 1: a sveiper -> 6 poeng. Hull 2: b vinner 2 av 3 (bingo+bongo), c 1 -> b:2, c:1
    assert totals == {"a": 6, "b": 2, "c": 1}


# ---------------------------------------------------------------------------
# Flaggturnering/Flag tournament (2026-07-30)
# ---------------------------------------------------------------------------

def test_flag_result_completes_all_holes_with_budget_left():
    result = flag_result([5, 4, 6], total_strokes_budget=20)
    assert result == FlagResult(holes_completed=3, ran_out=False, strokes_remaining=5)


def test_flag_result_exact_budget_no_remainder():
    result = flag_result([5, 4, 6], total_strokes_budget=15)
    assert result == FlagResult(holes_completed=3, ran_out=False, strokes_remaining=0)


def test_flag_result_runs_out_mid_round():
    # Budsjett 10: hull 1 (5) -> 5 igjen. Hull 2 (4) -> 1 igjen. Hull 3
    # krever 6, men kun 1 igjen -- går tom PÅ hull 3, kun 2 hull fullført.
    result = flag_result([5, 4, 6], total_strokes_budget=10)
    assert result == FlagResult(holes_completed=2, ran_out=True, strokes_remaining=1)


def test_flag_result_runs_out_on_first_hole():
    result = flag_result([5, 4, 6], total_strokes_budget=3)
    assert result == FlagResult(holes_completed=0, ran_out=True, strokes_remaining=3)


def test_flag_result_no_holes_played_yet():
    result = flag_result([], total_strokes_budget=90)
    assert result == FlagResult(holes_completed=0, ran_out=False, strokes_remaining=90)


# --- Runde 2+ (lap-fortsettelse, 2026-08-14) --------------------------------

def test_flag_result_runs_out_mid_lap_two():
    # Lap 1 fullført (18 hull, par 4 hver = 72), budsjett 100 -> 28 igjen inn
    # i lap 2. Lap 2: hull 1 (5) -> 23, hull 2 (4) -> 19, hull 3 krever 30
    # (umulig i praksis, men beviser bare at flag_result() selv ikke bryr
    # seg om laps -- kalleren limer bare sammen sekvensen).
    lap1 = [4] * 18
    lap2_prefix = [5, 4, 30]
    result = flag_result(lap1 + lap2_prefix, total_strokes_budget=100)
    assert result == FlagResult(holes_completed=20, ran_out=True, strokes_remaining=19)


def test_flag_result_completes_two_full_laps_with_budget_left():
    lap1 = [4] * 18
    lap2 = [4] * 18
    result = flag_result(lap1 + lap2, total_strokes_budget=200)
    assert result == FlagResult(holes_completed=36, ran_out=False, strokes_remaining=56)


def test_flag_lap_and_hole_within_lap_one_start_hole_one():
    play_order = list(range(1, 19))
    assert flag_lap_and_hole(0, play_order) == (1, 1)
    assert flag_lap_and_hole(2, play_order) == (1, 3)
    assert flag_lap_and_hole(17, play_order) == (1, 18)


def test_flag_lap_and_hole_wraps_into_lap_two():
    play_order = list(range(1, 19))
    # 18 fullførte hull = akkurat ferdig med lap 1 -- neste er lap 2, hull 1.
    assert flag_lap_and_hole(18, play_order) == (2, 1)
    assert flag_lap_and_hole(20, play_order) == (2, 3)
    assert flag_lap_and_hole(35, play_order) == (2, 18)
    assert flag_lap_and_hole(36, play_order) == (3, 1)


def test_flag_lap_and_hole_respects_non_default_start_hole():
    # start_hole=10 -> play_order er 10,11,...,18,1,2,...,9 (samme sirkulære
    # rekkefølge round-detail.tsx/rounds.py allerede bruker andre steder).
    play_order = [10, 11, 12, 13, 14, 15, 16, 17, 18, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    assert flag_lap_and_hole(0, play_order) == (1, 10)
    assert flag_lap_and_hole(8, play_order) == (1, 18)
    assert flag_lap_and_hole(9, play_order) == (1, 1)
    # Lap 2 gjentar SAMME spillerekkefølge, ikke fysisk hull 1.
    assert flag_lap_and_hole(18, play_order) == (2, 10)


# ---------------------------------------------------------------------------
# Shamble (2026-07-30)
# ---------------------------------------------------------------------------

def test_shamble_hole_score_two_best_of_four():
    # Konkret eksempel fra kilden: scores 4,5,4,6 -- "2 Best Balls of 4" -> 4+4=8.
    assert shamble_hole_score([4, 5, 4, 6], best_n=2) == 8


def test_shamble_hole_score_one_best_of_two():
    assert shamble_hole_score([5, 3], best_n=1) == 3


def test_shamble_hole_score_all_count_equals_sum():
    assert shamble_hole_score([4, 5, 6], best_n=3) == 15


def test_shamble_hole_score_rejects_best_n_out_of_range():
    try:
        shamble_hole_score([4, 5], best_n=3)
        assert False, "skulle kastet ValueError"
    except ValueError:
        pass
    try:
        shamble_hole_score([4, 5], best_n=0)
        assert False, "skulle kastet ValueError"
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# Money Ball/Lone Ranger (2026-07-30)
# ---------------------------------------------------------------------------

def test_money_ball_hole_1_uses_player_0():
    # Hull 1 -> rotasjonsindeks 0. Money-ball=scores[0]=5, laveste av [4,6,7] er 4.
    assert money_ball_hole_score([5, 4, 6, 7], hole_number=1) == 5 + 4


def test_money_ball_hole_2_uses_player_1():
    # Hull 2 -> rotasjonsindeks 1. Money-ball=scores[1]=4, laveste av [5,6,7] er 5.
    assert money_ball_hole_score([5, 4, 6, 7], hole_number=2) == 4 + 5


def test_money_ball_rotation_wraps_after_four_holes():
    # Hull 5 -> (5-1)%4 = 0, samme som hull 1.
    assert money_ball_hole_score([5, 4, 6, 7], hole_number=5) == money_ball_hole_score([5, 4, 6, 7], hole_number=1)


def test_money_ball_rejects_wrong_player_count():
    try:
        money_ball_hole_score([4, 5, 6], hole_number=1)
        assert False, "skulle kastet ValueError"
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# High-low-high (2026-07-30) -- verifisert tall for tall mot brukerens eget
# eksempel: lag 1 = A+B, lag 2 = C+D. Hull 1: A=3, B=0(stryk), C=2, D=2.
# Hull 2: A=1, B=2, C=3, D=1. Etter hull 1: 1-1. Etter hull 2: 2-1 til lag 2.
# ---------------------------------------------------------------------------

def test_high_low_high_hole_1_matches_user_example():
    # High: A(3) vs C-eller-D(2, likt) -> A vinner (3>2) -> 1p lag1.
    # Low: B(0) vs C-eller-D(2, likt) -> lag2 vinner (2>0) -> 1p lag2.
    points = high_low_high_points_for_hole((3, 0), (2, 2))
    assert points == (1, 1)


def test_high_low_high_hole_2_matches_user_example():
    # High: B(2) vs C(3) -> lag2 vinner -> 1p lag2.
    # Low: A(1) vs D(1) -> UAVGJORT -> 0p til begge (IKKE 0.5/0.5-splitt).
    points = high_low_high_points_for_hole((1, 2), (3, 1))
    assert points == (0, 1)


def test_high_low_high_running_score_matches_user_example():
    hole1 = high_low_high_points_for_hole((3, 0), (2, 2))
    hole2 = high_low_high_points_for_hole((1, 2), (3, 1))
    running_after_1 = high_low_high_running_score([hole1])
    assert running_after_1 == (1, 1), running_after_1  # "1-1 etter hull 1"
    running_after_2 = high_low_high_running_score([hole1, hole2])
    assert running_after_2 == (1, 2), running_after_2  # "2-1 til lag 2" (lag2=2, lag1=1)


def test_high_low_high_tie_on_both_duels_gives_no_points():
    points = high_low_high_points_for_hole((2, 2), (2, 2))
    assert points == (0, 0)


def test_high_low_high_sweep_gives_two_points():
    points = high_low_high_points_for_hole((4, 3), (2, 1))
    assert points == (2, 0)


# ---------------------------------------------------------------------------
# Individuelle scoringsmetoder (ADR-037): bruttoslagspill/nettoslagspill/
# Stableford
# ---------------------------------------------------------------------------

def test_stroke_play_gross_total_sums_raw_strokes():
    assert stroke_play_gross_total([5, 4, 6]) == 15


def test_stroke_play_gross_total_empty_is_zero():
    assert stroke_play_gross_total([]) == 0


def test_stroke_play_net_total_subtracts_received_strokes():
    # 5-1, 4-0, 6-1 = 4, 4, 5 -> 13
    assert stroke_play_net_total([5, 4, 6], [1, 0, 1]) == 13


def test_stableford_points_for_hole_net_par_is_two():
    # Par 4, brutto 4, ingen mottatte slag -> netto par -> 2 poeng.
    assert stableford_points_for_hole(par=4, gross_strokes=4, strokes_received=0) == 2


def test_stableford_points_for_hole_net_birdie_is_three():
    # Par 4, brutto 3 -> netto birdie -> 3 poeng.
    assert stableford_points_for_hole(par=4, gross_strokes=3, strokes_received=0) == 3


def test_stableford_points_for_hole_with_received_stroke():
    # Par 4, brutto 6, 1 mottatt slag -> netto 5 (bogey) -> 1 poeng.
    assert stableford_points_for_hole(par=4, gross_strokes=6, strokes_received=1) == 1


def test_stableford_points_for_hole_floors_at_zero():
    # Par 5, brutto 9, 1 mottatt slag -> netto 8 -> 5-8+2 = -1 -> gulvet på 0.
    assert stableford_points_for_hole(par=5, gross_strokes=9, strokes_received=1) == 0


def test_stableford_total_matches_hand_computed_sum():
    pars = [4, 4, 5]
    gross = [4, 6, 9]
    received = [0, 1, 1]
    # Hull for hull: 2 (net par) + 1 (net bogey) + 0 (gulvet) = 3.
    assert stableford_total(pars, gross, received) == 3
    assert stableford_total(pars, gross, received) == sum(
        stableford_points_for_hole(p, g, r) for p, g, r in zip(pars, gross, received)
    )


# ---------------------------------------------------------------------------
# Order of Merit
# ---------------------------------------------------------------------------

def test_oom_points_for_position_basic():
    table = [10, 8, 6, 4, 2]
    assert order_of_merit_points_for_position("1", table) == 10
    assert order_of_merit_points_for_position("3", table) == 6
    assert order_of_merit_points_for_position("5", table) == 2


def test_oom_points_for_position_tie_shares_value_not_averaged():
    table = [10, 8, 6, 4, 2]
    # To spillere delt 2. plass -- begge får poengverdien for plass 2 (8),
    # IKKE gjennomsnittet av plass 2+3 (7).
    assert order_of_merit_points_for_position("T2", table) == 8
    assert order_of_merit_points_for_position("T2", table) == order_of_merit_points_for_position("T2", table)


def test_oom_points_for_position_beyond_table_is_zero():
    table = [10, 8, 6]
    assert order_of_merit_points_for_position("4", table) == 0
    assert order_of_merit_points_for_position("T10", table) == 0


def test_oom_aggregate_empty_is_none():
    assert order_of_merit_aggregate([], "sum", None) is None
    assert order_of_merit_aggregate([], "average", 2) is None


def test_oom_aggregate_sum_all():
    assert order_of_merit_aggregate([10, 8, 6], "sum", None) == 24


def test_oom_aggregate_average_all():
    assert order_of_merit_aggregate([10, 8, 6], "average", None) == 8


def test_oom_aggregate_best_n_drops_worst():
    # Behold de 2 beste (10, 8) -- dropp den dårligste (6).
    assert order_of_merit_aggregate([10, 8, 6], "sum", 2) == 18
    assert order_of_merit_aggregate([10, 8, 6], "average", 2) == 9


def test_oom_aggregate_best_n_larger_than_available_counts_all():
    assert order_of_merit_aggregate([10, 8], "sum", 5) == 18


def test_oom_aggregate_higher_is_better_gross_uses_negated_values():
    # Brutto 70/75/68 -- kalleren negerer (-70/-75/-68) slik at "størst er
    # best" fortsatt betyr "lavest brutto er best". Behold de 2 beste
    # (dvs. laveste to brutto-tall: 68, 70) -> negert sum -138, altså
    # ekte sum 138.
    negated = [-70, -75, -68]
    assert order_of_merit_aggregate(negated, "sum", 2) == -138


# ---------------------------------------------------------------------------
# Eclectic (org individuell turnering, "Del C") -- beste resultat per hull
# på tvers av rundene.
# ---------------------------------------------------------------------------

def test_eclectic_picks_lowest_gross_per_hole_across_rounds():
    # Hull 1: runde 0 ga 5, runde 1 ga 4 -- 4 skal plukkes.
    # Hull 2: runde 0 ga 3, runde 1 ga 6 -- 3 skal plukkes.
    values_by_hole = [
        [EclecticHoleValue(round_index=0, value=5), EclecticHoleValue(round_index=1, value=4)],
        [EclecticHoleValue(round_index=0, value=3), EclecticHoleValue(round_index=1, value=6)],
    ]
    result = eclectic_best_per_hole(values_by_hole, "gross")
    assert result.total == 4 + 3
    assert result.holes == [
        EclecticHolePick(hole_number=1, value=4, round_index=1),
        EclecticHolePick(hole_number=2, value=3, round_index=0),
    ]


def test_eclectic_best_result_comes_from_different_rounds_for_different_holes():
    # Beviser at plukkingen faktisk skjer PER HULL, ikke "velg beste hele
    # runde": runde 0 er best på hull 1, runde 1 er best på hull 2 -- ingen
    # av rundene alene ville gitt denne totalen (5 hadde vært verre enn 4
    # på hull 1, og 6 hadde vært verre enn 3 på hull 2, uansett hvilken
    # HELE runde man valgte).
    values_by_hole = [
        [EclecticHoleValue(round_index=0, value=4), EclecticHoleValue(round_index=1, value=6)],
        [EclecticHoleValue(round_index=0, value=7), EclecticHoleValue(round_index=1, value=3)],
    ]
    result = eclectic_best_per_hole(values_by_hole, "gross")
    assert [h.round_index for h in result.holes] == [0, 1]
    assert result.total == 4 + 3


def test_eclectic_net_also_picks_lowest():
    values_by_hole = [[EclecticHoleValue(round_index=0, value=2), EclecticHoleValue(round_index=1, value=1)]]
    result = eclectic_best_per_hole(values_by_hole, "net")
    assert result.total == 1
    assert result.holes[0].round_index == 1


def test_eclectic_stableford_picks_highest():
    values_by_hole = [[EclecticHoleValue(round_index=0, value=2), EclecticHoleValue(round_index=1, value=4)]]
    result = eclectic_best_per_hole(values_by_hole, "stableford")
    assert result.total == 4
    assert result.holes[0].round_index == 1


def test_eclectic_hole_with_no_recorded_value_is_excluded_not_zero():
    # Hull 2 er ikke spilt i NOEN runde ennå -- skal utelates fra
    # `holes` (og dermed fra totalen), ikke telle som 0.
    values_by_hole = [
        [EclecticHoleValue(round_index=0, value=5)],
        [],
        [EclecticHoleValue(round_index=0, value=4)],
    ]
    result = eclectic_best_per_hole(values_by_hole, "gross")
    assert result.total == 9
    assert [h.hole_number for h in result.holes] == [1, 3]


# ---------------------------------------------------------------------------
# Enkel selvsjekk uten pytest
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    import traceback

    tests = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
    passed = 0
    failed = 0
    for t in tests:
        try:
            t()
            print(f"  ok   {t.__name__}")
            passed += 1
        except Exception:
            print(f"  FEIL {t.__name__}")
            traceback.print_exc()
            failed += 1
    print(f"\n{passed} bestått, {failed} feilet, {len(tests)} totalt")
    raise SystemExit(1 if failed else 0)
