#!/usr/bin/env python3
"""Independent standard-library verifier for the poker-stat sample-size bundle."""

import csv
import hashlib
import json
import math
from pathlib import Path

ROOT = Path(__file__).resolve().parent
JSON_PATH = ROOT / "reference.json"
Z95 = 1.959963984540054


def close(actual, expected, tolerance=6e-12):
    return math.isclose(actual, expected, rel_tol=tolerance, abs_tol=tolerance)


def wilson(successes, opportunities, z=Z95):
    assert isinstance(successes, int)
    assert isinstance(opportunities, int)
    assert 0 <= successes <= opportunities
    assert opportunities > 0
    observed = successes / opportunities
    z_squared = z * z
    denominator = 1 + z_squared / opportunities
    center = (observed + z_squared / (2 * opportunities)) / denominator
    margin = z * math.sqrt(
        observed * (1 - observed) / opportunities
        + z_squared / (4 * opportunities * opportunities)
    ) / denominator
    return max(0.0, center - margin), min(1.0, center + margin)


def wald(successes, opportunities, z=Z95):
    observed = successes / opportunities
    margin = z * math.sqrt(observed * (1 - observed) / opportunities)
    return observed - margin, observed + margin


def exact_zero_event_upper(opportunities, alpha=0.05):
    assert isinstance(opportunities, int) and opportunities > 0
    assert 0 < alpha < 1
    return 1 - alpha ** (1 / opportunities)


def read_csv(name):
    with (ROOT / name).open(newline="", encoding="utf-8") as handle:
        return list(csv.DictReader(handle))


def parse_bool(value):
    if value == "true":
        return True
    if value == "false":
        return False
    raise ValueError(f"Expected lowercase CSV boolean, got {value!r}")


def verify_manifest():
    expected_names = {
        "reference.json",
        "same-rate-wilson.csv",
        "zero-event-wilson.csv",
        "context-split.csv",
        "decision-threshold.csv",
        "opportunity-planning.csv",
        "wald-comparison.csv",
        "calculator.html",
        "generate.mjs",
        "verify.py",
        "verification-report.json",
        "README.md",
        "../../images/blog_images/poker-stat-sample-size-wilson.svg",
    }
    entries = {}
    for line in (ROOT / "MANIFEST.sha256").read_text(encoding="utf-8").splitlines():
        digest, name = line.split("  ", 1)
        assert len(digest) == 64 and all(character in "0123456789abcdef" for character in digest)
        assert name not in entries
        entries[name] = digest
    assert set(entries) == expected_names
    for name, expected_digest in entries.items():
        actual_digest = hashlib.sha256((ROOT / name).resolve().read_bytes()).hexdigest()
        assert actual_digest == expected_digest, name


