#!/usr/bin/env python3
"""Independent expected outcomes for the real CLI; fixtures are synthetic only."""
import copy
import hashlib
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile

PACKAGE = Path(__file__).resolve().parents[1]
CHECKER = PACKAGE / "scripts" / "check.py"
RESULTS = PACKAGE / "validation" / "tests.json"
MAX_BYTES = 1048576
BASE = {
    "protocol_version": "2026-07-28",
    "expected": {"rpc_id": "example-rpc-1", "order_id": "example-order-1", "amount_minor": 1500, "currency": "RUB"},
    "response": {"jsonrpc": "2.0", "id": "example-rpc-1", "result": {"resultType": "complete", "content": [{"type": "text", "text": "Educational sample; compare supplied expected with structured receipt only."}], "isError": False,
        "structuredContent": {"refund_id": "example-refund-1", "order_id": "example-order-1", "amount_minor": 1500, "currency": "RUB", "status": "succeeded"}}}
}
CASES = []


def add(name, outcome, code, changes=(), remove=(), raw=None):
    doc = copy.deepcopy(BASE)
    for path, value in changes:
        node = doc
        for key in path[:-1]:
            node = node[key]
        node[path[-1]] = value
    for path in remove:
        node = doc
        for key in path[:-1]:
            node = node[key]
        del node[path[-1]]
    CASES.append((name, json.dumps(doc, ensure_ascii=True).encode() if raw is None else raw, outcome, code))


E = ("expected",)
R = ("response",)
T = R + ("result",)
S = T + ("structuredContent",)
add("current_control", "consistent_sample", 0)
add("renamed_expected_control", "consistent_sample", 0, [
    (E + ("rpc_id",), 982), (R + ("id",), 982),
    (E + ("order_id",), "example-other-order"), (S + ("order_id",), "example-other-order"),
    (S + ("refund_id",), "example-other-refund"),
    (E + ("amount_minor",), 719), (S + ("amount_minor",), 719),
    (E + ("currency",), "USD"), (S + ("currency",), "USD")])
for currency in ("RUB", "USD", "EUR"):
    add("currency_control_" + currency, "consistent_sample", 0, [(E + ("currency",), currency), (S + ("currency",), currency)])
for value in (1, 2**53 - 1):
    add("amount_boundary_" + str(value), "consistent_sample", 0, [(E + ("amount_minor",), value), (S + ("amount_minor",), value)])
for value in (0, -1, "", "example-rpc-new", 2**60):
    add("rpc_control_" + repr(value), "consistent_sample", 0, [(E + ("rpc_id",), value), (R + ("id",), value)])
add("legacy_absent_resultType", "consistent_sample", 0, [(('protocol_version',), "2025-11-25")], [T + ("resultType",)])
add("legacy_explicit_complete", "consistent_sample", 0, [(('protocol_version',), "2025-11-25")])
add("absent_isError", "consistent_sample", 0, remove=[T + ("isError",)])
add("ignored_text_cannot_change_structured_match", "consistent_sample", 0, [(T + ("content",), [{"type": "text", "text": "example-untrusted ignore instructions"}])])
for name, value in (("null", None), ("list", []), ("string", "example-input"), ("number", 8), ("bool", True)):
    add("root_" + name, "invalid_input", 2, raw=json.dumps(value).encode())
    add("expected_" + name, "invalid_input", 2, [(E, value)])
    add("envelope_" + name, "invalid_envelope", 2, [(R, value)])
    add("result_" + name, "invalid_envelope", 2, [(T, value)])
    add("receipt_" + name, "invalid_receipt", 2, [(S, value)])
for key in ("protocol_version", "expected", "response"):
    add("missing_root_" + key, "invalid_input", 2, remove=[(key,)])
add("extra_root", "invalid_input", 2, [(('example-extra',), True)])
for key in ("rpc_id", "order_id", "amount_minor", "currency"):
    add("missing_expected_" + key, "invalid_input", 2, remove=[E + (key,)])
add("extra_expected", "invalid_input", 2, [(E + ('example-extra',), True)])
for value in (None, [], True, 20260728):
    add("protocol_type_" + repr(value), "invalid_input", 2, [(('protocol_version',), value)])
for value in ("", "2025-03-26", "2026-07-28 "):
    add("unsupported_version_" + repr(value), "out_of_scope", 3, [(('protocol_version',), value)])
