#!/usr/bin/env python3
"""Offline paired-order accounting fixture. No model calls or release verdict.

Input: {"cases": ["c1"], "judgements": [
 {"case": "c1", "order": "AB", "winner": "first"},
 {"case": "c1", "order": "BA", "winner": "second"}]}
A and B are stable answer identities. Winners: first, second, tie, unknown.
"""
import copy
import json
import sys
import unittest


def audit(data):
    if not isinstance(data, dict) or set(data) != {"cases", "judgements"}:
        raise ValueError("expected cases and judgements")
    cases = data["cases"]
    if (not isinstance(cases, list) or not cases or
            any(not isinstance(c, str) or not c.strip() for c in cases) or
            len(set(cases)) != len(cases)):
        raise ValueError("cases must be unique nonempty strings")
    if not isinstance(data["judgements"], list):
        raise ValueError("judgements must be a list")
    pairs = {c: {} for c in cases}
    first = decisive = 0
    for row in data["judgements"]:
        if not isinstance(row, dict) or set(row) != {"case", "order", "winner"}:
            raise ValueError("invalid judgement fields")
        c, order, winner = row["case"], row["order"], row["winner"]
        if (not isinstance(c, str) or c not in pairs or
                order not in ("AB", "BA") or
                winner not in ("first", "second", "tie", "unknown")):
            raise ValueError("invalid case, order or winner")
        if order in pairs[c]:
            raise ValueError("duplicate case/order")
        mapped = order[0] if winner == "first" else order[1] if winner == "second" else winner
        pairs[c][order] = mapped
        if winner in ("first", "second"):
            decisive += 1
            first += winner == "first"
    counts = dict(stable_A=0, stable_B=0, stable_tie=0,
                  order_disagreement=0, unknown_pairs=0)
    for pair in pairs.values():
        if set(pair) != {"AB", "BA"}:
            raise ValueError("missing case/order; record unknown explicitly")
        a, b = pair["AB"], pair["BA"]
        if "unknown" in (a, b):
            counts["unknown_pairs"] += 1
        elif a != b:
            counts["order_disagreement"] += 1
        else:
            counts["stable_" + a] += 1
    complete = len(cases) - counts["unknown_pairs"]
    return dict(cases=len(cases), complete_pairs=complete, **counts,
                disagreement_fraction=(counts["order_disagreement"] / complete if complete else None),
                first_position_wins=first, decisive_judgements=decisive,
                first_position_fraction=(first / decisive if decisive else None))


def fixture():
    outcomes = [("first", "second"), ("first", "first"),
                ("tie", "tie"), ("second", "unknown")]
    return {"cases": ["c1", "c2", "c3", "c4"], "judgements": [
        {"case": f"c{i}", "order": order, "winner": win}
        for i, wins in enumerate(outcomes, 1)
        for order, win in zip(("AB", "BA"), wins)]}


class Tests(unittest.TestCase):
    def test_fixture(self):
        r = audit(fixture())
        self.assertEqual([r[k] for k in ("stable_A", "stable_B", "stable_tie", "order_disagreement", "unknown_pairs")], [1, 0, 1, 1, 1])
        self.assertEqual(r["disagreement_fraction"], 1 / 3)
        self.assertEqual(r["first_position_fraction"], 3 / 5)

    def test_reversed_rows(self):
        d = fixture(); d["judgements"].reverse()
        self.assertEqual(audit(d), audit(fixture()))

    def test_stable_b(self):
        d = {"cases": ["c"], "judgements": [
            {"case": "c", "order": "AB", "winner": "second"},
            {"case": "c", "order": "BA", "winner": "first"}]}
        self.assertEqual(audit(d)["stable_B"], 1)

    def test_missing(self):
        d = fixture(); d["judgements"].pop()
        with self.assertRaises(ValueError): audit(d)

    def test_duplicate(self):
        d = fixture(); d["judgements"].append(copy.deepcopy(d["judgements"][0]))
        with self.assertRaises(ValueError): audit(d)

    def test_unknown_case(self):
        d = fixture(); d["judgements"][0]["case"] = "absent"
        with self.assertRaises(ValueError): audit(d)

    def test_bad_winner(self):
        d = fixture(); d["judgements"][0]["winner"] = "A"
        with self.assertRaises(ValueError): audit(d)

    def test_bad_order(self):
        d = fixture(); d["judgements"][0]["order"] = "AA"
        with self.assertRaises(ValueError): audit(d)

    def test_bad_inventory(self):
        for cases in ([], ["c", "c"], [""], [3], "c"):
            d = fixture(); d["cases"] = cases
            with self.assertRaises(ValueError): audit(d)

    def test_all_unknown(self):
        d = fixture()
        for row in d["judgements"]: row["winner"] = "unknown"
        r = audit(d)
        self.assertIsNone(r["disagreement_fraction"])
        self.assertIsNone(r["first_position_fraction"])
        self.assertEqual(r["unknown_pairs"], 4)

    def test_tie_change_is_disagreement(self):
        d = fixture(); d["judgements"][0]["winner"] = "tie"
        self.assertEqual(audit(d)["order_disagreement"], 2)

    def test_malformed(self):
        for d in (None, {}, {"cases": ["x"], "judgements": [None]}):
            with self.assertRaises(ValueError): audit(d)


if __name__ == "__main__":
    if len(sys.argv) == 1:
        print(json.dumps(audit(fixture()), indent=2))
        result = unittest.TextTestRunner(verbosity=2).run(unittest.defaultTestLoader.loadTestsFromTestCase(Tests))
        sys.exit(0 if result.wasSuccessful() else 1)
    if len(sys.argv) != 2:
        sys.exit("usage: judge-order-audit.py [manifest.json]")
    try:
        with open(sys.argv[1], encoding="utf-8") as handle:
            print(json.dumps(audit(json.load(handle)), indent=2))
    except (ValueError, OSError) as exc:
        print(str(exc), file=sys.stderr)
        sys.exit(1)
