from __future__ import annotations

import base64
import copy
import hashlib
import json
import os
import pathlib
import re
import shutil
import subprocess
import sys
import tempfile
import unittest

ROOT = pathlib.Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
import check_organizational_readiness as c
import run_unlinkability_protocol as r
import strict_json_schema as s


def canon(value):
    return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()


def side(tag):
    raw = canon({"tag": tag})
    body = {
        "subjectCommitmentSha256": hashlib.sha256(("s" + tag).encode()).hexdigest(),
        "partnerContextSha256": hashlib.sha256(("p" + tag).encode()).hexdigest(),
        "chainId": "base",
        "nonceSha256": hashlib.sha256(("n" + tag).encode()).hexdigest(),
        "caseInputBase64": base64.b64encode(raw).decode(),
        "caseInputSha256": hashlib.sha256(raw).hexdigest(),
    }
    return {"contentId": "content:" + r.sha(r.DOMAIN_CONTENT, body), **body}


def pair(test_id, number):
    left, right = side(f"{test_id}-{number}-l"), side(f"{test_id}-{number}-r")
    if number % 2 == 0:
        for key in (
            "subjectCommitmentSha256",
            "partnerContextSha256",
            "chainId",
        ):
            right[key] = left[key]
        right_body = {
            key: right[key]
            for key in sorted(set(right) - {"contentId"})
        }
        right["contentId"] = "content:" + r.sha(r.DOMAIN_CONTENT, right_body)
    return {
        "pairId": "pair:"
        + r.sha(
            r.DOMAIN_PAIR,
            {
                "testId": test_id,
                "contentIds": sorted([left["contentId"], right["contentId"]]),
            },
        ),
        "testId": test_id,
        "left": left,
        "right": right,
    }


def file_sha(path):
    return hashlib.sha256(pathlib.Path(path).read_bytes()).hexdigest()


def runtime_libraries():
    mounts = {}
    targets = [pathlib.Path("/usr/local/bin/python3.14")]
    targets += list(
        pathlib.Path("/usr/local/lib/python3.14/lib-dynload").glob("*.so")
    )
    for target in targets:
        result = subprocess.run(
            ["/usr/bin/ldd", str(target)],
            capture_output=True,
            text=True,
            check=False,
        )
        for line in result.stdout.splitlines():
            for mount_path in re.findall(r"(/[A-Za-z0-9_./+.-]+)", line):
                source = pathlib.Path(mount_path)
                if source.is_file():
                    source = source.resolve()
                    mounts[mount_path] = {
                        "sourcePath": str(source),
                        "mountPath": mount_path,
                        "sha256": file_sha(source),
                    }
    return [mounts[key] for key in sorted(mounts)]


