"""Independent max-card-per-player verification of the fixed teaching cases.

Run with Python 3.10+: python verify.py (also works under python -O).
No random sampling, empirical outcomes, or general tournament rulings.
"""
from copy import deepcopy
from pathlib import Path
import json
from generate import calculate

HERE = Path(__file__).resolve().parent


def require(condition, message):
    if not condition:
        raise RuntimeError(message)


def independently_calculate(case):
    # Alternate algorithm: retain just each player's highest card, then take
    # the top N players. The generator instead sorts all cards and skips
    # repeat winners. These implementations do not share the award algorithm.
    rank = {r: n for n, r in enumerate('23456789TJQKA', 2)}
    suit = {'c': 0, 'd': 1, 'h': 2, 's': 3}
    players = case['players']
    remaining = [p['oldChips'] % 4 for p in players]
    retained = [p['higherValue'] + (p['oldChips'] // 4) * 100 for p in players]
    n_awards = sum(remaining) // 4
    best = [(max((rank[c[0]], suit[c[1]]) for c in p['cards']), i)
            for i, p in enumerate(players) if p['cards']]
    winners = [i for _, i in sorted(best, reverse=True)[:n_awards]]
    ordinary = [100 if i in winners else 0 for i in range(len(players))]
    protection = [100 if retained[i] == 0 and ordinary[i] == 0 and remaining[i] > 0 else 0
                  for i in range(len(players))]
    return {
        'winners': [players[i]['seat'] for i in winners],
        'after': [retained[i] + ordinary[i] + protection[i] for i in range(len(players))],
        'ordinary': ordinary,
        'protection': protection,
    }


def expect_rejection(data, label, mutate):
    bad = deepcopy(data)
    mutate(bad)
    try:
        calculate(bad)
    except ValueError:
        return
    raise RuntimeError('Expected rejection: ' + label)


def main():
    data = json.loads((HERE / 'inputs.json').read_text(encoding='utf-8'))
    saved = json.loads((HERE / 'results.json').read_text(encoding='utf-8'))
    calculated = calculate(data)
    require(saved == calculated, 'Saved results differ from regeneration')
    known = {
        'last-chips': ([700, 100, 600], 1300, 1400, [0, 100, 0]),
        'other-chip': ([700, 100, 600], 1400, 1400, [0, 0, 0]),
        'b-wins': ([700, 100, 500], 1300, 1300, [0, 0, 0]),
    }
    require({c['id'] for c in data['cases']} == set(known), 'Unexpected case set')
    for case, result in zip(data['cases'], calculated['cases']):
        alternative = independently_calculate(case)
        expected_after, expected_before, expected_total, expected_protection = known[case['id']]
        rows = result['players']
        require(alternative['after'] == expected_after, 'Independent arithmetic mismatch')
        require([r['after'] for r in rows] == expected_after, 'Final stacks mismatch')
        require(result['totalBefore'] == expected_before, 'Initial total mismatch')
        require(result['totalAfter'] == expected_total, 'Final total mismatch')
        require(result['winners'] == alternative['winners'], 'Winner order mismatch')
        require([r['ordinaryAward'] for r in rows] == alternative['ordinary'], 'Ordinary awards mismatch')
        require([r['protection'] for r in rows] == expected_protection == alternative['protection'], 'Protection mismatch')
        require(result['pool'] == 200 and result['ordinaryChips'] == 2, 'Pool mismatch')
        require(sum(r['protection'] for r in rows) == expected_total - expected_before,
                'Protection must account for every added chip unit in these divisible cases')

    def player_field(field, value):
        return lambda d: d['cases'][0]['players'][0].__setitem__(field, value)

    rejects = [
        ('unsupported denomination', lambda d: d.__setitem__('oldDenomination', 5)),
        ('negative old chip count', player_field('oldChips', -1)),
        ('fractional old chip count', player_field('oldChips', 7.5)),
        ('invalid retained value', player_field('higherValue', 525)),
        ('negative retained value', player_field('higherValue', -100)),
        ('invalid rank', player_field('cards', ['Xs', 'Ks', 'Qs'])),
        ('invalid suit', player_field('cards', ['Ax', 'Ks', 'Qs'])),
        ('duplicate card', player_field('cards', ['As', 'As', 'Qs'])),
        ('cross-player duplicate card', player_field('cards', ['8h', 'Ks', 'Qs'])),
        ('card-count mismatch', player_field('cards', ['As', 'Ks'])),
        ('duplicate seat', player_field('seat', 'B')),
    ]
    for label, mutate in rejects:
        expect_rejection(data, label, mutate)

    def fractional_pool(d):
        # Preserve the one-card-per-remainder invariant so the pool guard is
        # what excludes this case: 7 odd chips = 175, rather than 8 = 200.
        p = d['cases'][0]['players'][2]
        p['oldChips'] = 1
        p['cards'] = ['Js']

    expect_rejection(data, 'fractional pooled awards', fractional_pool)
    print('PASS: 3 fixed cases, independent maxima algorithm, exact known totals, 12 invalid/excluded inputs')


if __name__ == '__main__':
    main()
