from __future__ import annotations

import copy
import hashlib
import json
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path

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


class FourthGenerationAuditRegressions(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.base = s.load_json(ROOT / "manifests/organizational-readiness.template.json")

    def reject(self, fn):
        with self.assertRaises(Exception):
            fn()

    def artifact_probe(self, evidence_id: str, target: dict | None = None):
        candidate = "sha256:" + "c" * 64
        scope = "d" * 64
        manifest = copy.deepcopy(self.base)
        manifest["subjectOrganizationId"] = "organization:" + "1" * 64
        manifest["partnerOrganizationId"] = "organization:" + "2" * 64
        manifest["releaseCandidate"].update({
            "sourceCommit": "a" * 40,
            "gitTree": "b" * 40,
            "evidenceCommit": "e" * 40,
            "evidenceTree": "f" * 40,
            "artifactManifest": {"path": c.ARTIFACT_MANIFEST_PATH, "sha256": "9" * 64},
        })
        facts = {}
        true_keys = {
            "entityVerified", "authorityVerified", "channelsControlTested",
            "evidenceReviewed", "candidateApproved", "noSurveillanceControlsApproved",
            "scopeAccepted", "candidateAccepted", "separationVerified",
            "subjectExecuted", "partnerExecuted", "subjectSignaturesVerified",
            "partnerSignaturesVerified", "intakeRouteControlTested",
            "coverageVerified", "staffingVerified", "responsibilitiesAccepted",
            "candidateIdMatched", "allStagesComplete", "allPassed",
            "singleStageMatched", "maximumLossBounded",
            "residualLiabilityAccepted", "forcedPushRejected",
            "pullSettlementVerified", "unknownDenied", "staleDenied",
            "malformedDenied", "unauthorizedDenied", "unlinkabilityVerified",
            "crossChainValueFlowAbsent", "allEvidenceTracked",
            "ignoredEvidenceAbsent", "uniqueBlobs", "pausedRefundPassed",
            "terminalExitPassed", "principalPreserved", "closedInputEnumVerified",
            "forbiddenConceptMutationsRejected",
        }
        for key in c.ARTIFACT_FACT_KEYS[evidence_id]:
            if key.endswith("Sha256"):
                facts[key] = hashlib.sha256(key.encode()).hexdigest()
            elif key in true_keys:
                facts[key] = True
            else:
                facts[key] = "typed-value"
        if "sourceCommit" in facts: facts["sourceCommit"] = "a" * 40
        if "gitTree" in facts: facts["gitTree"] = "b" * 40
        if "testSourceCommit" in facts: facts["testSourceCommit"] = "a" * 40
        if "testGitTree" in facts: facts["testGitTree"] = "b" * 40
        if "artifactManifestSha256" in facts: facts["artifactManifestSha256"] = "9" * 64
        refs = {
            key: {"path": f"virtual/{evidence_id}/{key}.json", "sha256": value}
            for key, value in facts.items() if key.endswith("Sha256")
        }
        artifact = {
            "schemaVersion": "1.0.0", "artifactId": evidence_id,
            "artifactType": c.EVIDENCE_SPECS[evidence_id]["claim"],
            "candidateId": candidate, "sourceCommit": "a" * 40,
            "subjectOrganizationId": manifest["subjectOrganizationId"],
            "partnerOrganizationId": manifest["partnerOrganizationId"],
            "scopeSha256": scope, "facts": facts, "blobRefs": refs,
        }
        resolved = {
            refs["sourceRecordSha256"]["path"]: {
                "schemaVersion": "1.0.0",
                "recordType": c.EVIDENCE_SPECS[evidence_id]["claim"],
                "candidateId": candidate,
                "subjectOrganizationId": manifest["subjectOrganizationId"],
                "partnerOrganizationId": manifest["partnerOrganizationId"],
                "evidenceId": evidence_id,
                "assertions": {k: v for k, v in facts.items() if not k.endswith("Sha256")},
            }
        }
        for key, ref in refs.items():
            resolved.setdefault(ref["path"], {"garbage": True})
        if target is not None:
            for key in refs:
                if key != "sourceRecordSha256":
                    resolved[refs[key]["path"]] = target
                    break
        old = c.safe_repo_file, c.require_tracked_exact, c.digest, c.load_json
        try:
            c.safe_repo_file = lambda p, *a, **k: Path(p)
            c.require_tracked_exact = lambda *a, **k: None
            c.digest = lambda p: next(v["sha256"] for v in refs.values() if v["path"] == str(p))
            c.load_json = lambda p: (
                resolved[str(p)] if str(p) in resolved else s.load_json(Path(p))
            )
            return c.validate_objective_artifact(
                artifact, evidence_id, manifest, candidate, scope
            )
        finally:
            c.safe_repo_file, c.require_tracked_exact, c.digest, c.load_json = old

    def test_fourth_01_trust_root_override_while_disabled(self):
        old = os.environ.get("VAULTFIRE_EXTERNAL_TRUST_ROOT")
        try:
            os.environ["VAULTFIRE_EXTERNAL_TRUST_ROOT"] = "/tmp/caller.json"
            self.reject(c.load_trust)
        finally:
            if old is None: os.environ.pop("VAULTFIRE_EXTERNAL_TRUST_ROOT", None)
            else: os.environ["VAULTFIRE_EXTERNAL_TRUST_ROOT"] = old

    def test_fourth_02_tracked_policy_disables_ready(self):
        policy = s.load_json(ROOT / "config/trust-root-policy.json")
        self.assertEqual(policy["status"], "no-external-authority-established")

    def test_fourth_03_duplicate_key(self): self.reject(lambda: s.loads_json('{"x":1,"x":2}'))
    def test_fourth_04_nan(self): self.reject(lambda: s.loads_json('{"x":NaN}'))
    def test_fourth_05_infinity(self): self.reject(lambda: s.loads_json('{"x":Infinity}'))
    def test_fourth_06_trailing_json(self): self.reject(lambda: s.loads_json('{"x":1}{"y":2}'))
    def test_fourth_07_bom_polyglot(self): self.reject(lambda: s.loads_json('\ufeff{"x":1}'))
    def test_fourth_08_bool_not_integer(self): self.reject(lambda: s.validate(True, {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"integer"}))
    def test_fourth_09_integer_not_boolean(self): self.reject(lambda: s.validate(1, {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"boolean"}))
    def test_fourth_10_ready_without_authority(self): self.reject(c.load_trust)

    def candidate(self):
        item = copy.deepcopy(self.base)
        item["subjectOrganizationId"]="organization:"+"1"*64
        item["partnerOrganizationId"]="organization:"+"2"*64
        item["scope"].update(selectedModule="M",selectedActionClass="read-only-query",selectedRouteId="base.r.read-only-query",deniedModules=["N"])
        item["releaseCandidate"].update(candidateId="sha256:"+"0"*64,envelopeId="sha256:"+"0"*64,sourceCommit="1"*40,gitTree="2"*40,evidenceCommit="3"*40,evidenceTree="4"*40,artifactManifest={"path":c.ARTIFACT_MANIFEST_PATH,"sha256":"5"*64},packageVersion="fixture",createdAtUtc="2026-08-25T00:00:00Z",deploymentManifest={"path":c.DEPLOYMENT_PATH,"sha256":"6"*64},canonicalBindings={k:{"path":v,"sha256":hashlib.sha256(k.encode()).hexdigest()} for k,v in c.CANONICAL_PATHS.items()})
        return item

    def test_fourth_11_candidate_stable_across_phase_two(self):
        item=self.candidate(); first=c.expected_candidate_id(item)
        item["releaseCandidate"]["evidenceCommit"]="7"*40
        self.assertEqual(first,c.expected_candidate_id(item))

    def test_fourth_12_envelope_binds_phase_two(self):
        item=self.candidate(); first=c.expected_envelope_id(item)
        item["releaseCandidate"]["evidenceTree"]="7"*40
        self.assertNotEqual(first,c.expected_envelope_id(item))

    def test_fourth_13_phase_two_fixed_point_removed(self):
        source=(ROOT/"scripts/check_organizational_readiness.py").read_text()
        self.assertNotIn('facts["evidenceCommit"] == manifest["releaseCandidate"]["evidenceCommit"]', source)
        self.assertNotIn("artifactManifestSha256", c.ARTIFACT_FACT_KEYS["CANDIDATE-BINDING"])
        self.assertNotIn("artifactManifestSha256", c.ARTIFACT_FACT_KEYS["IMMUTABLE-EVIDENCE"])

    def test_fourth_14_arbitrary_bounded_liabilities(self): self.reject(lambda: self.artifact_probe("BOUNDED-LIABILITIES"))
    def test_fourth_15_arbitrary_privacy_cross_chain(self): self.reject(lambda: self.artifact_probe("PRIVACY-CROSS-CHAIN"))
    def test_fourth_16_arbitrary_pull_payments(self): self.reject(lambda: self.artifact_probe("PULL-PAYMENTS"))
    def test_fourth_17_arbitrary_fail_closed_apis(self): self.reject(lambda: self.artifact_probe("FAIL-CLOSED-APIS"))
    def test_fourth_18_arbitrary_pause_safe_exits(self): self.reject(lambda: self.artifact_probe("PAUSE-SAFE-EXITS"))
    def test_fourth_19_arbitrary_protected_inputs(self): self.reject(lambda: self.artifact_probe("PROTECTED-DECISION-INPUTS"))

    def test_fourth_20_weak_test_results(self):
        weak={"schemaVersion":"anything","candidateId":"sha256:"+"c"*64,"sourceCommit":"a"*40,"gitTree":"b"*40,"allPassed":True,"tests":[False]}
        self.reject(lambda: self.artifact_probe("EXACT-COMMIT-TESTS",weak))

    def test_fourth_21_weak_signature_set(self):
        weak={"schemaVersion":"anything","candidateId":"sha256:"+"c"*64,"subjectOrganizationId":"organization:"+"9"*64,"partnerOrganizationId":"organization:"+"8"*64,"verifiedIdentityIds":[],"allVerified":True}
        self.reject(lambda: self.artifact_probe("SIGNATURES",weak))

    def test_fourth_22_artifact_bool_int_confusion(self): self.reject(lambda: s.validate(1,{"$schema":"https://json-schema.org/draft/2020-12/schema","const":True}))
    def test_fourth_23_absolute_path(self): self.reject(lambda: c.safe_repo_file("/tmp/x.json","x"))
    def test_fourth_24_dotdot_path(self): self.reject(lambda: c.safe_repo_file("pr10/partner-package/../x.json","x"))
    def test_fourth_25_backslash_path(self): self.reject(lambda: c.safe_repo_file("pr10\\x.json","x"))

    def test_fourth_26_symlink_path(self):
        with tempfile.TemporaryDirectory(dir=c.REPOSITORY) as td:
            target=Path(td)/"target.json"; target.write_text("{}")
            link=Path(td)/"link.json"; link.symlink_to(target)
            self.reject(lambda: c.safe_repo_file(link.relative_to(c.REPOSITORY).as_posix(),"x"))

    def protected(self,value):
        item=copy.deepcopy(self.base);item["decisionInputPolicy"]["observedInputClasses"]=[value]
        self.reject(lambda:c.validate_shape(item))

    def test_fourth_27_hashed_biometric(self): self.protected("allowed:sha256-biometric")
    def test_fourth_28_derived_reputation(self): self.protected("allowed:derived-reputation")
    def test_fourth_29_transitive_surveillance(self): self.protected("allowed:transitive-surveillance")
    def test_fourth_30_flourishing_input(self): self.protected("allowed:flourishing")
    def test_fourth_31_opaque_profile(self): self.protected("allowed:opaque-profile")

    def test_fourth_32_tracked_worktree_substitution(self):
        with tempfile.NamedTemporaryFile("wb",delete=False) as handle:
            handle.write(b"worktree"); path=Path(handle.name)
        old_git,old_run=c.git_bytes,c.subprocess.run
        try:
            c.git_bytes=lambda *a:b"tracked"
            class R:returncode=1
            c.subprocess.run=lambda *a,**k:R()
            self.reject(lambda:c.require_tracked_exact("x",path,"a"*40,"x"))
        finally:
            c.git_bytes, c.subprocess.run=old_git,old_run;path.unlink()

    def test_fourth_33_role_assignment_equality(self):
        source=(ROOT/"scripts/check_organizational_readiness.py").read_text()
        self.assertIn('identity_id == assignments[role]["identityId"]',source)

    def test_staged_git_positive_fixture_is_finite_and_acyclic(self):
        with tempfile.TemporaryDirectory() as td:
            repo=Path(td)
            subprocess.run(["git","init","-q",str(repo)],check=True)
            subprocess.run(["git","-C",str(repo),"config","user.name","fixture"],check=True)
            subprocess.run(["git","-C",str(repo),"config","user.email","fixture@example.invalid"],check=True)
            (repo/"README").write_text("source\n")
            subprocess.run(["git","-C",str(repo),"add","README"],check=True)
            subprocess.run(["git","-C",str(repo),"commit","-q","-m","source"],check=True)
            source=subprocess.check_output(["git","-C",str(repo),"rev-parse","HEAD"],text=True).strip()
            selection_path=repo/"pr10/partner-package/evidence/organizational/privacy-inputs/selection.json"
            selection_path.parent.mkdir(parents=True);selection_path.write_text('{"phase":"selection"}\n')
            subprocess.run(["git","-C",str(repo),"add",selection_path.relative_to(repo)],check=True)
            subprocess.run(["git","-C",str(repo),"commit","-q","-m","selection"],check=True)
            selection=subprocess.check_output(["git","-C",str(repo),"rev-parse","HEAD"],text=True).strip()
            artifact=repo/"pr10/partner-package/evidence/organizational/artifact-manifest.json"
            artifact.write_text('{"phase":2}\n')
            subprocess.run(["git","-C",str(repo),"add",artifact.relative_to(repo)],check=True)
            subprocess.run(["git","-C",str(repo),"commit","-q","-m","evidence"],check=True)
            evidence=subprocess.check_output(["git","-C",str(repo),"rev-parse","HEAD"],text=True).strip()
            envelope=repo/c.POST_COMMIT_ENVELOPE_PATH
            envelope.write_text(json.dumps({"evidenceCommit":evidence})+"\n")
            subprocess.run(["git","-C",str(repo),"add",envelope.relative_to(repo)],check=True)
            subprocess.run(["git","-C",str(repo),"commit","-q","-m","attestation"],check=True)
            head,selection_tree,evidence_tree,attestation_tree=c.validate_staged_git_flow(repo,source,selection,evidence,envelope)
            self.assertNotEqual(source,selection)
            self.assertNotEqual(source,evidence)
            self.assertNotEqual(evidence,head)
            self.assertNotEqual(evidence_tree,attestation_tree)


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