class EleventhGenerationAuditRegressions(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.seed = hashlib.sha256(b"eleventh-independent-beacon").digest()
        cls.rows = [
            pair(test_id, number)
            for test_id in sorted(r.TESTS)
            for number in range(401)
        ]
        cls.bwrap = pathlib.Path("/usr/bin/bwrap")
        cls.python = pathlib.Path("/usr/local/bin/python3.14")
        cls._runtime_temp = tempfile.TemporaryDirectory()
        cls.stdlib = pathlib.Path(cls._runtime_temp.name) / "python3.14"
        source_stdlib = pathlib.Path("/usr/local/lib/python3.14")
        probe = subprocess.run(
            [
                str(cls.python),
                "-I",
                "-B",
                "-c",
                (
                    "import base64,binascii,datetime,hashlib,json,math,os,re,"
                    "resource,secrets,selectors,signal,socket,stat,subprocess,sys,time;"
                    "base64.b64encode(b'x');json.dumps({'x':1});"
                    "socket.socket().close();"
                    "print(json.dumps(sorted({m.__file__ for m in sys.modules.values() "
                    "if getattr(m,'__file__',None)})))"
                ),
            ],
            capture_output=True,
            text=True,
            check=True,
            env={"PATH": "", "PYTHONDONTWRITEBYTECODE": "1"},
        )
        loaded_files = [pathlib.Path(item) for item in json.loads(probe.stdout)]
        runtime_files = set(source_stdlib.glob("*.py"))
        runtime_files.update((source_stdlib / "lib-dynload").glob("*.so"))
        for loaded in loaded_files:
            try:
                relative = loaded.resolve().relative_to(source_stdlib.resolve())
            except ValueError:
                continue
            top = source_stdlib / relative.parts[0]
            if top.is_dir() and top.name != "lib-dynload":
                runtime_files.update(top.rglob("*.py"))
            else:
                runtime_files.add(loaded)
        for source in sorted(runtime_files):
            relative = source.resolve().relative_to(source_stdlib.resolve())
            if source.is_symlink() or not source.is_file():
                continue
            destination = cls.stdlib / relative
            destination.parent.mkdir(parents=True, exist_ok=True)
            shutil.copyfile(source, destination)
        cls.runtime = {
            "isolation": {
                "provider": "bubblewrap-user-namespace-v1",
                "binaryPath": str(cls.bwrap),
                "binarySha256": file_sha(cls.bwrap),
                "binaryVersion": subprocess.run(
                    [str(cls.bwrap), "--version"],
                    capture_output=True,
                    text=True,
                    check=False,
                ).stdout.strip(),
                "networkNamespaceRequired": True,
                "readOnlyFilesystemRequired": True,
                "childProcessLimit": 1,
                "cpuSeconds": 30,
                "addressSpaceBytes": 536870912,
                "fileSizeBytes": 16777216,
                "openFileLimit": 32,
                "wallClockSeconds": 30,
            },
            "toolchain": {
                "pythonPath": str(cls.python),
                "pythonSha256": file_sha(cls.python),
                "pythonVersion": subprocess.run(
                    [str(cls.python), "--version"],
                    capture_output=True,
                    text=True,
                    check=False,
                ).stdout.strip(),
                "stdlibPath": str(cls.stdlib),
                "stdlibTreeSha256": r._tree_digest(cls.stdlib),
                "stdlibMode": "source-only-no-bytecode-v1",
                "rejectSymlinks": True,
                "dynamicLibraries": runtime_libraries(),
            },
        }

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

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

    def test_eleventh_03_undercoverage_rejected(self):
        with self.assertRaises(ValueError):
            r.derive_selection(self.rows[:-2], self.seed)

    def test_eleventh_04_balanced_400_by_3(self):
        selected = r.derive_selection(self.rows, self.seed)
        by_id = {row["pairId"]: row for row in self.rows}
        self.assertEqual(
            {
                test_id: sum(by_id[item]["testId"] == test_id for item in selected)
                for test_id in sorted(r.TESTS)
            },
            {test_id: 400 for test_id in sorted(r.TESTS)},
        )

    def test_eleventh_05_seed_changes_selection_or_order(self):
        self.assertNotEqual(
            r.derive_selection(self.rows, self.seed),
            r.derive_selection(self.rows, hashlib.sha256(b"other").digest()),
        )

    def test_eleventh_06_global_duplicate_rejected(self):
        with self.assertRaises(ValueError):
            r.derive_selection(self.rows + [copy.deepcopy(self.rows[-1])], self.seed)

    def test_eleventh_07_global_reversal_rejected(self):
        reversed_row = copy.deepcopy(self.rows[-1])
        reversed_row["left"], reversed_row["right"] = (
            reversed_row["right"],
            reversed_row["left"],
        )
        with self.assertRaises(ValueError):
            r.derive_selection(self.rows + [reversed_row], self.seed)

    def test_eleventh_08_candidate_self_declared_predicates_rejected(self):
        chosen = [self.rows[0]]
        responses = []
        for item in (chosen[0]["left"], chosen[0]["right"]):
            responses.append(
                {
                    "requestId": item["contentId"],
                    "portabilityOutputBase64": base64.b64encode(b"opaque").decode(),
                    "privacyPredicates": {"explicitConsent": True},
                }
            )
        with self.assertRaisesRegex(ValueError, "only requestId and opaque output"):
            r._independent_oracle(
                {"candidateId": "sha256:" + "a" * 64},
                chosen,
                b"".join(canon(item) + b"\n" for item in responses),
                chosen[0]["testId"],
            )

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

    def test_eleventh_10_noncanonical_implementation_path_rejected(self):
        source = (ROOT / "scripts/run_unlinkability_protocol.py").read_text()
        self.assertIn(
            'CANONICAL_IMPLEMENTATION_PATH = "v3-src/privacy-portability/canonical_api.py"',
            source,
        )
        schema = s.load_json(ROOT / "schemas/pilot-scope-registry.schema.json")
        props = schema["properties"]["routes"]["items"]["properties"]
        self.assertEqual(
            props["privacyPortabilityImplementationPath"]["const"],
            r.CANONICAL_IMPLEMENTATION_PATH,
        )

    def test_eleventh_11_symlink_candidate_rejected(self):
        with tempfile.TemporaryDirectory() as temp_text:
            temp = pathlib.Path(temp_text)
            actual = temp / "actual.py"
            actual.write_text("pass\n")
            alias = temp / "alias.py"
            alias.symlink_to(actual)
            policy = {
                **self.runtime,
                "candidateImplementationSha256": file_sha(actual),
            }
            with self.assertRaisesRegex(ValueError, "identity mismatch"):
                r._run_isolated(policy, alias, b"{}\n")

    def test_eleventh_12_hardlink_candidate_rejected(self):
        with tempfile.TemporaryDirectory() as temp_text:
            temp = pathlib.Path(temp_text)
            actual = temp / "actual.py"
            actual.write_text("pass\n")
            alias = temp / "alias.py"
            os.link(actual, alias)
            policy = {
                **self.runtime,
                "candidateImplementationSha256": file_sha(actual),
            }
            with self.assertRaisesRegex(ValueError, "identity mismatch"):
                r._run_isolated(policy, alias, b"{}\n")

    def test_eleventh_13_live_execution_authority_structurally_disabled(self):
        policy = s.load_json(ROOT / "config/privacy-execution-trust-policy.json")
        self.assertFalse(policy["positiveReadinessAllowed"])
        with self.assertRaisesRegex(ValueError, "structurally disabled"):
            r._policy(ROOT.parents[1])

    def test_eleventh_14_genuine_hermetic_positive_fixture(self):
        with tempfile.TemporaryDirectory() as temp_text:
            implementation = pathlib.Path(temp_text) / "canonical.py"
            implementation.write_text(
                "import base64,json,sys\n"
                "a=sys.argv;i=a[a.index('--request-jsonl')+1];"
                "o=a[a.index('--response-jsonl')+1]\n"
                "rows=[]\n"
                "for line in open(i):\n"
                " x=json.loads(line); rows.append({'requestId':x['requestId'],"
                "'portabilityOutputBase64':base64.b64encode(b'opaque').decode()})\n"
                "open(o,'w').write(''.join(json.dumps(x,sort_keys=True,"
                "separators=(',',':'))+'\\n' for x in rows))\n"
            )
            policy = {
                **self.runtime,
                "candidateImplementationSha256": file_sha(implementation),
            }
            request = canon(
                {
                    "requestId": "content:" + "a" * 64,
                    "caseInputSha256": "b" * 64,
                }
            ) + b"\n"
            response, measurement = r._run_isolated(
                policy, implementation, request
            )
            self.assertEqual(measurement["exitCode"], 0)
            self.assertEqual(measurement["stdoutSha256"], r.EMPTY_SHA256)
            self.assertIn(b'"requestId":"content:', response)

    def test_eleventh_15_network_filesystem_and_child_escape_denied(self):
        with tempfile.TemporaryDirectory() as temp_text:
            implementation = pathlib.Path(temp_text) / "hostile.py"
            implementation.write_text(
                "import base64,json,os,socket,sys\n"
                "denied=[]\n"
                "try: open('/etc/hostname').read()\n"
                "except Exception: denied.append('filesystem')\n"
                "try: socket.create_connection(('127.0.0.1',9),.05)\n"
                "except Exception: denied.append('network')\n"
                "try:\n"
                " p=os.fork()\n"
                " if p==0: os._exit(0)\n"
                " os.waitpid(p,0)\n"
                "except Exception: denied.append('child')\n"
                "a=sys.argv;i=a[a.index('--request-jsonl')+1];"
                "o=a[a.index('--response-jsonl')+1]\n"
                "rows=[]\n"
                "for line in open(i):\n"
                " x=json.loads(line); rows.append({'requestId':x['requestId'],"
                "'portabilityOutputBase64':base64.b64encode("
                "','.join(sorted(denied)).encode()).decode()})\n"
                "open(o,'w').write(''.join(json.dumps(x,sort_keys=True,"
                "separators=(',',':'))+'\\n' for x in rows))\n"
            )
            policy = {
                **self.runtime,
                "candidateImplementationSha256": file_sha(implementation),
            }
            request = canon({"requestId": "content:" + "c" * 64}) + b"\n"
            response, _ = r._run_isolated(policy, implementation, request)
            row = r.strict_object(response.decode().strip())
            denied = base64.b64decode(row["portabilityOutputBase64"]).decode()
            self.assertEqual(set(denied.split(",")), {"filesystem", "network", "child"})

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

    def test_eleventh_17_atomic_distinct_execution_artifacts(self):
        with tempfile.TemporaryDirectory() as temp_text:
            root = pathlib.Path(temp_text)
            generation_id = "generation-" + "a" * 64
            r._atomic_generation(
                root,
                generation_id,
                b'{"event":1}\n',
                b'{"receipt":1}\n',
                "channel/generations/" + generation_id + "/raw-events.jsonl",
                "channel/generations/" + generation_id + "/execution-record.json",
            )
            generation = root / "generations" / generation_id
            self.assertEqual(
                (generation / "raw-events.jsonl").read_bytes(), b'{"event":1}\n'
            )
            self.assertEqual(
                (generation / "execution-record.json").read_bytes(),
                b'{"receipt":1}\n',
            )
            self.assertEqual(
                json.loads((root / "CURRENT.json").read_text())["generationId"],
                generation_id,
            )
        schema = s.load_json(
            ROOT / "schemas/evidence/privacy-unlinkability-execution-output.schema.json"
        )
        self.assertIn("launcherSignature", schema["required"])
        self.assertIn("durationMonotonicNs", schema["required"])

    def test_eleventh_18_incident_timing_and_receipt_freshness_closed(self):
        source = (ROOT / "scripts/check_organizational_readiness.py").read_text()
        for predicate in (
            "output_started <= sent <= received == assertion_time",
            "output_started <= action_started < action_completed",
            "output_started <= sent <= acknowledged == assertion_time",
            "assertion_time <= receipt_signed_at <= output_ended",
            "receipt_signed_at - assertion_time <= timedelta(minutes=5)",
            "receipt[\"nonceSha256\"] not in incident_receipt_nonces",
        ):
            self.assertIn(predicate, source)


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