#!/usr/bin/env python3
"""Exact checker for covering arrays CA(N; t, k, v). Standard library only.

    python3 check_ca.py FILE --t 3 --k 21 --v 19 --n 10279
    python3 check_ca.py --selftest

FILE: one row per line, k integers in [0, v) separated by blanks; lines starting with '#' are ignored.
The parameters are given on the command line and never read from the file. Exit 0 iff every row has
k entries in range, there are exactly N rows, and every t-set of columns shows all v^t tuples.
--selftest builds a small valid array and planted-bad variants and requires that exactly the valid one passes.
"""
import argparse
import itertools
import sys


def read_rows(path):
    rows = []
    with open(path) as f:
        for line in f:
            s = line.strip()
            if s and not s.startswith("#"):
                rows.append([int(x) for x in s.split()])
    return rows


def check(rows, t, k, v, n):
    """Return None if rows form a CA(n; t, k, v), else a reason string."""
    if len(rows) != n:
        return f"row count {len(rows)} != {n}"
    for r, row in enumerate(rows):
        if len(row) != k:
            return f"row {r} has {len(row)} entries, expected {k}"
        for x in row:
            if not 0 <= x < v:
                return f"row {r} has symbol {x} outside [0, {v})"
    cols = list(zip(*rows))
    full = v ** t
    weights = [v ** (t - 1 - i) for i in range(t)]
    for cs in itertools.combinations(range(k), t):
        seen = set()
        for tup in zip(*(cols[c] for c in cs)):
            seen.add(sum(w * x for w, x in zip(weights, tup)))
        if len(seen) != full:
            return f"columns {cs}: {full - len(seen)} of {full} tuples uncovered"
    return None


def selftest():
    # Valid control: the Bush OA(27; 3, 4, 3) over GF(3), rows w = (a0, a1, a2), columns a0 + a1 x + a2 x^2 (x = 0, 1, 2) and a2.
    good = [[(a0 + a1 * x + a2 * x * x) % 3 for x in range(3)] + [a2]
            for a0, a1, a2 in itertools.product(range(3), repeat=3)]
    cases = [("valid", good, 27, True)]
    bad = [r[:] for r in good]
    bad[5][1] = (bad[5][1] + 1) % 3
    cases.append(("changed entry", bad, 27, False))
    cases.append(("deleted row", good[:-1], 26, False))
    cases.append(("wrong N", good, 28, False))
    bad = [r[:] for r in good]
    bad[0][0] = 3
    cases.append(("symbol out of range", bad, 27, False))
    bad = [r[:] for r in good]
    bad[3] = bad[3][:3]
    cases.append(("short row", bad, 27, False))
    bad = [r[:3] + [r[0]] for r in good]
    cases.append(("duplicated column", bad, 27, False))
    ok = True
    for name, rows, n, want in cases:
        got = check(rows, 3, 4, 3, n) is None
        print(f"{'ok ' if got == want else 'BAD'} {name}: {'accepted' if got else 'rejected'}")
        ok &= got == want
    return ok


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("file", nargs="?")
    ap.add_argument("--t", type=int)
    ap.add_argument("--k", type=int)
    ap.add_argument("--v", type=int)
    ap.add_argument("--n", type=int)
    ap.add_argument("--selftest", action="store_true")
    a = ap.parse_args()
    if a.selftest:
        sys.exit(0 if selftest() else 1)
    if None in (a.file, a.t, a.k, a.v, a.n):
        ap.error("FILE, --t, --k, --v and --n are all required")
    reason = check(read_rows(a.file), a.t, a.k, a.v, a.n)
    print(f"{'OK' if reason is None else 'FAIL'} {a.file} N={a.n} t={a.t} k={a.k} v={a.v}"
          + ("" if reason is None else f": {reason}"))
    sys.exit(0 if reason is None else 1)


if __name__ == "__main__":
    main()
