#!/usr/bin/env python3
"""Independent verifier for the PLO pot-sized-raise public fixtures."""

from __future__ import annotations

import csv
import json
from pathlib import Path


BUNDLE_DIR = Path(__file__).resolve().parent
JSON_PATH = BUNDLE_DIR / "plo-pot-sized-raise-fixtures.json"
CSV_PATH = BUNDLE_DIR / "plo-pot-sized-raise-results.csv"


def calculate(entry: dict[str, int | None]) -> dict[str, int | bool]:
    pot = entry["sizingPotBeforeAction"]
    amount_to_match = entry["amountToMatch"]
    committed = entry["actorCommittedThisStreet"]
    stack = entry.get("stackBehind")

    for name, value in (
        ("sizingPotBeforeAction", pot),
        ("amountToMatch", amount_to_match),
        ("actorCommittedThisStreet", committed),
    ):
        if not isinstance(value, int) or isinstance(value, bool) or value < 0:
            raise AssertionError(f"{name} is not a non-negative integer")
    if stack is not None and (
        not isinstance(stack, int) or isinstance(stack, bool) or stack < 0
    ):
        raise AssertionError("stackBehind is not null or a non-negative integer")
    if committed > amount_to_match:
        raise AssertionError("actor contribution exceeds amount to match")
    if pot < amount_to_match or pot < committed:
        raise AssertionError("pot does not include all declared wagers")

    call = amount_to_match - committed
    after_call = pot + call
    raise_increment = after_call
    full_add = call + raise_increment
    full_to = committed + full_add
    post_action_pot = pot + full_add
    capped_add = full_add if stack is None else min(stack, full_add)

    return {
        "callAmount": call,
        "sizingPotAfterCall": after_call,
        "maximumRaiseIncrement": raise_increment,
        "chipsAddedForFullPot": full_add,
        "fullPotRaiseTo": full_to,
        "sizingPotAfterFullPotAction": post_action_pot,
        "stackCappedChipsAdded": capped_add,
        "stackCappedWagerTo": committed + capped_add,
        "canCoverCall": stack is None or stack >= call,
        "isStackCapped": stack is not None and stack < full_add,
    }


def csv_value(value: object) -> str:
    if value is None:
        return ""
    if isinstance(value, bool):
        return "true" if value else "false"
    return str(value)


def main() -> None:
    payload = json.loads(JSON_PATH.read_text(encoding="utf-8"))
    fixtures = payload["fixtures"]
    expected_ids = {
        "standard-one-two-open",
        "tda-dead-small-blind",
        "tda-short-big-blind",
        "wsop-dead-small-blind-derived",
        "hundred-pot-hundred-bet",
        "forty-five-pot-first-repot",
        "forty-five-pot-second-repot",
        "big-blind-facing-seven",
        "multiway-bet-and-call",
        "limped-pot-big-blind-option",
        "prior-bet-facing-raise",
        "stack-capped-raise",
    }
    actual_ids = {fixture["id"] for fixture in fixtures}
    if actual_ids != expected_ids:
        raise AssertionError(
            f"fixture IDs changed: missing={expected_ids - actual_ids}, "
            f"extra={actual_ids - expected_ids}"
        )

    results: dict[str, dict[str, object]] = {}
    for fixture in fixtures:
        actual = calculate(fixture["input"])
        if actual != fixture["expected"]:
            raise AssertionError(
                f"{fixture['id']} result mismatch\n"
                f"expected={fixture['expected']}\nactual={actual}"
            )
        results[fixture["id"]] = {
            "id": fixture["id"],
            "label": fixture["label"],
            "claimSet": fixture["claimSet"],
            "rulesProfile": fixture["rulesProfile"],
            "sourceLocation": fixture.get("sourceLocation"),
            "physicalLargestWager": fixture.get("physicalLargestWager"),
            "physicalPotBeforeAction": fixture.get("physicalPotBeforeAction"),
            "physicalPotAfterCall": fixture.get("physicalPotAfterCall"),
            "physicalPotAfterAction": fixture.get("physicalPotAfterAction"),
            **fixture["input"],
            **actual,
            "note": fixture.get("note"),
        }

    repot = results["forty-five-pot-second-repot"]
    if (
        repot["callAmount"],
        repot["sizingPotAfterCall"],
        repot["chipsAddedForFullPot"],
        repot["fullPotRaiseTo"],
    ) != (135, 405, 540, 585):
        raise AssertionError("$45 repot sentinel failed")

    tda = results["tda-dead-small-blind"]
    if (
        tda["sizingPotBeforeAction"],
        tda["amountToMatch"],
        tda["fullPotRaiseTo"],
        tda["physicalPotAfterAction"],
    ) != (300, 200, 700, 900):
        raise AssertionError("Poker TDA Rule 54-B sentinel failed")

    tda_short = results["tda-short-big-blind"]
    if (
        tda_short["physicalLargestWager"],
        tda_short["amountToMatch"],
        tda_short["sizingPotAfterCall"],
        tda_short["fullPotRaiseTo"],
        tda_short["physicalPotAfterCall"],
        tda_short["physicalPotAfterAction"],
    ) != (100, 200, 500, 700, 400, 900):
        raise AssertionError("Poker TDA Rule 54-B short-blind sentinel failed")

    wsop = results["wsop-dead-small-blind-derived"]
    if (
        wsop["sizingPotBeforeAction"],
        wsop["amountToMatch"],
        wsop["fullPotRaiseTo"],
        wsop["physicalPotAfterAction"],
    ) != (200, 200, 600, 800):
        raise AssertionError("WSOP true-value derivation sentinel failed")

    with CSV_PATH.open(newline="", encoding="utf-8") as handle:
        csv_rows = {row["id"]: row for row in csv.DictReader(handle)}
    if set(csv_rows) != expected_ids:
        raise AssertionError("CSV and JSON fixture IDs differ")
    for fixture_id, result in results.items():
        row = csv_rows[fixture_id]
        for key, value in result.items():
            if row[key] != csv_value(value):
                raise AssertionError(
                    f"CSV mismatch for {fixture_id}.{key}: "
                    f"{row[key]!r} != {csv_value(value)!r}"
                )

    print(
        "Verified 12 PLO pot-sized-raise fixtures, the CSV mirror, "
        "and the independent $45-repot/TDA-dead-blind/TDA-short-blind/WSOP sentinels."
    )


if __name__ == "__main__":
    main()
