#!/usr/bin/env python3
"""Check behind /blog/several-accounts-one-statement.

1. Writes one synthetic statement holding a checking and a savings account with make_pdfs.py,
   through `uv run --with pikepdf==10.17.0`: each account alone (the controls), the two combined in
   four layouts, and five parts cut out of the combined files page by page.
2. Reads each the way Evidilo's upload and conversion do (pagesOf, readStatementPeriod,
   readStatementAccount, readStatement with no model adapter, the rules lane a page with a text
   layer takes) through read-accounts.harness.ts and vitest, with harness.vitest.config.mjs, and
   compares every row with the transactions the file prints, in the order it prints them.
3. Re-reads Dart Bank's combined statements page and confirms the quoted sentences are still there.

Exit 0: every result matches dataset.csv and every quote is found. Exit 1: a result differs or a
quote is gone. Exit 2: the cited page could not be reached at all (nothing else failed).
`--recheck` runs the same steps (none takes over a minute). `--write` rewrites dataset.csv.
"""

import csv
import html
import json
import os
import re
import subprocess
import sys
import tempfile
import urllib.error
import urllib.request
from pathlib import Path

HERE = Path(__file__).resolve().parent
REPO = HERE.parents[4]  # apps/web/public/blog/<slug> -> repository root
PIKEPDF = "pikepdf==10.17.0"
UA = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
    "(KHTML, like Gecko) Chrome/130.0 Safari/537.36"
)
QUOTES = {
    "https://dart.bank/combined-statements": [
        "Checking, Savings, CDs and loans are able to be combined on one monthly statement.",
        "You will not see loan transaction history, just your balance in the",
    ],
}
FIELDS = [
    "id", "case", "accounts_printed", "rows_read", "rows_matching_print", "row_differences",
    "account_read", "opening_read", "closing_read", "balance_check", "first_broken_row",
    "difference", "lanes",
]


def plain(figure):
    return figure.replace(",", "")


def measure():
    with tempfile.TemporaryDirectory() as tmp:
        pdfs = Path(tmp) / "pdfs"
        subprocess.run(
            ["uv", "run", "-q", "--with", PIKEPDF, "python3", str(HERE / "make_pdfs.py"), str(pdfs)],
            check=True, stdout=subprocess.DEVNULL,
        )
        out = Path(tmp) / "accounts.json"
        env = dict(os.environ, PAGES_DIR=str(pdfs), PAGES_OUT=str(out))
        subprocess.run(
            ["npx", "vitest", "run", "--config", str(HERE / "harness.vitest.config.mjs")],
            cwd=REPO, env=env, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
        )
        read = json.loads(out.read_text())["cases"]
        printed = json.loads((pdfs / "cases.json").read_text())

    cases = {c["id"]: c for c in printed["cases"]}
    accounts = printed["accounts"]
    rows = []
    for case in read:
        spec = cases[case["id"]]
        names = spec["accounts"]
        base = {f: "" for f in FIELDS}
        base.update(id=case["id"], case=spec["label"],
                    accounts_printed=" then ".join(accounts[n]["number"][-4:] for n in names))
        if case["outcome"] != "read":
            rows.append({**base, "rows_read": "refused: " + case.get("message", "")})
            continue
        truth = [r for n in names for r in accounts[n]["rows"]]
        got = case["rows"]
        differences = []
        matching = 0
        for i, (date, description, amount, balance) in enumerate(truth):
            have = got[i] if i < len(got) else None
            if have is None:
                differences.append(f"row {i + 1} missing")
                continue
            diff = []
            if have["date"] != "2026-" + date.replace("/", "-"):
                diff.append(f'date {have["date"]}')
            if have["amount"] != plain(amount):
                diff.append(f'amount {have["amount"]}')
            if have["balance"] != plain(balance):
                diff.append(f'balance {have["balance"]}')
            if have["description"] != description:
                diff.append(f'description "{have["description"]}"')
            if diff:
                differences.append(f"row {i + 1}: " + ", ".join(diff))
            else:
                matching += 1
        if len(got) > len(truth):
            differences.append(f"{len(got) - len(truth)} rows more than printed")
        continuity = case["balanceContinuity"] or {}
        difference = (continuity.get("difference") or {}).get("minorUnits")
        rows.append({
            **base,
            "rows_read": str(len(got)),
            "rows_matching_print": str(matching),
            "row_differences": "; ".join(differences) or "none",
            "account_read": (case["account"] or "none")[-4:],
            "opening_read": case["opening"] or "",
            "closing_read": case["closing"] or "",
            "balance_check": continuity.get("status", "not run"),
            "first_broken_row": str(continuity.get("firstBrokenRow") or ""),
            "difference": "" if difference is None else f"{int(difference) / 100:.2f}",
            "lanes": " ".join(case["lanes"]),
        })
    return rows


def page_text(url):
    req = urllib.request.Request(url, headers={"User-Agent": UA})
    with urllib.request.urlopen(req, timeout=30) as resp:
        raw = resp.read().decode("utf-8", "replace")
    raw = re.sub(r"<script.*?</script>|<style.*?</style>", " ", raw, flags=re.S)
    text = html.unescape(re.sub(r"<[^>]+>", " ", raw))
    return re.sub(r"\s+", " ", text).replace("’", "'")


def check_quotes():
    """0 when every quote is found, 1 when one is gone, 2 when a page could not be read."""
    status = 0
    for url, quotes in QUOTES.items():
        try:
            text = page_text(url)
        except (urllib.error.URLError, TimeoutError) as error:
            print(f"UNREACHABLE {url}: {error}")
            status = max(status, 2)
            continue
        for quote in quotes:
            if quote not in text:
                print(f"FAIL quote gone from {url}: {quote!r}")
                status = 1
    return status


def main():
    rows = measure()
    target = HERE / "dataset.csv"
    if "--write" in sys.argv:
        with target.open("w", newline="") as f:
            w = csv.DictWriter(f, fieldnames=FIELDS)
            w.writeheader()
            w.writerows(rows)

    failed = False
    with target.open(newline="") as f:
        stored = list(csv.DictReader(f))
    if stored != rows:
        failed = True
        stored_by_id = {r["id"]: r for r in stored}
        for row in rows:
            if stored_by_id.get(row["id"]) != row:
                print(f'FAIL {row["id"]}: now {row}, stored {stored_by_id.get(row["id"])}')
        if len(stored) != len(rows):
            print(f"FAIL {len(rows)} cases measured, {len(stored)} stored")

    quotes = check_quotes()
    if failed or quotes == 1:
        return 1
    if quotes == 2:
        return 2
    print(f"ok: {len(rows)} cases match dataset.csv, every quote found")
    return 0


if __name__ == "__main__":
    sys.exit(main())
