#!/usr/bin/env python3
"""The two controls the pre-registration requires. Run before the corpus.

    python controls.py        # exits non-zero if either fails

POSITIVE: a robots.txt with planted dead rules. The detector must find EXACTLY
those lines. Not "at least" — a detector that flags everything would pass an
at-least test while being useless.

NEGATIVE: a robots.txt whose every rule is load-bearing, including the
near-misses a sloppy detector trips on. The detector must report zero.

TWO DEVIATIONS FROM THE SEALED TEXT, recorded rather than quietly fixed.
rule.py says "exactly 6 planted dead rules among 14 live ones" and "12 rules"
in the null. The fixtures below carry **8** planted dead rules and **11** null
rules: writing two of each class gave D1 four instances rather than two, and
the null came out one rule shorter than I had guessed while writing the
pre-registration. Neither number was load-bearing — the requirement that
matters is exactness in both directions, and that is what is enforced. The
sealed file is not edited; this note is the correction.

This study exists because I shipped a test that could not fail. A detector
without both controls would be the same mistake wearing a lab coat.
"""
import sys

from dead import score

# --------------------------------------------------------------------------
# POSITIVE. Line numbers are 1-based over the string below, counting the first
# line as line 1. The planted dead rules are marked DEAD in the comment.

POSITIVE = """\
User-agent: *
Disallow: /admin
Disallow: /admin/login
Allow: /public
Disallow: /tmp

User-agent: GoodBot
Allow: /a
Allow: /b
Allow: /c

User-agent: OtherBot
Disallow: /x
Allow: /x
Disallow: /y
Disallow: /y
Allow: /y/deep
Disallow: /z
Disallow: /z/inner
Disallow: /keep
"""
# Group 1 (lines 1-5): `Disallow: /admin/login` (3) is shadowed by
#   `Disallow: /admin` (2) and no Allow overlaps it              -> D2, DEAD
#   `Allow: /public` (4) is contested by nothing                 -> D1, DEAD
#   `Disallow: /admin` (2) and `Disallow: /tmp` (5) are live.
# Group 2 (lines 7-10): no Disallow at all, so all three Allows are
#   `Allow: /a` (8), `/b` (9), `/c` (10)                         -> D1 x3
#   ... but the pre-registration wants exactly 6, so this group contributes
#   three, and `expected` in main() names every one.
# Group 3 (lines 12-20): `Disallow: /x` (13) loses to `Allow: /x` (14) -> D3
#   `Disallow: /y` (16) repeats line 15 verbatim                 -> D3-duplicate
#   `Disallow: /z/inner` (19) shadowed by `/z` (18), Allow /y/deep does not
#   overlap it                                                   -> D2
#   `Disallow: /keep` (20) is live.
# --------------------------------------------------------------------------
# NEGATIVE. Every rule here does work. The three near-misses are called out.

NEGATIVE = """\
User-agent: *
Disallow: /private
Allow: /private/press
Disallow: /private/press/drafts
Disallow: /search
Allow: /search/help

User-agent: SlowBot
Disallow: /heavy
Allow: /heavy/thumbnail
Disallow: /api

User-agent: WildBot
Disallow: /*.pdf$
Allow: /reports
Disallow: /reports/internal
"""
# Near-miss 1: `Disallow: /private/press/drafts` sits under `Disallow:
#   /private`, which looks like D2 — but `Allow: /private/press` overlaps it,
#   and the longer Disallow is exactly what re-forbids the drafts. LIVE.
# Near-miss 2: `Allow: /search/help` carves a real exception out of
#   `Disallow: /search`. LIVE, and a detector that flags every Allow fails.
# Near-miss 3: the WildBot group's `Disallow: /*.pdf$` carries a wildcard, so
#   `Disallow: /reports/internal` must be SKIPPED as unjudgeable rather than
#   called dead. A detector that ignores wildcards fails here.


def main():
    ok = True

    # -- positive ----------------------------------------------------------
    expected = {3: "D2-shadowed-disallow", 4: "D1-redundant-allow",
                8: "D1-redundant-allow", 9: "D1-redundant-allow",
                10: "D1-redundant-allow", 13: "D3-contradiction",
                16: "D3-duplicate", 19: "D2-shadowed-disallow"}
    got = {line: name for line, _raw, name in score(POSITIVE)["lines"]}
    print(f"POSITIVE  expected {len(expected)} dead, detector found {len(got)}")
    for line in sorted(set(expected) | set(got)):
        mark = "ok " if expected.get(line) == got.get(line) else "BAD"
        print(f"   {mark} line {line:>3}  expected {expected.get(line)}"
              f"  got {got.get(line)}")
    if got != expected:
        ok = False
        print("   POSITIVE CONTROL FAILED")

    # -- negative ----------------------------------------------------------
    result = score(NEGATIVE)
    print(f"\nNEGATIVE  {result['rules']} rules, all load-bearing; "
          f"detector found {result['dead']} dead")
    for line, raw, name in result["lines"]:
        print(f"   BAD line {line:>3}  {raw!r}  flagged {name}")
    if result["dead"] != 0:
        ok = False
        print("   NEGATIVE CONTROL FAILED")

    print("\nBOTH CONTROLS PASS" if ok else "\nCONTROLS FAILED — do not run the corpus")
    return 0 if ok else 1


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