def main():
    if not __debug__:
        raise SystemExit("Run verify.py without Python optimization; verification assertions must stay enabled.")

    model = json.loads(JSON_PATH.read_text(encoding="utf-8"))
    assert model["schemaVersion"] == 1
    assert model["generatedOn"] == "2026-09-04"
    assert model["method"]["confidenceLevel"] == 0.95
    assert close(model["method"]["zTwoSided"], Z95)

    benchmark = model["nistBenchmark"]
    assert (benchmark["successes"], benchmark["opportunities"]) == (8, 30)
    benchmark_lower, benchmark_upper = wilson(8, 30)
    assert close(benchmark["computedWilsonLower"], benchmark_lower)
    assert close(benchmark["computedWilsonUpper"], benchmark_upper)
    assert round(benchmark_lower, 6) == benchmark["publishedWilsonLowerSixDecimals"] == 0.141827
    assert round(benchmark_upper, 6) == benchmark["publishedWilsonUpperSixDecimals"] == 0.444480

    same_rows = model["sameObservedRate"]["rows"]
    assert [row["opportunities"] for row in same_rows] == [20, 40, 100, 200, 500, 1000]
    previous_width = math.inf
    for row in same_rows:
        assert close(row["successes"] / row["opportunities"], 0.25)
        lower, upper = wilson(row["successes"], row["opportunities"])
        assert close(row["wilson95Lower"], lower)
        assert close(row["wilson95Upper"], upper)
        width = (upper - lower) * 100
        assert close(row["widthPercentagePoints"], width, 6e-7)
        assert width < previous_width
        previous_width = width

    same_csv = read_csv("same-rate-wilson.csv")
    assert len(same_csv) == len(same_rows)
    for generated, published in zip(same_rows, same_csv):
        assert int(published["successes"]) == generated["successes"]
        assert int(published["eligible_opportunities"]) == generated["opportunities"]
        assert close(float(published["observed_rate"]), generated["observedRate"])
        assert close(float(published["wilson_95_lower"]), generated["wilson95Lower"])
        assert close(float(published["wilson_95_upper"]), generated["wilson95Upper"])
        assert close(float(published["interval_width_percentage_points"]), generated["widthPercentagePoints"], 6e-7)

    zero_rows = model["zeroEvents"]["rows"]
    assert [row["opportunities"] for row in zero_rows] == [10, 25, 50, 100, 250]
    for row in zero_rows:
        _, two_sided_upper = wilson(0, row["opportunities"])
        exact_upper = exact_zero_event_upper(row["opportunities"])
        assert close(row["wilson95TwoSidedUpperEndpoint"], two_sided_upper)
        assert close(row["exactBinomial95OneSidedUpper"], exact_upper)
        assert row["wilson95TwoSidedUpperEndpoint"] > 0
        assert row["exactBinomial95OneSidedUpper"] > 0
        assert row["ruleOfThreeApplicable"] == (row["opportunities"] > 30)
        if row["opportunities"] > 30:
            assert close(row["ruleOfThreeUpperApproximation"], 3 / row["opportunities"])
        else:
            assert row["ruleOfThreeUpperApproximation"] is None
    zero_csv = read_csv("zero-event-wilson.csv")
    assert len(zero_csv) == len(zero_rows)
    for generated, published in zip(zero_rows, zero_csv):
        assert int(published["successes"]) == generated["successes"]
        assert int(published["eligible_opportunities"]) == generated["opportunities"]
        assert close(float(published["observed_rate"]), generated["observedRate"])
        assert close(float(published["wilson_95_two_sided_upper_endpoint"]), generated["wilson95TwoSidedUpperEndpoint"])
        assert close(float(published["exact_binomial_95_one_sided_upper"]), generated["exactBinomial95OneSidedUpper"])
        assert parse_bool(published["rule_of_three_applicable_n_gt_30"]) == generated["ruleOfThreeApplicable"]
        if generated["ruleOfThreeApplicable"]:
            assert close(float(published["rule_of_three_upper_approximation"]), generated["ruleOfThreeUpperApproximation"])
        else:
            assert published["rule_of_three_upper_approximation"] == ""

    context_rows = model["contextSplit"]["rows"]
    assert len(context_rows) == 3
    assert all(row["simulated"] is True for row in context_rows)
    assert close(context_rows[0]["observedRate"], 0.40)
    assert close(context_rows[1]["observedRate"], 0.10)
    assert context_rows[2]["successes"] == context_rows[0]["successes"] + context_rows[1]["successes"]
    assert context_rows[2]["opportunities"] == context_rows[0]["opportunities"] + context_rows[1]["opportunities"]
    assert close(context_rows[2]["observedRate"], 0.25)
    context_csv = read_csv("context-split.csv")
    assert len(context_csv) == len(context_rows)
    for generated, published in zip(context_rows, context_csv):
        assert published["context_key"] == generated["key"]
        assert published["context_label"] == generated["label"]
        assert int(published["successes"]) == generated["successes"]
        assert int(published["eligible_opportunities"]) == generated["opportunities"]
        assert close(float(published["observed_rate"]), generated["observedRate"])
        assert close(float(published["wilson_95_lower"]), generated["wilson95Lower"])
        assert close(float(published["wilson_95_upper"]), generated["wilson95Upper"])
        assert parse_bool(published["simulated"]) == generated["simulated"]

    threshold_rows = model["decisionThresholdIllustration"]["rows"]
    assert len(threshold_rows) == 2
    assert all(row["simulated"] is True for row in threshold_rows)
    assert all(close(row["predeclaredThreshold"], 0.30) for row in threshold_rows)
    assert threshold_rows[0]["relationToThreshold"] == "crosses"
    assert threshold_rows[0]["wilson95Lower"] < 0.30 < threshold_rows[0]["wilson95Upper"]
    assert threshold_rows[1]["relationToThreshold"] == "below"
    assert threshold_rows[1]["wilson95Upper"] < 0.30
    threshold_csv = read_csv("decision-threshold.csv")
    assert len(threshold_csv) == len(threshold_rows)
    for generated, published in zip(threshold_rows, threshold_csv):
        assert int(published["successes"]) == generated["successes"]
        assert int(published["eligible_opportunities"]) == generated["opportunities"]
        assert close(float(published["observed_rate"]), generated["observedRate"])
        assert close(float(published["wilson_95_lower"]), generated["wilson95Lower"])
        assert close(float(published["wilson_95_upper"]), generated["wilson95Upper"])
        assert close(float(published["predeclared_threshold"]), generated["predeclaredThreshold"])
        assert published["relation_to_threshold"] == generated["relationToThreshold"]
        assert parse_bool(published["simulated"]) == generated["simulated"]

    plan_rows = model["opportunityPlanning"]["rows"]
    assert len(plan_rows) == 12
    assert {(row["targetOpportunities"], row["assumedEligibilityRate"]) for row in plan_rows} == {
        (target, rate) for target in [20, 100, 500] for rate in [0.05, 0.10, 0.25, 0.50]
    }
    for row in plan_rows:
        assert close(row["expectedTotalHands"], row["targetOpportunities"] / row["assumedEligibilityRate"])
    plan_csv = read_csv("opportunity-planning.csv")
    assert len(plan_csv) == len(plan_rows)
    for generated, published in zip(plan_rows, plan_csv):
        assert int(published["target_eligible_opportunities"]) == generated["targetOpportunities"]
        assert close(float(published["assumed_eligibility_rate"]), generated["assumedEligibilityRate"])
        assert close(float(published["expected_total_hands"]), generated["expectedTotalHands"])

    comparison_rows = model["waldComparison"]["rows"]
    assert len(comparison_rows) == 6
    for row in comparison_rows:
        wilson_lower, wilson_upper = wilson(row["successes"], row["opportunities"])
        wald_lower, wald_upper = wald(row["successes"], row["opportunities"])
        assert close(row["wilson95Lower"], wilson_lower)
        assert close(row["wilson95Upper"], wilson_upper)
        assert close(row["wald95LowerRaw"], wald_lower)
        assert close(row["wald95UpperRaw"], wald_upper)
        assert row["waldProducesImpossibleBound"] == (wald_lower < 0 or wald_upper > 1)
        assert row["waldCollapsesAtBoundary"] == (row["successes"] in [0, row["opportunities"]])
    assert comparison_rows[0]["waldCollapsesAtBoundary"] is True
    assert comparison_rows[1]["waldProducesImpossibleBound"] is True
    comparison_csv = read_csv("wald-comparison.csv")
    assert len(comparison_csv) == len(comparison_rows)
    for generated, published in zip(comparison_rows, comparison_csv):
        assert int(published["successes"]) == generated["successes"]
        assert int(published["eligible_opportunities"]) == generated["opportunities"]
        assert close(float(published["observed_rate"]), generated["observedRate"])
        assert close(float(published["wilson_95_lower"]), generated["wilson95Lower"])
        assert close(float(published["wilson_95_upper"]), generated["wilson95Upper"])
        assert close(float(published["wald_95_lower_raw"]), generated["wald95LowerRaw"])
        assert close(float(published["wald_95_upper_raw"]), generated["wald95UpperRaw"])
        assert parse_bool(published["wald_impossible_bound"]) == generated["waldProducesImpossibleBound"]
        assert parse_bool(published["wald_boundary_collapse"]) == generated["waldCollapsesAtBoundary"]

    assert len(model["sources"]) == 6
    assert all(source["accessed"] == "2026-09-04" for source in model["sources"])
    assert any("stable underlying rate" in item for item in model["limitations"])
    assert any("selection issues" in item for item in model["limitations"])

    report = json.loads((ROOT / "verification-report.json").read_text(encoding="utf-8"))
    assert report["status"] == "pass"
    assert report["verifiedAt"] == "2026-09-04T17:35:06Z"
    assert len(report["implementations"]) == 2
    assert report["checks"] == {
        "nistPublishedRoundedLimits": 2,
        "sameRateRows": 6,
        "zeroEventRows": 5,
        "contextRows": 3,
        "decisionThresholdRows": 2,
        "opportunityPlanningRows": 12,
        "waldComparisonRows": 6,
        "sha256Artifacts": 13,
    }

    verify_manifest()
    print(
        "Verified 6 same-rate rows, 5 zero-event rows, 3 context rows, "
        "2 threshold rows, 12 opportunity-plan rows, 6 Wald comparisons, "
        "the NIST 8/30 benchmark, and 13 SHA-256 artifacts."
    )


if __name__ == "__main__":
    main()
