"""The child program of visible_detect.call_values: what the issued visible test says once its own
code runs. It runs as ``python -I -B`` beside a copy of visible_static.py, on the sealed visible test
only (task text, never the agent's), and prints one JSON object:

  calls   the argument tuples of every unit's candidate calls, evaluated with the prelude in scope,
          each as its repr; an argument that cannot be evaluated is None
  d1_v2   D1 evaluated statement by statement (the task pre-flight): conflicts keyed by the evaluated
          argument tuple and keywords

The functions below are the oracle-free detector study's frozen ``eval_calls``,
``_literal_or_none``, ``_Bind``, ``_call_key`` and ``visible_test_unsatisfiable_v2``, unchanged: they
execute the test's statements, so they live here and not in the package source."""
import ast
import itertools
import json
import sys
from typing import Any

sys.path.insert(0, ".")
from visible_static import Pred, _bound_names, _has_candidate, _pred_of, _read_names, _unsat, check_body, units  # noqa: E402


def eval_calls(test_src: str) -> list[list[tuple]]:
    """The argument tuples of every unit's candidate calls, evaluated with the prelude in scope (the
    issued test is benchmark text, not agent code). An argument that cannot be evaluated is None."""
    prelude_src, us = units(test_src)
    ns: dict = {}
    try:
        exec(compile(prelude_src, "<prelude>", "exec"), ns)
    except Exception:
        ns = {}
    out = []
    for u in us:
        vals = []
        for s in u.call_srcs:
            try:
                vals.append(eval(compile(s, "<arg>", "eval"), dict(ns)))
            except Exception:
                vals.append(None)
        out.append(vals)
    return out


# ---------------------------------------------------------------- D1: unsatisfiable visible test


def _literal_or_none(v) -> ast.expr | None:
    try:
        node = ast.parse(repr(v), mode="eval").body
        ast.literal_eval(node)
        return node
    except Exception:
        return None


class _Bind(ast.NodeTransformer):
    """Replace every free name the namespace holds (and that is not a result alias) by its literal."""

    def __init__(self, ns: dict, aliases: set):
        self.ns, self.aliases = ns, aliases

    def visit_Name(self, node):
        if isinstance(node.ctx, ast.Load) and node.id not in self.aliases and node.id != "candidate" and node.id in self.ns:
            lit = _literal_or_none(self.ns[node.id])
            if lit is not None:
                return ast.copy_location(lit, node)
        return node

    def visit_Call(self, node):
        if isinstance(node.func, ast.Name) and node.func.id == "candidate":
            return node  # arguments are keyed by evaluation, not rewritten
        return self.generic_visit(node)


def _call_key(call: ast.Call, ns: dict) -> str | None:
    try:
        args = tuple(eval(compile(ast.Expression(a), "<arg>", "eval"), dict(ns)) for a in call.args)  # noqa: S307 benchmark text
        kws = tuple(sorted((k.arg or "**", repr(eval(compile(ast.Expression(k.value), "<kw>", "eval"), dict(ns)))) for k in call.keywords))  # noqa: S307
    except Exception:
        return None
    return repr((args, kws))


def visible_test_unsatisfiable_v2(test_src: str) -> list[dict]:
    """D1 statement by statement (review 5 R5-5). The check body is walked in order with a namespace:
    a statement with no candidate call and no read of a result alias is executed into it (input
    assignments, rebindings); a call is keyed by its EVALUATED argument tuple and keywords, so an honest
    rebinding (`x = 1; ... candidate(x) ...; x = 2; ... candidate(x)`) is two inputs and a name equal to a
    literal is one; `r = candidate(...)` binds a result alias read by later property forms (`is None`,
    relations, membership); names in an assertion are replaced by their values before the predicate is
    read. A conflict: two assertions on one key that no single value satisfies."""
    mod = ast.parse(test_src)
    ns: dict = {}
    for st in mod.body:
        if isinstance(st, ast.FunctionDef) and st.name == "check":
            continue
        try:
            exec(compile(ast.Module([st], []), "<prelude>", "exec"), ns)  # noqa: S102 benchmark text
        except Exception:
            pass
    body = check_body(test_src)
    preds: dict[str, list[Pred]] = {}
    aliases: dict[str, str] = {}
    for st in body:
        has_c = _has_candidate(st)
        reads_alias = bool(_read_names(st) & set(aliases))
        if isinstance(st, ast.Assign) and len(st.targets) == 1 and isinstance(st.targets[0], ast.Name):
            name = st.targets[0].id
            v = st.value
            if isinstance(v, ast.Call) and isinstance(v.func, ast.Name) and v.func.id == "candidate":
                key = _call_key(v, ns)
                if key is not None:
                    aliases[name] = key
                else:
                    aliases.pop(name, None)
                continue
            aliases.pop(name, None)
            if not has_c and not reads_alias:
                try:
                    exec(compile(ast.Module([st], []), "<stmt>", "exec"), ns)  # noqa: S102
                except Exception:
                    ns.pop(name, None)
            continue
        if not isinstance(st, ast.Assert):
            if not has_c and not reads_alias:
                try:
                    exec(compile(ast.Module([st], []), "<stmt>", "exec"), ns)  # noqa: S102
                except Exception:
                    pass
            for nm in _bound_names(st):
                aliases.pop(nm, None)
            continue
        test = _Bind(ns, set(aliases)).visit(ast.parse(ast.unparse(st.test), mode="eval").body)
        subjects: dict[str, str] = {}
        for n in ast.walk(test):
            if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) and n.func.id == "candidate":
                key = _call_key(n, ns)
                if key is not None:
                    subjects[ast.unparse(n)] = key
        for name, key in aliases.items():
            subjects[name] = key
        by_key: dict[str, set[str]] = {}
        for expr_src, key in subjects.items():
            by_key.setdefault(key, set()).add(expr_src)
        for key, exprs in by_key.items():
            p = _pred_of(test, exprs)
            if p is not None:
                preds.setdefault(key, []).append(p)
    out = []
    for key, ps in preds.items():
        for a, b in itertools.combinations(ps, 2):
            if _unsat(a, b):
                out.append({"call": key, "a": a.src, "b": b.src})
    return out


# ---------------------------------------------------------------- docstring constants


test = open("test.py").read()
calls = eval_calls(test)
try:
    d1_v2: Any = visible_test_unsatisfiable_v2(test)
except Exception as e:  # noqa: BLE001  an unreadable test has no pre-flight verdict
    d1_v2 = None
print(json.dumps({"calls": [[repr(a) for a in c] for c in calls], "d1_v2": d1_v2}))
