from __future__ import annotations

import copy
import json
import sys
import unittest
from pathlib import Path

HERE = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(HERE))

import check_ledger  # noqa: E402


class LedgerCheckerTests(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.ledger = json.loads((HERE / "ledger.json").read_text())

    def errors_for(self, mutate):
        ledger = copy.deepcopy(self.ledger)
        mutate(ledger)
        return check_ledger.check(ledger, compare_inventory=False)

    def test_current_ledger_is_reconciled_and_fail_closed(self):
        errors = check_ledger.check(self.ledger, compare_inventory=False)
        self.assertEqual([], errors)
        self.assertTrue(
            all(
                item["deploymentStatus"] in {
                    "not-deployed-not-authorized", "not-applicable"
                }
                for item in self.ledger["items"]
            )
        )

    def test_all_36_contract_identities_have_truthful_nondefault_dispositions(self):
        contracts = [
            item for item in self.ledger["items"] if item["category"] == "v2-contract"
        ]
        self.assertEqual(36, len(contracts))
        self.assertEqual(
            {"ported", "redesigned", "retired", "deferred"},
            {item["v3SupportStatus"] for item in contracts},
        )
        self.assertTrue(all(item["v3Disposition"] != "blocked" for item in contracts))
        self.assertTrue(
            all(item["deploymentStatus"] == "not-deployed-not-authorized" for item in contracts)
        )

    def test_duplicate_id_fails(self):
        errors = self.errors_for(lambda ledger: ledger["items"].append(ledger["items"][0]))
        self.assertTrue(any("duplicate stable IDs" in error for error in errors))

    def test_unsupported_disposition_fails(self):
        errors = self.errors_for(
            lambda ledger: ledger["items"][0].update(v3Disposition="maybe")
        )
        self.assertTrue(any("unsupported disposition" in error for error in errors))

    def test_complete_without_evidence_fails(self):
        def mutate(ledger):
            ledger["items"][0]["v3Disposition"] = "ported"
            ledger["items"][0]["v3Target"] = "v3-src/example"
            ledger["items"][0]["evidence"] = []
        errors = self.errors_for(mutate)
        self.assertTrue(any("completion disposition has no evidence" in error for error in errors))

    def test_forbidden_signal_controlling_settlement_fails(self):
        def mutate(ledger):
            item = ledger["items"][0]
            item["influencePolicy"] = {
                "signals": ["belief"],
                "controls": ["settlement"],
                "controlAllowed": True,
                "decision": "allowed",
            }
        errors = self.errors_for(mutate)
        self.assertTrue(any("forbidden signals" in error for error in errors))

    def test_activated_prohibited_coupling_fails_even_when_flag_false(self):
        def mutate(ledger):
            item = ledger["items"][0]
            item["v3Disposition"] = "compatibility-wrapper"
            item["influencePolicy"] = {
                "signals": ["reputation"],
                "controls": ["access"],
                "controlAllowed": False,
                "decision": "blocked-pending-decoupling",
            }
        errors = self.errors_for(mutate)
        self.assertTrue(any("cannot be activated" in error for error in errors))

    def test_missing_required_field_fails(self):
        errors = self.errors_for(lambda ledger: ledger["items"][0].pop("activationGate"))
        self.assertTrue(any("missing fields" in error for error in errors))

    def test_contract_disposition_drift_fails(self):
        def mutate(ledger):
            item = next(row for row in ledger["items"] if row["category"] == "v2-contract")
            item["evidenceStatus"] = "invented"
        errors = self.errors_for(mutate)
        self.assertTrue(any("drifts from canonical manifest" in error for error in errors))


class SafetyManifestTests(unittest.TestCase):
    def test_current_final_authorities_pass(self):
        self.assertEqual(check_ledger.check_current_authorities(), [])

    def test_stale_sdk_count_fails(self):
        facts = copy.deepcopy(check_ledger._load("current-authority-facts.json"))
        facts["sdk"]["abis"]["governance"]["functionCount"] = 41
        errors = check_ledger.check_current_authorities(facts)
        self.assertTrue(
            any("SDK governance function count drifted" in error for error in errors),
            errors,
        )

    def test_external_evidence_cannot_be_promoted(self):
        facts = copy.deepcopy(check_ledger._load("current-authority-facts.json"))
        facts["externalEvidence"]["x402"] = "complete"
        errors = check_ledger.check_current_authorities(facts)
        self.assertTrue(
            any("external truth boundary drifted" in error for error in errors),
            errors,
        )

    @classmethod
    def setUpClass(cls):
        cls.dispositions = json.loads((HERE / "v2-contract-dispositions.json").read_text())
        cls.supported = json.loads((HERE / "supported-v3-modules.json").read_text())
        cls.graph = json.loads((HERE / "mission-boundary-graph.json").read_text())
        cls.policy = json.loads(
            (HERE.parents[1] / "ci/manifests/base-v3-governance-policy.json").read_text()
        )

    def errors_for(self, *, dispositions=None, supported=None, graph=None):
        return check_ledger.check_safety_manifests(
            dispositions=dispositions or copy.deepcopy(self.dispositions),
            supported=supported or copy.deepcopy(self.supported),
            graph=graph or copy.deepcopy(self.graph),
        )

    def test_every_prohibited_class_is_required_in_canonical_policy(self):
        for prohibited_class in self.supported["unsafeCapabilityClasses"]:
            with self.subTest(prohibited_class=prohibited_class):
                policy = copy.deepcopy(self.policy)
                policy["prohibitedInputClasses"].remove(prohibited_class)
                errors = check_ledger.check_governance_policy(
                    policy, self.supported, self.graph
                )
                self.assertTrue(
                    any("prohibited classes drifted" in error for error in errors)
                )

    def test_hashed_and_transitive_representations_are_required(self):
        for representation in ("hashed", "derived", "transitive"):
            with self.subTest(representation=representation):
                policy = copy.deepcopy(self.policy)
                policy["prohibitedRepresentations"].remove(representation)
                errors = check_ledger.check_governance_policy(
                    policy, self.supported, self.graph
                )
                self.assertTrue(
                    any("prohibited representations" in error for error in errors)
                )

    def test_every_protected_decision_is_required_in_canonical_policy(self):
        for protected_decision in self.supported["protectedDecisions"]:
            with self.subTest(protected_decision=protected_decision):
                policy = copy.deepcopy(self.policy)
                policy["protectedDecisions"].remove(protected_decision)
                errors = check_ledger.check_governance_policy(
                    policy, self.supported, self.graph
                )
                self.assertTrue(
                    any("protected decisions are incomplete" in error for error in errors)
                )

    def test_current_manifests_are_reconciled_without_operations_dependency(self):
        errors = self.errors_for()
        self.assertEqual([], errors)

    def test_sdk_source_path_mutation_fails(self):
        source = (HERE.parents[1] / "repos/vaultfire-sdk/src/v3/supported.ts").read_text()
        mutated = source.replace("sourcePath: '", "sourcePath: 'v3-src/contracts/retired/", 1)
        paths = set(check_ledger.re.findall(r"sourcePath:\s*'([^']+)'", mutated))
        self.assertTrue(paths - {row["path"] for row in self.supported["modules"]})

    def test_retired_identifier_word_boundary_mutation_fails(self):
        source = (HERE.parents[1] / "repos/vaultfire-sdk/src/v3/supported.ts").read_text()
        mutated = source + "\nexport const forbidden = TaskEscrowV3;\n"
        self.assertIsNotNone(check_ledger.re.search(r"\bTaskEscrowV3\b", mutated))

    def test_immutable_v2_hash_drift_fails(self):
        manifest = copy.deepcopy(self.dispositions)
        manifest["records"][0]["immutableV2Evidence"]["sha256"] = "0" * 64
        errors = self.errors_for(dispositions=manifest)
        self.assertTrue(any("immutable V2 ABI checksum changed" in error for error in errors))

    def test_source_support_never_implies_deployment(self):
        manifest = copy.deepcopy(self.dispositions)
        manifest["records"][0]["deploymentStatus"] = "deployed"
        errors = self.errors_for(dispositions=manifest)
        self.assertTrue(any("must not claim deployment" in error for error in errors))

    def test_reputation_module_cannot_control_access(self):
        supported = copy.deepcopy(self.supported)
        reputation = next(
            module for module in supported["modules"]
            if "reputation" in module["capabilityTags"]
        )
        reputation["allowedProtectedDecisions"] = ["access"]
        errors = self.errors_for(supported=supported)
        self.assertTrue(any("unsafe capability is allowed" in error for error in errors))

    def test_retired_discretionary_modules_cannot_reenter_supported_closure(self):
        retired = {
            "TaskEscrowV3": "v3-src/contracts/escrow/TaskEscrowV3.sol",
            "DisputeRegistryV3": "v3-src/contracts/disputes/DisputeRegistryV3.sol",
            "MutualAidPoolV3": "v3-src/contracts/mutualaid/MutualAidPoolV3.sol",
            "ERC8004ValidationRegistryV3Safe": (
                "v3-src/contracts/reputation/ERC8004ValidationRegistryV3Safe.sol"
            ),
        }
        for module, path in retired.items():
            with self.subTest(module=module):
                supported = copy.deepcopy(self.supported)
                supported["modules"].append({
                    "module": module,
                    "path": path,
                    "supportStatus": "supported-source",
                    "deploymentStatus": "not-deployed-not-authorized",
                    "capabilityTags": ["objective-protocol"],
                    "allowedProtectedDecisions": ["custody"],
                    "boundaryRule": "incorrectly reintroduced retired protected-decision surface",
                })
                errors = self.errors_for(supported=supported)
                self.assertTrue(any(
                    "retired strict-boundary modules remain supported" in error
                    for error in errors
                ))

    def test_enforced_retirements_cannot_downgrade_to_source_evidence(self):
        manifest = copy.deepcopy(self.dispositions)
        record = next(
            row for row in manifest["records"]
            if row["v2ContractIdentity"] == "VaultfireTaskEscrow"
        )
        record["evidenceStatus"] = "source-evidenced"
        errors = self.errors_for(dispositions=manifest)
        self.assertTrue(any("evidence status must be 'retirement-enforced'" in error for error in errors))

    def test_least_privilege_decision_grants_are_exact(self):
        supported = copy.deepcopy(self.supported)
        bond = next(module for module in supported["modules"] if module["module"] == "AccountabilityBondV3Safe")
        bond["allowedProtectedDecisions"].append("routing")
        errors = self.errors_for(supported=supported)
        self.assertTrue(any("protected decisions drifted" in error for error in errors))

    def test_transitive_unsafe_path_across_surfaces_fails(self):
        graph = copy.deepcopy(self.graph)
        edge = next(
            edge for edge in graph["edges"]
            if edge["from"] == "unsafe:belief" and edge["to"] == "surface:api-metadata"
        )
        edge["carriesDecisionInfluence"] = True
        errors = self.errors_for(graph=graph)
        self.assertTrue(any("transitive unsafe influence" in error for error in errors))

    def test_every_required_surface_is_enforced(self):
        for kind in check_ledger.REQUIRED_SURFACE_KINDS:
            with self.subTest(kind=kind):
                graph = copy.deepcopy(self.graph)
                graph["nodes"] = [node for node in graph["nodes"] if node["kind"] != kind]
                graph["edges"] = [
                    edge for edge in graph["edges"]
                    if edge["from"] in {node["id"] for node in graph["nodes"]}
                    and edge["to"] in {node["id"] for node in graph["nodes"]}
                ]
                errors = self.errors_for(graph=graph)
                self.assertTrue(any("missing surfaces" in error for error in errors))

    def test_all_eight_protected_decisions_are_enforced(self):
        graph = copy.deepcopy(self.graph)
        graph["nodes"] = [
            node for node in graph["nodes"] if node["id"] != "decision:routing"
        ]
        graph["edges"] = [
            edge for edge in graph["edges"] if edge["to"] != "decision:routing"
        ]
        errors = self.errors_for(graph=graph)
        self.assertTrue(any("omits protected-decision sinks" in error for error in errors))


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