#!/usr/bin/env python3
"""Offline teaching fixture: observed repeated-trial counts, not a benchmark estimator.
Run without arguments for synthetic demonstration and unit tests.
Run with a JSON manifest for validation and descriptive counts. Exit 1 on bad input.
A zero exit validates the input, not a release decision. No network or model calls.
"""
import copy
import json
from pathlib import Path
import sys
import unittest


def summarise(data):
    if not isinstance(data, dict) or set(data) != {"tasks", "k", "trials"}:
        raise ValueError("expected tasks, k and trials")
    tasks, k, trials = data["tasks"], data["k"], data["trials"]
    if not isinstance(tasks, list) or not tasks:
        raise ValueError("tasks must be a nonempty list")
    if any(not isinstance(t, str) or not t or t != t.strip() for t in tasks):
        raise ValueError("invalid task identifier")
    if len(set(tasks)) != len(tasks):
        raise ValueError("duplicate task identifier")
    if type(k) is not int or k < 1:
        raise ValueError("k must be a positive integer")
    if not isinstance(trials, list):
        raise ValueError("trials must be a list")
    expected = {(task, attempt) for task in tasks for attempt in range(1, k + 1)}
    observed = {}
    for row in trials:
        if not isinstance(row, dict) or set(row) != {"task", "attempt", "outcome"}:
            raise ValueError("invalid trial record")
        task, attempt, outcome = row["task"], row["attempt"], row["outcome"]
        if not isinstance(task, str) or type(attempt) is not int:
            raise ValueError("invalid trial identity")
        key = (task, attempt)
        if key not in expected or key in observed:
            raise ValueError("unexpected or duplicate trial")
        if not isinstance(outcome, str) or outcome not in {"pass", "fail", "timeout", "error", "unknown"}:
            raise ValueError("invalid outcome")
        observed[key] = outcome
    if set(observed) != expected:
        raise ValueError("missing scheduled trial; retain its explicit outcome")
    groups = [[observed[(t, i)] for i in range(1, k + 1)] for t in tasks]
    return {
        "tasks": len(tasks), "trials_per_task": k, "scheduled_trials": len(expected),
        "first_trial_pass_tasks": sum(g[0] == "pass" for g in groups),
        "any_pass_tasks": sum("pass" in g for g in groups),
        "all_pass_tasks": sum(all(v == "pass" for v in g) for g in groups),
        "passed_trials": sum(v == "pass" for v in observed.values()),
        "outcomes": {s: sum(v == s for v in observed.values())
                     for s in ("pass", "fail", "timeout", "error", "unknown")},
    }


def fixture():
    sequences = {"A": ["pass", "pass", "pass"], "B": ["fail", "pass", "fail"],
                 "C": ["fail", "fail", "pass"], "D": ["pass", "timeout", "fail"]}
    return {"tasks": list(sequences), "k": 3,
            "trials": [{"task": t, "attempt": i, "outcome": value}
                       for t, values in sequences.items() for i, value in enumerate(values, 1)]}


class Tests(unittest.TestCase):
    def test_counts(self):
        result = summarise(fixture())
        self.assertEqual([result[x] for x in ("tasks", "scheduled_trials", "first_trial_pass_tasks", "any_pass_tasks", "all_pass_tasks", "passed_trials")], [4, 12, 2, 4, 1, 6])
        self.assertEqual(result["outcomes"]["timeout"], 1)

    def test_missing(self):
        data = fixture(); data["trials"].pop()
        with self.assertRaises(ValueError): summarise(data)

    def test_duplicate(self):
        data = fixture(); data["trials"].append(copy.deepcopy(data["trials"][0]))
        with self.assertRaises(ValueError): summarise(data)

    def test_extra_task(self):
        data = fixture(); data["trials"][0]["task"] = "E"
        with self.assertRaises(ValueError): summarise(data)

    def test_unknown_retained(self):
        data = fixture(); data["trials"][0]["outcome"] = "unknown"
        result = summarise(data)
        self.assertEqual(result["all_pass_tasks"], 0)
        self.assertEqual(result["scheduled_trials"], 12)
        self.assertEqual(result["outcomes"]["unknown"], 1)

    def test_invalid_status(self):
        data = fixture(); data["trials"][0]["outcome"] = "success"
        with self.assertRaises(ValueError): summarise(data)

    def test_invalid_k(self):
        for k in (True, 0, -1, 1.5, "3"):
            data = fixture(); data["k"] = k
            with self.subTest(k=k), self.assertRaises(ValueError): summarise(data)

    def test_invalid_tasks(self):
        for tasks in ([], ["A", "A"], [" A"], [None]):
            data = fixture(); data["tasks"] = tasks
            with self.subTest(tasks=tasks), self.assertRaises(ValueError): summarise(data)

    def test_attempt_identity(self):
        for attempt in (True, 0, 4, "1"):
            data = fixture(); data["trials"][0]["attempt"] = attempt
            with self.subTest(attempt=attempt), self.assertRaises(ValueError): summarise(data)

    def test_order_independent(self):
        data = fixture(); data["trials"].reverse()
        self.assertEqual(summarise(data), summarise(fixture()))

    def test_all_fail(self):
        data = fixture()
        for row in data["trials"]: row["outcome"] = "fail"
        result = summarise(data)
        self.assertEqual(result["any_pass_tasks"], 0)
        self.assertEqual(result["all_pass_tasks"], 0)

    def test_schema(self):
        for data in ([], {}, {"tasks": ["A"], "k": 1, "trials": [None]}):
            with self.subTest(data=data), self.assertRaises(ValueError): summarise(data)


if __name__ == "__main__":
    if len(sys.argv) == 1:
        print(json.dumps(summarise(fixture()), indent=2), flush=True)
        unittest.main(argv=[sys.argv[0]])
    elif len(sys.argv) == 2:
        try:
            print(json.dumps(summarise(json.loads(Path(sys.argv[1]).read_text())), indent=2))
        except (ValueError, OSError) as exc:
            print("INVALID: " + str(exc), file=sys.stderr)
            sys.exit(1)
    else:
        sys.exit("usage: agent-repeated-trials.py [manifest.json]")
