"""Independent standard-library check; run with Python 3.10+ or python -O.

Does not import generate.py. Counts actual 3-card subsets, then computes ending
stacks across miss, hit/win and hit/loss branches rather than reusing its EV
expression. All q/F/L inputs are hypothetical, not observed poker frequencies.
"""
from collections import Counter
from copy import deepcopy
import csv
from fractions import Fraction
from itertools import combinations
import json
from pathlib import Path

ROOT = Path(__file__).resolve().parent
F = Fraction


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


# A fixed pair of rank 0, suits 0 and 1, is removed from a standard deck.
deck = [(rank, suit) for rank in range(13) for suit in range(4)
        if (rank, suit) not in [(0, 0), (0, 1)]]
flops = list(combinations(deck, 3))
counts = Counter()
for flop in flops:
    ranks = Counter(card[0] for card in flop)
    matches = ranks.get(0, 0)
    if matches == 2:
        counts['quads'] += 1
    elif matches == 1:
        counts['matching_full_house' if 2 in ranks.values() else 'set'] += 1
    elif 3 in ranks.values():
        counts['board_trips_non_hit'] += 1
    else:
        counts['other_non_hit'] += 1

hits = counts['quads'] + counts['matching_full_house'] + counts['set']
p = F(hits, len(flops))
require(dict(counts) == {'set': 2112, 'matching_full_house': 144,
                        'quads': 48, 'board_trips_non_hit': 48,
                        'other_non_hit': 17248}, 'Unexpected flop partition')


def net_from_ending_stacks(q, future_won, future_lost):
    require(0 <= q <= 1, 'Conditional win chance must be in [0,1]')
    require(0 <= future_won <= 97, 'Winning future contribution exceeds capacity')
    require(0 <= future_lost <= 97, 'Losing future contribution exceeds capacity')
    # BB has 99bb before this decision and pays 2bb more. Current pot is 4.5bb.
    start, call, current_pot = F(99), F(2), F(9, 2)
    miss_end = start - call
    # Use a matched future contribution on the win branch, return full pot.
    win_end = start - call - future_won + (current_pot + call + 2 * future_won)
    loss_end = start - call - future_lost
    # Weighted terminal stacks, with directly enumerated flop class counts.
    end_sum = (len(flops) - hits) * miss_end
    end_sum += hits * q * win_end + hits * (1 - q) * loss_end
    return end_sum / len(flops) - start, q * win_end + (1 - q) * loss_end - start


def check_result(result):
    require(result['flops'] == len(flops), 'Flop total mismatch')
    require(result['rank_matching_flops'] == hits, 'Hit total mismatch')
    require(F(result['hit_probability']) == p, 'Hit fraction mismatch')
    require(result['hit_probability_percent'] == round(float(p * 100), 6), 'Hit percent mismatch')
    require(F(result['pot_before_call_bb']) == F(9, 2), 'Wrong pot')
    require(F(result['incremental_call_bb']) == 2, 'Wrong incremental call')
    require(result['remaining_stack_after_call_bb'] == 97, 'Wrong future capacity')
    # With miss net -2, pay exactly enough on each hit to offset all misses.
    required_hit = F((len(flops) - hits) * 2, hits)
    require(F(result['exact_required_mean_hit_net']) == required_hit, 'Hit threshold mismatch')
    require(result['required_mean_hit_net_bb'] == round(float(required_hit), 6), 'Rounded hit threshold mismatch')
    require([r['case'] for r in result['cases']] == ['A', 'B', 'C', 'D'], 'Case inventory mismatch')
    case_inputs = [(F(9, 10), 20, 20), (F(9, 10), 20, 97), (F(1), 0, 0), (F(1), 20, 0)]
    for row, expected_inputs in zip(result['cases'], case_inputs):
        inputs = tuple(F(row[k]) for k in ['hit_win_probability', 'win_future_opponent_bb', 'loss_future_hero_bb'])
        require(inputs == expected_inputs, 'Case inputs mismatch')
        ev, hit_net = net_from_ending_stacks(*inputs)
        require(F(row['exact_call_ev']) == ev, 'Exact EV mismatch')
        require(row['call_model_ev_bb'] == round(float(ev), 6), 'Rounded EV mismatch')
        require(row['hit_mean_net_bb'] == round(float(hit_net), 6), 'Hit mean mismatch')
    expected_grid = [(q, loss) for q in [F(1), F(9, 10), F(4, 5)] for loss in [0, 20, 97]]
    require(len(result['thresholds']) == 9, 'Threshold count mismatch')
    for row, expected_inputs in zip(result['thresholds'], expected_grid):
        q, loss = F(row['hit_win_probability']), F(row['loss_future_hero_bb'])
        require((q, loss) == expected_inputs, 'Grid inputs mismatch')
        future = F(row['exact_required'])
        require(net_from_ending_stacks(q, future, loss)[0] == 0, 'Threshold is not exact zero')
        require(net_from_ending_stacks(q, future - F(1, 1000), loss)[0] < 0, 'Below threshold is not negative')
        require(net_from_ending_stacks(q, future + F(1, 1000), loss)[0] > 0, 'Above threshold is not positive')
        require(row['required_win_future_opponent_bb'] == round(float(future), 6), 'Rounded threshold mismatch')


result = json.loads((ROOT / 'results.json').read_text(encoding='utf-8'))
check_result(result)
for filename, key in [('cases.csv', 'cases'), ('thresholds.csv', 'thresholds')]:
    with (ROOT / filename).open(encoding='utf-8', newline='') as handle:
        rows = list(csv.DictReader(handle))
    expected = [{key: str(value) for key, value in row.items()} for row in result[key]]
    require(rows == expected, filename + ' differs from verified JSON')

# In-memory tampering checks prove mismatches fail without editing public data.
mutants = []
for field, value in [('rank_matching_flops', 2305), ('incremental_call_bb', '3')]:
    mutant = deepcopy(result)
    mutant[field] = value
    mutants.append(mutant)
mutant = deepcopy(result)
mutant['cases'][0]['exact_call_ev'] = '0'
mutants.append(mutant)
mutant = deepcopy(result)
mutant['thresholds'][0]['exact_required'] = '10'
mutants.append(mutant)
for mutant in mutants:
    try:
        check_result(mutant)
    except ValueError:
        continue
    raise ValueError('Tampered data unexpectedly accepted')
for inputs in [(F(-1), F(20), F(20)), (F(2), F(20), F(20)),
               (F(1), F(98), F(0)), (F(1), F(20), F(-1))]:
    try:
        net_from_ending_stacks(*inputs)
    except ValueError:
        continue
    raise ValueError('Invalid parameter unexpectedly accepted')

print(json.dumps({'status': 'PASS', 'flops_enumerated': len(flops),
                  'flop_partition': dict(counts), 'hit_fraction': str(p),
                  'cases_checked': 4, 'exact_zero_crossings_checked': 9,
                  'csv_parity': True, 'tampering_checks': 4,
                  'invalid_parameter_checks': 4}, indent=2))
