#!/usr/bin/env python3
"""Run and verify MetaCrate's controlled red test-suite baseline."""

from __future__ import annotations

import argparse
from collections import Counter, defaultdict, deque
from dataclasses import dataclass
import json
import os
from pathlib import Path
import re
import subprocess


ROOT = Path(__file__).resolve().parents[1]
CATALOG = ROOT / "tests" / "upstream-tests.json"
BASELINE = ROOT / "tests" / "red-suite-baseline.json"
LIVE_KEYS = ("GRID_USER", "GRID_PASSWORD", "GRID_LOGIN_URL")
SUMMARY_RE = re.compile(
    r"test result: (?:ok|FAILED)\. (\d+) passed; (\d+) failed; "
    r"(\d+) ignored; (\d+) measured; (\d+) filtered out"
)
RESULT_RE = re.compile(
    r"^test (.+) \.\.\. (ok|FAILED|ignored)(?:, .*)?$", re.MULTILINE
)
PANIC_RE = re.compile(r"(?:^|\n)thread '([^']+)'(?: \(\d+\))? panicked at ")
MEMBER_RE = re.compile(
    r'unimplemented C# API member: ([^\n]+)|NotImplemented \{ csharp_member: "([^"]+)" \}'
)


@dataclass(frozen=True)
class LogAudit:
    passed: int
    failed: int
    ignored: int
    measured: int
    filtered: int
    passed_names: tuple[str, ...]
    failed_names: tuple[str, ...]
    ignored_names: tuple[str, ...]
    members: Counter[str]
    nonstandard_failures: tuple[str, ...]


def parse_log(text: str) -> LogAudit:
    """Parse libtest output and associate each failed test with its member ID."""
    summaries = [tuple(map(int, match)) for match in SUMMARY_RE.findall(text)]
    if not summaries:
        raise ValueError("controlled suite log contains no libtest summaries")
    totals = tuple(sum(row[index] for row in summaries) for index in range(5))
    results = RESULT_RE.findall(text)
    names = {
        state: tuple(name for name, result in results if result == state)
        for state in ("ok", "FAILED", "ignored")
    }

    panic_members: dict[str, deque[tuple[str, ...]]] = defaultdict(deque)
    starts = list(PANIC_RE.finditer(text))
    for index, start in enumerate(starts):
        end = starts[index + 1].start() if index + 1 < len(starts) else len(text)
        chunk = text[start.end() : end]
        members = tuple(first or second for first, second in MEMBER_RE.findall(chunk))
        panic_members[start.group(1)].append(members)

    member_counts: Counter[str] = Counter()
    nonstandard: list[str] = []
    for name in names["FAILED"]:
        members = panic_members[name].popleft() if panic_members[name] else ()
        if members:
            member_counts[members[0]] += 1
        else:
            nonstandard.append(name)

    if totals[0] != len(names["ok"]) or totals[1] != len(names["FAILED"]):
        raise ValueError("libtest summaries do not match parsed pass/fail result lines")
    return LogAudit(
        *totals,
        names["ok"],
        names["FAILED"],
        names["ignored"],
        member_counts,
        tuple(nonstandard),
    )


def dotenv_values() -> dict[str, str]:
    values: dict[str, str] = {}
    path = ROOT / ".env"
    if not path.exists():
        return values
    for raw_line in path.read_text().splitlines():
        line = raw_line.strip()
        if not line or line.startswith("#"):
            continue
        line = line.removeprefix("export ")
        if "=" not in line:
            continue
        key, value = line.split("=", 1)
        values[key.strip()] = value.strip().strip("'\"")
    return values


def has_live_credentials() -> bool:
    dotenv = dotenv_values()
    return all((os.environ.get(key) or dotenv.get(key, "")).strip() for key in LIVE_KEYS)


def pending_calls() -> list[str]:
    pending: list[str] = []
    roots = (ROOT / "tests" / "compat" / "tests", ROOT / "crates")
    for path in sorted(file for root in roots for file in root.rglob("*.rs")):
        if re.search(r"\bpending\s*\(", path.read_text()):
            pending.append(path.relative_to(ROOT).as_posix())
    return pending


def verify(log: LogAudit, live: bool) -> None:
    catalog = json.loads(CATALOG.read_text())
    baseline = json.loads(BASELINE.read_text())
    report = catalog["report"]
    expected_cases = baseline["reviewed_cases"]
    if catalog["upstream_commit"] != baseline["upstream_commit"]:
        raise ValueError("red-suite baseline targets a different upstream commit")
    if catalog["expected_cases"] != expected_cases or sum(report.values()) != expected_cases:
        raise ValueError("parity catalog does not contain the fixed reviewed case count")
    expected_report = baseline["parity_report"]
    if report != expected_report:
        raise ValueError(f"parity report changed: expected {expected_report}, found {report}")
    pending = pending_calls()
    if pending:
        raise ValueError("pending( remains in Rust test sources: " + ", ".join(pending))

    by_test = defaultdict(list)
    for case in catalog["tests"]:
        by_test[case["rust_test"]].append(case["id"])
    passed_cases = sorted(
        case_id for name in log.passed_names for case_id in by_test.get(name, [])
    )
    allowed_passes = sorted(baseline["allowed_parity_passes"])
    if passed_cases != allowed_passes:
        raise ValueError(
            f"parity pass baseline changed: expected {allowed_passes}, found {passed_cases}"
        )
    support_passes = log.passed - len(passed_cases)
    if support_passes != baseline["support_passes"]:
        raise ValueError(
            f"support pass baseline changed: expected {baseline['support_passes']}, "
            f"found {support_passes}"
        )

    expected_ignored = 0 if live else report["ignored_live"]
    expected_failed = expected_cases - len(allowed_passes) - expected_ignored
    if (log.failed, log.ignored) != (expected_failed, expected_ignored):
        raise ValueError(
            "controlled totals changed: "
            f"expected failed={expected_failed}, ignored={expected_ignored}; "
            f"found failed={log.failed}, ignored={log.ignored}"
        )
    if log.nonstandard_failures:
        raise ValueError(
            "failures without standardized member IDs: "
            + ", ".join(log.nonstandard_failures)
        )
    if sum(log.members.values()) != log.failed:
        raise ValueError("standardized member aggregation does not cover every failure")


def run_suite() -> tuple[int, str]:
    command = [
        "cargo",
        "test",
        "--workspace",
        "--no-fail-fast",
        "--",
        "--nocapture",
        "--test-threads=1",
    ]
    completed = subprocess.run(command, cwd=ROOT, capture_output=True, text=True, check=False)
    return completed.returncode, completed.stdout + completed.stderr


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--log", type=Path, help="audit an existing captured cargo-test log")
    args = parser.parse_args()
    if args.log:
        return_code, text = 101, args.log.read_text()
    else:
        return_code, text = run_suite()
    if return_code != 101:
        raise SystemExit(f"controlled suite returned {return_code}, expected Cargo test failure 101")
    live = has_live_credentials()
    audit = parse_log(text)
    verify(audit, live)
    print(
        f"controlled red suite is current: passed={audit.passed}, failed={audit.failed}, "
        f"ignored={audit.ignored}, live_credentials={str(live).lower()}, "
        f"standardized_members={len(audit.members)}"
    )
    for member, count in audit.members.most_common():
        print(f"{count:4} {member}")


if __name__ == "__main__":
    main()