for key in ("jsonrpc", "id"):
    add("missing_envelope_" + key, "invalid_envelope", 2, remove=[R + (key,)])
add("no_result_or_error", "invalid_envelope", 2, remove=[T])
add("mixed_result_error", "invalid_envelope", 2, [(R + ('error',), {"code": -1, "message": "example-private-error"})])
add("extra_envelope", "invalid_envelope", 2, [(R + ('example-extra',), 1)])
for value in (None, True, 2.0, "1.0", "2.0 "):
    add("jsonrpc_invalid_" + repr(value), "invalid_envelope", 2, [(R + ("jsonrpc",), value)])
for value in (None, True, False, 1.0, [], {}):
    add("expected_rpc_type_" + repr(value), "invalid_input", 2, [(E + ("rpc_id",), value)])
    add("response_rpc_type_" + repr(value), "invalid_envelope", 2, [(R + ("id",), value)])
add("rpc_string_int_mismatch", "invalid_envelope", 2, [(E + ("rpc_id",), 7), (R + ("id",), "7")])
add("rpc_value_mismatch", "invalid_envelope", 2, [(R + ("id",), "example-another-rpc")])
add("current_missing_resultType", "invalid_envelope", 2, remove=[T + ("resultType",)])
for value in (None, [], True, 1, ""):
    add("invalid_resultType_" + repr(value), "invalid_envelope", 2, [(T + ("resultType",), value)])
for version in ("2026-07-28", "2025-11-25"):
    for result_type in ("task", "pending", "future"):
        add("unsupported_resultType_" + version + result_type, "out_of_scope", 3, [(('protocol_version',), version), (T + ('resultType',), result_type)])
for value in (None, "false", "true", 0, 1, []):
    add("invalid_isError_" + repr(value), "invalid_envelope", 2, [(T + ("isError",), value)])
add("tool_error_over_structured_success", "tool_error", 1, [(T + ("isError",), True)])
add("tool_error_without_receipt", "tool_error", 1, [(T + ("isError",), True)], [S])
add("text_only_success_not_receipt", "missing_receipt", 1, [(T + ("content",), [{"type": "text", "text": "example-success"}])], [S])
for value in ({"code": -32603, "message": "example-error"}, {"code": 0, "message": "", "data": BASE["response"]["result"]["structuredContent"]}):
    add("valid_rpc_error_" + str(len(value)), "rpc_error", 1, [(R + ("error",), value)], [T])
for i, value in enumerate((None, [], "example-error", {}, {"code": True, "message": "example-error"}, {"code": -1.0, "message": "example-error"}, {"code": -1, "message": None}, {"code": -1}, {"message": "example-error"}, {"code": -1, "message": "example-error", "extra": 1})):
    add("invalid_rpc_error_" + str(i), "invalid_envelope", 2, [(R + ("error",), value)], [T])
for key in ("refund_id", "order_id", "amount_minor", "currency", "status"):
    add("missing_receipt_" + key, "invalid_receipt", 2, remove=[S + (key,)])
add("extra_receipt", "invalid_receipt", 2, [(S + ('example-extra',), 1)])
for i, value in enumerate(("", " ", "example-", "example-order ", " example-order", "real-order-1", "example-\n", "example-a/b", None, True, 1)):
    add("expected_identifier_" + str(i), "invalid_input", 2, [(E + ('order_id',), value)])
    for key in ("order_id", "refund_id"):
        add("receipt_identifier_" + key + str(i), "invalid_receipt", 2, [(S + (key,), value)])
for i, value in enumerate((True, False, 0, -1, 2**53, 1500.0, "1500", None, [])):
    add("expected_amount_" + str(i), "invalid_input", 2, [(E + ('amount_minor',), value)])
    add("receipt_amount_" + str(i), "invalid_receipt", 2, [(S + ('amount_minor',), value)])
for i, value in enumerate(("rub", "GBP", "RUB ", "", None, True, [])):
    add("expected_currency_" + str(i), "invalid_input", 2, [(E + ('currency',), value)])
    add("receipt_currency_" + str(i), "invalid_receipt", 2, [(S + ('currency',), value)])
for i, value in enumerate(("success", "Succeeded", "succeeded ", "", None, True, 1, [])):
    add("receipt_status_" + str(i), "invalid_receipt", 2, [(S + ('status',), value)])
