"""Check the published PLO paired-board cases. Python 3.10+, standard library.

Run from any directory: python /path/to/verify.py
No arguments, network, random sampling, private data, or installed packages.
"""
from collections import Counter
from itertools import combinations, permutations
from pathlib import Path
import csv
import io
import json

ROOT = Path(__file__).resolve().parent
RANKS = '23456789TJQKA'
SUITS = 'shdc'
DECK = {r+s for r in RANKS for s in SUITS}

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

def score(cards):
    """Lexicographic standard five-card high-hand value; 0 high card..8 SF."""
    require(len(cards) == 5 and len(set(cards)) == 5 and set(cards) <= DECK,
            'Five unique standard cards required')
    ranks = sorted((RANKS.index(c[0])+2 for c in cards), reverse=True)
    groups = sorted(((n,r) for r,n in Counter(ranks).items()), reverse=True)
    flush = len({c[1] for c in cards}) == 1
    straight = 5 if ranks == [14,5,4,3,2] else (ranks[0] if len(set(ranks)) == 5 and ranks[0]-ranks[-1] == 4 else 0)
    if flush and straight: return (8,straight)
    if groups[0][0] == 4: return (7,groups[0][1],groups[1][1])
    if [n for n,r in groups] == [3,2]: return (6,groups[0][1],groups[1][1])
    if flush: return (5,*ranks)
    if straight: return (4,straight)
    if groups[0][0] == 3: return (3,groups[0][1],*sorted((r for n,r in groups[1:]),reverse=True))
    if [n for n,r in groups[:2]] == [2,2]: return (2,*sorted((r for n,r in groups[:2]),reverse=True),groups[2][1])
    if groups[0][0] == 2: return (1,groups[0][1],*sorted((r for n,r in groups[1:]),reverse=True))
    return (0,*ranks)

def best(hole, board):
    require(len(hole) == 4 and len(board) == 5, 'Four hole and five board cards required')
    require(len(set(hole+board)) == 9 and set(hole+board) <= DECK, 'Cards must be unique and valid')
    choices = [score(h+b) for h in combinations(hole,2) for b in combinations(board,3)]
    require(len(choices) == 60, 'Expected all 60 legal selections')
    return max(choices)

def run():
    data = json.loads((ROOT/'cases.json').read_text(encoding='utf-8'))
    cases = data['cases']
    require(len(cases) == 8 and len({c['id'] for c in cases}) == 8, 'Expected eight distinct cases')
    # Fixed boundaries cover every category, the ace-low straight, and tie breakers.
    controls = [
      ('As Kd 9c 7h 3s',(0,14,13,9,7,3)), ('Ks Kh As Qh 7d',(1,13,14,12,7)),
      ('As Ah Ks Kh 7d',(2,14,13,7)), ('Ks Kh Kd As Qh',(3,13,14,12)),
      ('As 2d 3c 4h 5s',(4,5)), ('9s Td Jc Qh Ks',(4,13)),
      ('As Js 9s 7s 3s',(5,14,11,9,7,3)), ('Ks Kh Kd As Ah',(6,13,14)),
      ('Ks Kh Kd Kc As',(7,13,14)), ('As 2s 3s 4s 5s',(8,5)),
      ('Ts Js Qs Ks As',(8,14))]
    for cards, expected in controls:
        require(score(cards.split()) == expected, 'Classifier boundary: '+cards)
    require(score('As Ah Ks Kh 2s'.split()) > score('Ks Kh Qs Qh As'.split()), 'Top pair must decide two pair')
    require(score('7s 7h 7d As Ah'.split()) < score('Ks Kh Kd 2s 2h'.split()), 'Trips must decide full house')
    require(score('Ks Kh Kd As Qh'.split()) > score('Ks Kh Kd As Jh'.split()), 'Second trips kicker must decide')
    require(best('7s 7h As Qc'.split(),'Ks Kh Kd 7c 7d'.split()) == (7,7,13), 'Pocket sevens counterexample')
    require(cases[6]['board'] == cases[7]['board'], 'Final comparison requires the same board')
    require(len(set(cases[6]['board']+cases[6]['hole']+cases[7]['hole'])) == 13,
            'Final showdown must have 13 distinct cards')
    require(tuple(cases[7]['expected_score']) > tuple(cases[6]['expected_score']), 'Final showdown winner')
    output=[]
    for c in cases:
        h,b=c['hole'],c['board']
        expected=tuple(c['expected_score'])
        require(best(h,b) == expected, 'Best hand mismatch: '+c['id'])
        witness=c['witness']
        require(len(witness) == 5 and len(set(witness)) == 5, 'Invalid witness')
        require(len(set(witness)&set(h)) == 2 and len(set(witness)&set(b)) == 3, 'Witness must use 2+3')
        require(score(witness) == expected, 'Witness does not attain maximum')
        require(best(h[::-1],b[::-1]) == expected, 'Order invariance failed')
        for suit_order in permutations(SUITS):
            mapping=dict(zip(SUITS,suit_order))
            relabel=lambda cards:[x[0]+mapping[x[1]] for x in cards]
            require(best(relabel(h),relabel(b)) == expected,'Global suit invariance failed')
        output.append([c['id'],' '.join(b),' '.join(h),c['expected_label'],' '.join(witness),60])
    expected_csv=io.StringIO(newline='')
    writer=csv.writer(expected_csv,lineterminator='\n')
    writer.writerow(['case_id','board','hole_cards','best_hand','one_legal_best_five','legal_selections_checked'])
    writer.writerows(output)
    require((ROOT/'results.csv').read_text(encoding='utf-8') == expected_csv.getvalue(),'Published CSV differs from checked cases')
    for h,b in [(cases[0]['hole'][:3],cases[0]['board']),
                (['Ks']+cases[0]['hole'][1:],cases[0]['board']),
                (['1s']+cases[0]['hole'][1:],cases[0]['board']),
                (cases[0]['hole'],cases[0]['board'][:4])]:
        try: best(h,b)
        except ValueError: pass
        else: raise ValueError('Invalid input accepted')
    print('PASS: 8 cases, 480 legal selections, 192 whole-state suit relabels, 14 classifier boundaries, pocket-sevens counterexample, disjoint final showdown, 4 invalid-input controls, CSV parity.')

if __name__ == '__main__':
    run()
