from __future__ import annotations

import base64
import copy
import hashlib
import json
import os
import pathlib
import sys
import tempfile
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
sys.path.insert(0, str(ROOT / "tests"))

import run_unlinkability_protocol as r
import test_eleventh_generation_audit as eleventh

canon = eleventh.canon
file_sha = eleventh.file_sha
pair = eleventh.pair


class TwelfthGenerationAuditRegressions(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        base = eleventh.EleventhGenerationAuditRegressions
        if not hasattr(base, "runtime"):
            base.setUpClass()
        cls.runtime = base.runtime
        cls.linked = pair("privacy.commitment-reuse", 0)
        cls.unlinked = pair("privacy.commitment-reuse", 1)
        cls.context = {"candidateId": "sha256:" + "a" * 64}

    def isolated(self, source: str, request: bytes | None = None, mutate=None):
        with tempfile.TemporaryDirectory() as text:
            implementation = pathlib.Path(text) / "candidate.py"
            implementation.write_text(source)
            policy = copy.deepcopy(self.runtime)
            policy["candidateImplementationSha256"] = file_sha(implementation)
            if mutate:
                mutate(policy)
            if request is None:
                request = canon({"requestId": "content:" + "a" * 64}) + b"\n"
            return r._run_isolated(policy, implementation, request)

    def responses(self, unique=False, constant=False, extra=False, duplicate=False, unknown=False):
        rows = []
        for corpus_row in (self.linked, self.unlinked):
            for index, side_name in enumerate(("left", "right")):
                side = corpus_row[side_name]
                if constant:
                    output = b"constant"
                elif unique:
                    output = side["contentId"].encode()
                elif corpus_row is self.linked:
                    output = b"linked-output"
                else:
                    output = ("unlinked-" + side_name).encode()
                item = {
                    "requestId": (
                        "content:" + "f" * 64
                        if unknown and not rows
                        else side["contentId"]
                    ),
                    "portabilityOutputBase64": base64.b64encode(output).decode(),
                }
                if extra:
                    item["passed"] = True
                rows.append(item)
        if duplicate:
            rows.append(rows[0])
        return b"".join(canon(row) + b"\n" for row in rows)

    def test_twelfth_01_duplicate_json_key_rejected(self):
        with self.assertRaises(ValueError):
            r.strict_object('{"x":1,"x":2}')

    def test_twelfth_02_nonfinite_json_rejected(self):
        with self.assertRaises(ValueError):
            r.strict_object('{"x":NaN}')

    def test_twelfth_03_caller_adapter_override_rejected(self):
        with self.assertRaisesRegex(ValueError, "caller-selected"):
            r.execute_candidate_protocol("x", "y", "z", candidate_adapter="evil.py")

    def test_twelfth_04_disabled_real_execution_authority(self):
        with self.assertRaisesRegex(ValueError, "structurally disabled"):
            r._policy(ROOT.parents[1])

    def test_twelfth_05_traversal_candidate_path_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            root = pathlib.Path(text)
            (root / "x").write_text("x")
            with self.assertRaises(ValueError):
                r._canonical_regular_file(root, "../x", "candidate")

    def test_twelfth_06_symlink_candidate_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            root = pathlib.Path(text)
            actual = root / "actual.py"
            actual.write_text("pass\n")
            alias = root / "alias.py"
            alias.symlink_to(actual)
            policy = copy.deepcopy(self.runtime)
            policy["candidateImplementationSha256"] = file_sha(actual)
            with self.assertRaises(ValueError):
                r._run_isolated(policy, alias, b"{}\n")

    def test_twelfth_07_hardlink_candidate_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            root = pathlib.Path(text)
            actual = root / "actual.py"
            actual.write_text("pass\n")
            alias = root / "alias.py"
            os.link(actual, alias)
            policy = copy.deepcopy(self.runtime)
            policy["candidateImplementationSha256"] = file_sha(actual)
            with self.assertRaises(ValueError):
                r._run_isolated(policy, alias, b"{}\n")

    def test_twelfth_08_toolchain_binary_mutation_rejected(self):
        policy = copy.deepcopy(self.runtime)
        policy["toolchain"]["pythonSha256"] = "0" * 64
        with self.assertRaisesRegex(ValueError, "python binary digest mismatch"):
            r._validate_exact_host_toolchain(policy)

    def test_twelfth_09_hermetic_env_cwd_network_fs_child_positive(self):
        source = """import base64,json,os,socket,sys
denied=[]
for name,fn in [
 ('fs',lambda:open('/etc/hostname').read()),
 ('net',lambda:socket.create_connection(('127.0.0.1',9),.05)),
 ('child',lambda:os.fork())]:
 try: fn()
 except BaseException: denied.append(name)
env=dict(os.environ)
ok=(set(env)=={'PYTHONHOME','PYTHONHASHSEED','PYTHONDONTWRITEBYTECODE','TZ','LC_ALL','PATH','PWD','VAULTFIRE_RESOURCE_LIMITS'} and env['PWD']=='/work' and os.getcwd()=='/work' and not any('/tmp/' in v or '/home/' in v for v in env.values()))
a=sys.argv;i=a[a.index('--request-jsonl')+1];o=a[a.index('--response-jsonl')+1]
rows=[]
for line in open(i):
 x=json.loads(line); payload=json.dumps({'denied':sorted(denied),'env':ok},sort_keys=True).encode(); rows.append({'requestId':x['requestId'],'portabilityOutputBase64':base64.b64encode(payload).decode()})
open(o,'w').write(''.join(json.dumps(x,sort_keys=True,separators=(',',':'))+'\\n' for x in rows))
"""
        response, measurement = self.isolated(source)
        payload = json.loads(
            base64.b64decode(
                r.strict_object(response.decode().strip())["portabilityOutputBase64"]
            )
        )
        self.assertEqual(payload, {"denied": ["child", "fs", "net"], "env": True})

    def test_twelfth_10_wall_timeout_fails_closed(self):
        with self.assertRaises(Exception):
            self.isolated("while True: pass\n", mutate=lambda p: p["isolation"].__setitem__("wallClockSeconds", 1))

    def test_twelfth_11_stdout_fails_closed(self):
        with self.assertRaises(ValueError):
            self.isolated("print('leak')\n")

    def test_twelfth_12_stderr_fails_closed(self):
        with self.assertRaises(ValueError):
            self.isolated("import sys;sys.stderr.write('leak')\n")

    def test_twelfth_13_nonzero_exit_fails_closed(self):
        with self.assertRaises(ValueError):
            self.isolated("raise SystemExit(4)\n")

    def test_twelfth_14_missing_response_fails_closed(self):
        with self.assertRaises(ValueError):
            self.isolated("pass\n")

    def test_twelfth_15_output_parent_symlink_escape_rejected(self):
        with tempfile.TemporaryDirectory() as text, tempfile.TemporaryDirectory() as outside:
            repo = pathlib.Path(text)
            parent = repo / "pr10/partner-package/evidence/organizational"
            parent.mkdir(parents=True)
            (parent / "privacy-outputs").symlink_to(outside, target_is_directory=True)
            with self.assertRaises(OSError):
                r._canonical_output(
                    repo,
                    "pr10/partner-package/evidence/organizational/privacy-outputs/run/raw-events.jsonl",
                    ".jsonl",
                )

    def test_twelfth_16_interrupted_publication_preserves_old_pointer(self):
        with tempfile.TemporaryDirectory() as text:
            root = pathlib.Path(text)
            old = b'{"generationId":"generation-old"}\n'
            (root / "CURRENT.json").write_bytes(old)
            original = r.os.rename
            calls = {"n": 0}
            def fail_pointer(*args, **kwargs):
                calls["n"] += 1
                if calls["n"] == 2:
                    raise OSError("injected pointer interruption")
                return original(*args, **kwargs)
            r.os.rename = fail_pointer
            try:
                with self.assertRaises(OSError):
                    r._atomic_generation(
                        root,
                        "generation-" + "a" * 64,
                        b"events",
                        b"record",
                        "x/raw-events.jsonl",
                        "x/execution-record.json",
                    )
            finally:
                r.os.rename = original
            self.assertEqual((root / "CURRENT.json").read_bytes(), old)

    def test_twelfth_17_pyc_in_runtime_closure_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            root = pathlib.Path(text)
            (root / "m.py").write_text("x=1\n")
            cache = root / "__pycache__"
            cache.mkdir()
            (cache / "m.pyc").write_bytes(b"bytecode")
            with self.assertRaisesRegex(ValueError, "prohibited bytecode"):
                r._tree_digest(root)

    def test_twelfth_18_symlink_in_runtime_closure_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            root = pathlib.Path(text)
            target = root / "m.py"
            target.write_text("x=1\n")
            (root / "alias.py").symlink_to(target)
            with self.assertRaisesRegex(ValueError, "symlink"):
                r._tree_digest(root)

    def test_twelfth_19_candidate_predicate_field_rejected(self):
        with self.assertRaises(ValueError):
            r._independent_oracle(self.context, [self.linked, self.unlinked], self.responses(extra=True), "privacy.commitment-reuse")

    def test_twelfth_20_duplicate_response_rejected(self):
        with self.assertRaises(ValueError):
            r._independent_oracle(self.context, [self.linked, self.unlinked], self.responses(duplicate=True), "privacy.commitment-reuse")

    def test_twelfth_21_unknown_response_rejected(self):
        with self.assertRaises(ValueError):
            r._independent_oracle(self.context, [self.linked, self.unlinked], self.responses(unknown=True), "privacy.commitment-reuse")

    def test_twelfth_22_unique_every_request_false_negatives_rejected(self):
        balanced = (
            [
                {"expectedRelationship": "linked", "observedRelationship": "linked"}
                for _ in range(r.RELATIONSHIP_SAMPLES_PER_CLASS)
            ]
            + [
                {
                    "expectedRelationship": "unlinked",
                    "observedRelationship": "unlinked",
                }
                for _ in range(r.RELATIONSHIP_SAMPLES_PER_CLASS)
            ]
        )
        self.assertTrue(r._relationship_verdict(balanced)["passed"])
        unique_every_request = copy.deepcopy(balanced)
        for event in unique_every_request:
            if event["expectedRelationship"] == "linked":
                event["observedRelationship"] = "unlinked"
        with self.assertRaisesRegex(ValueError, "verdict failed"):
            r._relationship_verdict(unique_every_request)
        constant_every_request = copy.deepcopy(balanced)
        for event in constant_every_request:
            if event["expectedRelationship"] == "unlinked":
                event["observedRelationship"] = "linked"
        with self.assertRaisesRegex(ValueError, "verdict failed"):
            r._relationship_verdict(constant_every_request)

    def test_twelfth_23_existing_output_symlink_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            repo = pathlib.Path(text)
            parent = repo / "pr10/partner-package/evidence/organizational/privacy-outputs/run"
            parent.mkdir(parents=True)
            actual = parent / "actual.json"
            actual.write_text("x")
            (parent / "receipt.json").symlink_to(actual)
            with self.assertRaises(ValueError):
                r._canonical_output(repo, "pr10/partner-package/evidence/organizational/privacy-outputs/run/receipt.json", ".json")

    def test_twelfth_24_existing_output_hardlink_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            repo = pathlib.Path(text)
            parent = repo / "pr10/partner-package/evidence/organizational/privacy-outputs/run"
            parent.mkdir(parents=True)
            actual = parent / "actual.json"
            actual.write_text("x")
            os.link(actual, parent / "receipt.json")
            with self.assertRaises(ValueError):
                r._canonical_output(repo, "pr10/partner-package/evidence/organizational/privacy-outputs/run/receipt.json", ".json")

    def test_twelfth_25_output_traversal_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            with self.assertRaises(ValueError):
                r._canonical_output(
                    pathlib.Path(text),
                    "pr10/partner-package/evidence/organizational/privacy-outputs/../x.json",
                    ".json",
                )

    def test_twelfth_26_identical_split_targets_rejected(self):
        with tempfile.TemporaryDirectory() as text:
            target = pathlib.Path(text) / "same"
            with self.assertRaisesRegex(ValueError, "split-file"):
                r._atomic_pair(target, b"a", target, b"b")


if __name__ == "__main__":
    unittest.main()