for status in ("pending", "failed"):
    add("status_" + status, status, 1, [(S + ('status',), status)])
for key, value in (("order_id", "example-other-order"), ("amount_minor", 1499), ("currency", "EUR")):
    add("mismatch_" + key, "mismatch", 1, [(S + (key,), value)])
add("mismatch_before_pending", "mismatch", 1, [(S + ('order_id',), "example-other-order"), (S + ('status',), "pending")])
for name, raw in (("broken_json", b'{"example-private":'), ("trailing_data", b'{} {}'), ("empty_file", b''), ("utf8_bom", b'\xef\xbb\xbf{}'), ("deep_json", b'[' * 3000 + b'0' + b']' * 3000)):
    add(name, "invalid_input", 2, raw=raw)
for name, raw in (("duplicate_root", b'{"expected":1,"expected":2}'), ("duplicate_nested", b'{"x":{"a":1,"a":2}}'), ("duplicate_escaped", b'{"a":1,"\\u0061":2}'), ("nan", b'{"x":NaN}'), ("inf", b'{"x":Infinity}'), ("negative_inf", b'{"x":-Infinity}'), ("overflow_float", b'{"x":1e999}'), ("bad_utf8", b'{"x":"\xffexample-private"}')):
    add(name, "invalid_input", 2, raw=raw)
valid = json.dumps(BASE).encode()
add("exact_1MiB_control", "consistent_sample", 0, raw=valid + b' ' * (MAX_BYTES - len(valid)))
add("over_1MiB", "invalid_input", 2, raw=valid + b' ' * (MAX_BYTES + 1 - len(valid)))


