from __future__ import annotations

import copy
import json
import os
import shutil
import subprocess
import sys
import tempfile
import unittest
from datetime import datetime, timedelta, timezone
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
CHECK = ROOT / "scripts/check_organizational_readiness.py"
TEMPLATE = ROOT / "manifests/organizational-readiness.template.json"


def strict_load(path: Path) -> dict:
    sys.path.insert(0, str(ROOT / "scripts"))
    from strict_json_schema import load_json
    return load_json(path)


def run_obj(obj: dict | None = None, raw: str | None = None) -> subprocess.CompletedProcess[str]:
    with tempfile.NamedTemporaryFile("w", suffix=".json", encoding="utf-8", delete=False) as handle:
        handle.write(raw if raw is not None else json.dumps(obj))
        name = handle.name
    try:
        return subprocess.run([sys.executable, str(CHECK), name], cwd=ROOT, text=True, capture_output=True, check=False)
    finally:
        os.unlink(name)


def fabricated_ready(base: dict) -> dict:
    item = copy.deepcopy(base)
    now = datetime.now(timezone.utc).replace(microsecond=0)
    item["asOfUtc"] = now.strftime("%Y-%m-%dT%H:%M:%SZ")
    item["partnerReady"] = True
    item["status"] = "candidate-bound-ready"
    item["scope"].update({
        "selectedModule": "everything-v3",
        "selectedActionClass": "write-activation",
        "selectedRouteId": "everything-v3",
        "deniedModules": [],
    })
    item["releaseCandidate"] = {
        "candidateId": "sha256:" + "0" * 64,
        "envelopeId": "sha256:" + "8" * 64,
        "sourceCommit": "1" * 40,
        "gitTree": "2" * 40,
        "evidenceCommit": "9" * 40,
        "evidenceTree": "a" * 40,
        "packageVersion": "fabricated-candidate",
        "createdAtUtc": (now - timedelta(minutes=1)).strftime("%Y-%m-%dT%H:%M:%SZ"),
        "deploymentManifest": {"path": "pr8/pilot/base_v3_deployment_manifest.json", "sha256": "3" * 64},
        "canonicalBindings": {
            key: {"path": path, "sha256": "4" * 64}
            for key, path in {
                "operationsStatus": "v3-operations/release-status.json",
                "policyCommitment": "v3-operations/policy-commitment.json",
                "readinessSupersession": "v3-operations/readiness-supersession.json",
                "openSpec": "v3-migration/completeness/supported-v3-modules.json",
                "generatedRouteInventory": "pr7/generated/route-registry.json",
                "canonicalLedger": "v3-migration/completeness/ledger.json",
                "partnerPackageSchema": "pr10/partner-package/schemas/organizational-readiness.schema.json",
                "trustedIdentities": "pr10/partner-package/config/trusted-identities.json",
                "trustRootPolicy": "pr10/partner-package/config/trust-root-policy.json",
                "scopeRegistry": "pr10/partner-package/config/pilot-scope-registry.json",
                "rolloutPlan": "pr10/partner-package/config/pilot-rollout-plan.json",
            }.items()
        },
    }
    for role in item["roles"]:
        role["identityId"] = "identity:" + "5" * 64
        role["contactChannelId"] = "channel:" + "6" * 64
    for blocker in item["blockers"]:
        blocker["status"] = "satisfied"
    item["attestation"] = {
        "preparedByIdentityId": "identity:" + "5" * 64,
        "reviewedByIdentityId": "identity:" + "5" * 64,
        "authorizedByIdentityId": "identity:" + "5" * 64,
        "partnerAcceptedByIdentityId": "identity:" + "5" * 64,
        "signedAtUtc": "9999-12-31T23:59:59Z",
        "signatureEvidenceId": "SIGNATURES",
    }
    item["evidence"] = [
        {
            "id": blocker["id"],
            "claimType": "generic-readme",
            "path": f"pr10/partner-package/evidence/organizational/{blocker['id'].lower()}.json",
            "sha256": "7" * 64,
            "candidateId": item["releaseCandidate"]["candidateId"],
        }
        for blocker in item["blockers"]
    ]
    return item


def use_real_single_module_scope(item: dict) -> None:
    spec = strict_load(ROOT.parents[1] / "v3-migration/completeness/supported-v3-modules.json")
    modules = sorted(row["module"] for row in spec["modules"])
    item["scope"].update({
        "selectedModule": modules[0],
        "selectedActionClass": "module-route",
        "selectedRouteId": "candidate-specific-single-route",
        "deniedModules": modules[1:],
    })
    payload = {
        "canonicalBindings": item["releaseCandidate"]["canonicalBindings"],
        "createdAtUtc": item["releaseCandidate"]["createdAtUtc"],
        "deploymentManifest": item["releaseCandidate"]["deploymentManifest"],
        "gitTree": item["releaseCandidate"]["gitTree"],
        "packageVersion": item["releaseCandidate"]["packageVersion"],
        "scope": item["scope"],
        "sourceCommit": item["releaseCandidate"]["sourceCommit"],
    }
    import hashlib
    canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode()
    item["releaseCandidate"]["candidateId"] = "sha256:" + hashlib.sha256(canonical).hexdigest()
    for row in item["evidence"]:
        row["candidateId"] = item["releaseCandidate"]["candidateId"]


class OrganizationalReadinessTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls) -> None:
        cls.base = strict_load(TEMPLATE)

    def assert_rejected(self, item: dict | None = None, raw: str | None = None, contains: str | None = None):
        result = run_obj(item, raw)
        self.assertNotEqual(result.returncode, 0, result.stdout)
        if contains:
            self.assertIn(contains, result.stderr)

    def test_truthful_blocked_baseline_and_determinism(self):
        first = subprocess.run([sys.executable, str(CHECK)], cwd=ROOT, text=True, capture_output=True)
        second = subprocess.run([sys.executable, str(CHECK)], cwd=ROOT, text=True, capture_output=True)
        self.assertEqual(first.returncode, 0, first.stderr)
        self.assertEqual(first.stdout, second.stdout)
        report = json.loads(first.stdout)
        self.assertFalse(report["partnerReady"])
        self.assertEqual(report["mandatoryEvidenceCount"], 25)
        self.assertEqual(report["externalHumanBlockerCount"], 14)
        self.assertEqual(len(report["openBlockers"]), 25)

    def test_dependency_free_draft_2020_12_validation_is_real(self):
        sys.path.insert(0, str(ROOT / "scripts"))
        from strict_json_schema import StrictJSONError, load_json, validate
        for schema_name, instance_name in [
            ("organizational-readiness.schema.json", "../manifests/organizational-readiness.template.json"),
            ("deployment-manifest.schema.json", "../manifests/base-v3-deployment-manifest.template.json"),
            ("proposal-template.schema.json", "../proposals/01-signer-guardian.json"),
            ("trusted-identities.schema.json", "../config/trusted-identities.json"),
            ("trust-root-policy.schema.json", "../config/trust-root-policy.json"),
            ("pilot-scope-registry.schema.json", "../config/pilot-scope-registry.json"),
            ("pilot-rollout-plan.schema.json", "../config/pilot-rollout-plan.json"),
        ]:
            schema = load_json(ROOT / "schemas" / schema_name)
            validate(load_json((ROOT / "schemas" / instance_name).resolve()), schema)
        with self.assertRaises(StrictJSONError):
            validate({}, {"$schema": "https://json-schema.org/draft/2020-12/schema", "unknownAssertion": True})

    # Independent audit attacks 01-15.
    def test_attack_01_fabricated_readiness_and_reused_readme(self):
        self.assert_rejected(fabricated_ready(self.base))

    def test_attack_02_duplicate_json_key(self):
        raw = TEMPLATE.read_text()
        raw = raw.replace('"partnerReady": false', '"partnerReady": false, "partnerReady": true', 1)
        self.assert_rejected(raw=raw, contains="duplicate JSON object key")

    def test_attack_03_noncanonical_dotdot_path(self):
        item = fabricated_ready(self.base)
        item["evidence"][0]["path"] = "pr10/partner-package/../partner-package/README.md"
        self.assert_rejected(item)

    def test_attack_04_non_utc_future_and_misordered_timestamps(self):
        item = fabricated_ready(self.base)
        item["asOfUtc"] = "2026-08-25T20:00:00+14:00"
        self.assert_rejected(item, contains="canonical RFC3339 UTC")

    def test_attack_05_absolute_path_escape(self):
        item = fabricated_ready(self.base); item["evidence"][0]["path"] = "/etc/hosts"
        self.assert_rejected(item)

    def test_attack_06_relative_path_escape(self):
        item = fabricated_ready(self.base); item["evidence"][0]["path"] = "../../../../etc/hosts"
        self.assert_rejected(item)

    def test_attack_07_integer_boolean_confusion(self):
        item = copy.deepcopy(self.base); item["partnerReady"] = 1
        self.assert_rejected(item)

    def test_attack_08_extra_organizational_field(self):
        item = copy.deepcopy(self.base); item["extra"] = "x"
        self.assert_rejected(item, contains="unexpected keys")

    def test_attack_09_duplicate_evidence_id(self):
        item = fabricated_ready(self.base); item["evidence"].append(copy.deepcopy(item["evidence"][0]))
        self.assert_rejected(item)

    def test_attack_10_duplicate_blocker_id(self):
        item = copy.deepcopy(self.base); item["blockers"].append(copy.deepcopy(item["blockers"][0]))
        self.assert_rejected(item)

    def test_attack_11_stale_or_tampered_evidence_digest(self):
        item = fabricated_ready(self.base); item["evidence"][0]["sha256"] = "0" * 64
        self.assert_rejected(item)

    def test_attack_12_partial_blocker_acceptance(self):
        item = fabricated_ready(self.base); item["blockers"][0]["status"] = "open"
        self.assert_rejected(item)

    def test_attack_13_candidate_id_mismatch(self):
        item = fabricated_ready(self.base); item["releaseCandidate"]["candidateId"] = "sha256:" + "f" * 64
        self.assert_rejected(item)

    def test_attack_14_deployment_and_proposal_extra_fields(self):
        with tempfile.TemporaryDirectory() as td:
            copied = Path(td) / "repo" / "pr10" / "partner-package"
            copied.parent.mkdir(parents=True)
            shutil.copytree(ROOT, copied)
            manifest = copied / "manifests/base-v3-deployment-manifest.template.json"
            value = json.loads(manifest.read_text()); value["unexpected"] = True
            manifest.write_text(json.dumps(value))
            result = subprocess.run([sys.executable, str(copied / "scripts/validate_partner_package.py")], cwd=copied, text=True, capture_output=True)
            self.assertNotEqual(result.returncode, 0)

    def test_attack_15_symlink_evidence_is_rejected(self):
        sys.path.insert(0, str(ROOT / "scripts"))
        import check_organizational_readiness as checker
        with tempfile.TemporaryDirectory() as td:
            outside = Path(td) / "outside.json"; outside.write_text("{}")
            link = ROOT / "evidence" / "organizational" / "attestations" / "audit-test-link.json"
            link.parent.mkdir(parents=True, exist_ok=True)
            try:
                link.symlink_to(outside)
                with self.assertRaises(checker.Blocked):
                    checker.safe_repo_file(
                        "pr10/partner-package/evidence/organizational/attestations/audit-test-link.json",
                        "audit symlink",
                        checker.EVIDENCE_ROOT,
                        ".json",
                    )
            finally:
                if link.is_symlink():
                    link.unlink()

    # New bypass regressions.
    def test_monolithic_everything_v3_scope_rejected(self):
        self.assert_rejected(fabricated_ready(self.base))

    def test_commitment_drift_rejected(self):
        item = copy.deepcopy(self.base)
        item["commitments"]["surveillanceDataNeverPermittedForProtectedDecisions"] = False
        self.assert_rejected(item)

    def test_hashed_transitive_prohibited_input_rejected(self):
        item = copy.deepcopy(self.base)
        item["decisionInputPolicy"]["observedInputClasses"] = ["allowed:hashed-biometric"]
        self.assert_rejected(item)

    def test_invented_identity_in_blocked_template_rejected(self):
        item = copy.deepcopy(self.base)
        item["roles"][0]["identityId"] = "identity:" + "a" * 64
        item["roles"][0]["contactChannelId"] = "channel:" + "b" * 64
        self.assert_rejected(item, contains="cannot invent people or contacts")

    def test_rfc8032_ed25519_known_vector_and_tamper(self):
        sys.path.insert(0, str(ROOT / "scripts"))
        import check_organizational_readiness as checker
        public = bytes.fromhex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")
        signature = bytes.fromhex(
            "e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155"
            "5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
        )
        self.assertTrue(checker.verify_ed25519(public, signature, b""))
        self.assertFalse(checker.verify_ed25519(public, signature, b"tampered"))
        identity_encoding = bytes.fromhex("01" + "00" * 31)
        self.assertFalse(checker.verify_ed25519(identity_encoding, bytes(64), b"anything"))

    def test_future_asof_rejected_even_for_blocked_manifest(self):
        item = copy.deepcopy(self.base)
        item["asOfUtc"] = (
            datetime.now(timezone.utc).replace(microsecond=0) + timedelta(days=1)
        ).strftime("%Y-%m-%dT%H:%M:%SZ")
        self.assert_rejected(item, contains="asOfUtc is in the future")

    def test_stale_ready_manifest_rejected_before_other_ready_checks(self):
        item = fabricated_ready(self.base)
        item["asOfUtc"] = (
            datetime.now(timezone.utc).replace(microsecond=0) - timedelta(hours=25)
        ).strftime("%Y-%m-%dT%H:%M:%SZ")
        self.assert_rejected(item)

    def test_nonexistent_commit_rejected_after_valid_single_module_scope(self):
        item = fabricated_ready(self.base)
        use_real_single_module_scope(item)
        self.assert_rejected(item)

    def test_trust_schema_rejects_undeclared_role_and_extra_key(self):
        sys.path.insert(0, str(ROOT / "scripts"))
        from strict_json_schema import StrictJSONError, load_json, validate
        schema = load_json(ROOT / "schemas/trusted-identities.schema.json")
        trust = {
            "$schema": "../schemas/trusted-identities.schema.json",
            "schemaVersion": "1.0.0",
            "status": "human-reviewed",
            "identities": [{
                "identityId": "identity:" + "a" * 64,
                "organizationId": "organization:" + "b" * 64,
                "displayLabel": "controlled-label",
                "keyType": "Ed25519",
                "publicKeyBase64": "A" * 43 + "=",
                "allowedRoles": ["undeclared-approver"],
                "authorityEvidenceId": "ROLE-ASSIGNMENTS",
                "unexpected": True,
            }],
        }
        with self.assertRaises(StrictJSONError):
            validate(trust, schema)


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