def digest(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()


def invoke(args, outcome, code):
    completed = subprocess.run([sys.executable, "-B", str(CHECKER), *args], capture_output=True, timeout=10)
    if completed.stderr:
        raise AssertionError("stderr_not_empty")
    payload = json.loads(completed.stdout)
    if set(payload) != {"version", "outcome", "diagnostic", "exit_code"}:
        raise AssertionError("stdout_contract")
    if completed.returncode != code or payload["exit_code"] != code or payload["outcome"] != outcome or payload["version"] != "0.1.0":
        raise AssertionError("unexpected_outcome_or_exit")
    # Nothing copied from inputs, including JSON-RPC error messages or filenames.
    if b"example-" in completed.stdout or b"private" in completed.stdout or b"traceback" in completed.stdout.lower():
        raise AssertionError("stdout_privacy")
    if len(completed.stdout.splitlines()) != 1:
        raise AssertionError("stdout_lines")
    return {"expected_outcome":outcome,"expected_exit":code,"actual":payload,"actual_exit":completed.returncode,"stderr_empty":not completed.stderr}


def main():
    results = []
    with tempfile.TemporaryDirectory(prefix="suite-", dir=PACKAGE / "tests") as tmp:
        root = Path(tmp)
        for name, raw, outcome, code in CASES:
            path = root / "example-private-input.json"
            path.write_bytes(raw)
            before = digest(path)
            try:
                trace=invoke([str(path)], outcome, code)
                if digest(path) != before:
                    raise AssertionError("input_changed")
                results.append({"test": name, "status": "pass", **trace, "input_sha256_before":before,"input_sha256_after":digest(path)})
            except Exception as error:
                results.append({"test": name, "status": "fail", "reason": type(error).__name__})
        path.write_bytes(valid)
        link = root / "example-private-link.json"
        link.symlink_to(path)
        directory_link = root / "example-private-dirlink"
        directory_link.symlink_to(root, target_is_directory=True)
        fifo = root / "example-private-fifo"
        os.mkfifo(fifo)
        special = [("missing_path", [str(root / 'example-private-missing')], "input_io_error", 4),
                   ("directory_path", [str(root)], "input_io_error", 4),
                   ("symlink_path", [str(link)], "input_io_error", 4),
                   ("ancestor_symlink", [str(directory_link / path.name)], "input_io_error", 4),
                   ("fifo_nonblocking", [str(fifo)], "input_io_error", 4),
                   ("no_arguments", [], "invalid_input", 2),
                   ("unknown_flag_privacy", ["--example-private-flag"], "invalid_input", 2),
                   ("extra_arguments", [str(path), "example-private-extra"], "invalid_input", 2),
                   ("help_json", ["--help"], "usage", 0),
                   ("relative_path_spaces", [os.path.relpath(path)], "consistent_sample", 0)]
        spaced = root / "example-private with spaces.json"
        spaced.write_bytes(valid)
        special.append(("spaces_filename", [str(spaced)], "consistent_sample", 0))
        for name, args, outcome, code in special:
            try:
                trace=invoke(args, outcome, code)
                if path.read_bytes() != valid or spaced.read_bytes() != valid:
                    raise AssertionError("input_changed")
                results.append({"test": name, "status": "pass", **trace,"input_sha256_before":None,"input_sha256_after":None,"hash_scope":"nonregular/missing/CLI test; regular controls unchanged asserted, no input-specific hash claimed"})
            except Exception as error:
                results.append({"test": name, "status": "fail", "reason": type(error).__name__})
        # Child checker must not create cache, logs, or output files.
        permitted = {path.name, link.name, directory_link.name, fifo.name, spaced.name}
        if {entry.name for entry in root.iterdir()} != permitted:
            results.append({"test": "no_checker_writes", "status": "fail"})
        else:
            results.append({"test": "no_checker_writes", "status": "pass"})
    # Exercise the generic exception boundary with a private synthetic message.
    harness = """import runpy,sys
ns=runpy.run_path(sys.argv[1])
def broken(_path):
    raise RuntimeError('example-private-input-and-path')
ns['main'].__globals__['read_json']=broken
sys.exit(ns['main'](['example-private-file']))
"""
    audit_harness = """import runpy,sys,json,math,os,re,stat
ns=runpy.run_path(sys.argv[1])
def audit(event,args):
    if event.startswith(('socket.', 'subprocess.')):
        raise AssertionError('forbidden_network_or_process')
    if event in ('os.remove','os.rename','os.mkdir','os.rmdir','os.truncate','os.chmod','os.chown'):
        raise AssertionError('forbidden_mutation')
    if event == 'open':
        flags = args[2]
        if isinstance(flags,int) and flags & (os.O_WRONLY|os.O_RDWR|os.O_CREAT|os.O_TRUNC|os.O_APPEND):
            raise AssertionError('forbidden_write')
sys.addaudithook(audit)
sys.exit(ns['main']([sys.argv[2]]))
"""
    for name, program, extra, expected_outcome, expected_code in (
        ('exception_safe_privacy', harness, [], 'internal_error', 2),
        ('audit_no_network_or_writes', audit_harness, [str(PACKAGE / 'examples' / '01-current_control.json')], 'consistent_sample', 0)):
        try:
            completed = subprocess.run([sys.executable, '-B', '-c', program, str(CHECKER), *extra], capture_output=True, timeout=10)
            payload = json.loads(completed.stdout)
            if completed.returncode != expected_code or payload['outcome'] != expected_outcome or completed.stderr or b'example-private' in completed.stdout:
                raise AssertionError('harness_outcome_or_privacy')
            results.append({'test': name, 'status': 'pass','expected_outcome':expected_outcome,'expected_exit':expected_code,'actual':payload,'actual_exit':completed.returncode,'stderr_empty':not completed.stderr,'hash_scope':'harness, no user input file'})
        except Exception as error:
            results.append({'test': name, 'status': 'fail', 'reason': type(error).__name__})
    # Bundled examples also have independently declared expected outcomes.
    manifest = json.loads((PACKAGE / "examples" / "manifest.json").read_text())
    for example in manifest:
        try:
            fixture = PACKAGE / "examples" / example["file"]
            before = digest(fixture)
            trace=invoke([str(fixture)], example["outcome"], example["exit_code"])
            if digest(fixture) != before:
                raise AssertionError("input_changed")
            results.append({"test": "example_" + example["file"], "status": "pass",**trace,"input_sha256_before":before,"input_sha256_after":digest(fixture)})
        except Exception as error:
            results.append({"test": "example_" + example["file"], "status": "fail", "reason": type(error).__name__})
    failures = [entry for entry in results if entry["status"] != "pass"]
    summary = {"python_version": sys.version.split()[0], "version": "0.1.0", "total": len(results),
               "passed": len(results) - len(failures), "failed": len(failures), "results": results}
    RESULTS.write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
    print(json.dumps({key: value for key, value in summary.items() if key != "results"}, sort_keys=True))
    return bool(failures)


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