"""Regression tests for the Base-mainnet bonds-only manifest/tooling."""
from __future__ import annotations

import copy
import importlib.util
import json
import hashlib
import sys
import tempfile
import unittest
from pathlib import Path

PILOT_DIR = Path(__file__).resolve().parent


def load_module(name: str):
    spec = importlib.util.spec_from_file_location(name, PILOT_DIR / f"{name}.py")
    assert spec and spec.loader
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    spec.loader.exec_module(module)
    return module


manifest_module = load_module("deployment_manifest")
generator = load_module("generate_bonds_only_package")
finalizer = load_module("finalize_bonds_only_manifest")


def manifest() -> dict:
    return json.loads((PILOT_DIR / "base_v3_deployment_manifest.json").read_text())


def intake() -> dict:
    return json.loads((PILOT_DIR / "base_v3_bonds_only_public_address_intake.template.json").read_text())


def complete_intake() -> dict:
    value = intake()
    value["roles"] = {
        "governanceSigners": [f"0x{number:040x}" for number in range(1, 6)],
        "guardian": "0x0000000000000000000000000000000000000006",
        "treasury": "0x0000000000000000000000000000000000000007",
        "attesters": ["0x0000000000000000000000000000000000000008", "0x0000000000000000000000000000000000000009", "0x000000000000000000000000000000000000000A"],
        "adjudicators": ["0x000000000000000000000000000000000000000B", "0x000000000000000000000000000000000000000C", "0x000000000000000000000000000000000000000D"],
        "designPartner": "0x000000000000000000000000000000000000000E",
    }
    value["approval"] = {"explicitMainnetApproval": True, "approvalEvidence": "owner-supplied-out-of-band"}
    return value


def finalized_manifest() -> dict:
    value = manifest()
    value["status"] = "finalization-candidate"
    value["provenance"] = {
        "sourceCommit": "a" * 40,
        "sourceTree": "b" * 40,
        "contractsTree": "c" * 40,
        "evidenceCommit": "d" * 40,
        "evidenceTree": "e" * 40,
        "sourceArchiveSha256": "f" * 64,
        "evidenceArchiveSha256": "1" * 64,
        "reviewerBundleSha256": "2" * 64,
        "validationArchiveSha256": "3" * 64,
    }
    payload = copy.deepcopy(value)
    payload.pop("integrity", None)
    value["integrity"]["manifestPayloadSha256"] = hashlib.sha256(
        manifest_module.canonical_json(payload)
    ).hexdigest()
    return value


def artifact(
    name: str,
    types: list[str],
    immutables: list[tuple[str, int]] | None = None,
    local_ast_immutables: list[tuple[str, int]] | None = None,
) -> dict:
    result = {
        "contractName": name,
        "abi": [{"type": "constructor", "inputs": [{"name": f"p{i}", "type": typ} for i, typ in enumerate(types)]}],
        "bytecode": {"object": "0x60006000"},
        "deployedBytecode": {"object": "0x6000"},
    }
    if immutables is not None:
        result["deployedBytecode"] = {
            "object": "0x" + "00" * (32 * len(immutables)),
            "immutableReferences": {
                str(node_id): [{"start": index * 32, "length": 32}]
                for index, (_, node_id) in enumerate(immutables)
            },
        }
        ast_immutables = immutables if local_ast_immutables is None else local_ast_immutables
        result["ast"] = {
            "nodeType": "SourceUnit",
            "nodes": [
                {"nodeType": "VariableDeclaration", "id": node_id, "name": immutable_name, "mutability": "immutable"}
                for immutable_name, node_id in ast_immutables
            ],
        }
    return result


class BondsOnlyManifestTests(unittest.TestCase):
    def test_checked_in_manifest_is_cryptographically_bound_and_review_only(self) -> None:
        value = manifest()
        manifest_module.validate_manifest(value)
        self.assertEqual(value["chain"], {"name": "base", "chainId": 8453})
        self.assertEqual(value["caps"]["globalWei"], "400000000000000000")
        self.assertEqual(value["caps"]["perPositionWei"], "200000000000000000")
        self.assertEqual(value["roles"]["governanceSigners"], [None] * 5)
        self.assertIsNone(value["roles"]["guardian"])
        self.assertIsNone(value["roles"]["treasury"])
        self.assertFalse(value["approval"]["deploymentAuthorized"])
        self.assertTrue(all(item is None for item in value["provenance"].values()))
        self.assertEqual(value["childBondPolicy"]["rewardProgramArgument"], "address(0)")
        self.assertEqual(value["childBondPolicy"]["maximumBondWei"], "20000000000000000")
        self.assertTrue(value["childBondPolicy"]["exactlyOneActiveAllowlistedPartner"])
        self.assertEqual(
            value["missionPolicy"]["priorities"],
            ["morals-over-metrics", "privacy-over-surveillance", "freedom-over-control"],
        )
        self.assertFalse(value["missionPolicy"]["metricsAffectSlashing"])
        self.assertFalse(value["missionPolicy"]["flourishingMeasurements"]["breachAdjudicationLinked"])
        self.assertEqual(
            [row["label"] for row in value["deployments"]],
            [
                "governance", "controller", "consensus", "bondFactory",
                "partnershipFactory", "accountabilityFactory",
            ],
        )
        forbidden = {"RewardProgramV3", "TaskEscrowV3", "MutualAidPoolV3", "VaultfireBIPVaultV3", "Solana", "cross-chain"}
        self.assertTrue(forbidden.issubset(value["scope"]["disabled"]))
        self.assertTrue(all(row["creationCodeKeccak256"] is None and row["runtimeCodeKeccak256"] is None for row in value["deployments"]))

    def test_manifest_rejects_integrity_scope_caps_and_placeholder_drift(self) -> None:
        mutations = [
            (lambda m: m["scope"]["enabled"].append("RewardProgramV3"), "enabled scope"),
            (lambda m: m["caps"].__setitem__("globalWei", "400000000000000001"), "cryptographic binding"),
            (lambda m: m["expectedAddresses"].__setitem__("governance", "0x0000000000000000000000000000000000000001"), "expected addresses"),
            (lambda m: m["guardianPauseFlow"].__setitem__("constructorTargets", ["$bond"]), "constructor targets"),
            (lambda m: m["provenance"].__setitem__("sourceCommit", "0" * 40), "provenance must remain null"),
            (lambda m: m["childBondPolicy"].__setitem__("rewardProgramArgument", "$reward"), "child bond policy"),
            (lambda m: m["missionPolicy"].__setitem__("surveillanceRequired", True), "mission policy"),
            (
                lambda m: m["missionPolicy"]["flourishingMeasurements"].__setitem__("slashable", True),
                "mission policy",
            ),
        ]
        for mutate, expected in mutations:
            with self.subTest(expected=expected):
                value = manifest()
                mutate(value)
                with self.assertRaisesRegex(manifest_module.ManifestValidationError, expected):
                    manifest_module.validate_manifest(value)

    def test_public_address_only_intake_is_fail_closed(self) -> None:
        value = intake()
        manifest_module.validate_public_intake(value)
        self.assertEqual(value["roles"]["governanceSigners"], [None] * 5)
        self.assertFalse(value["authorization"]["signing"])
        value["roles"]["guardian"] = "0x0000000000000000000000000000000000000001"
        with self.assertRaisesRegex(manifest_module.ManifestValidationError, "remain null"):
            manifest_module.validate_public_intake(value)

    def test_finalizer_derives_integration_provenance_from_git_not_template_constants(self) -> None:
        repository = PILOT_DIR.parents[1]
        commit = finalizer._git(repository, "rev-parse", "HEAD")
        with tempfile.TemporaryDirectory() as temp:
            reviewer = Path(temp) / "reviewer.tar.gz"
            validation = Path(temp) / "validation.tar.gz"
            reviewer.write_bytes(b"reviewer")
            validation.write_bytes(b"validation")
            parent = finalizer._git(repository, "rev-parse", f"{commit}^")
            finalized = finalizer.finalize_manifest(manifest(), repository, parent, commit, reviewer, validation)
            manifest_module.validate_manifest(finalized)
            self.assertEqual(finalized["status"], "finalization-candidate")
            self.assertEqual(finalized["provenance"]["sourceCommit"], parent)
            self.assertEqual(finalized["provenance"]["evidenceCommit"], commit)
            self.assertRegex(finalized["provenance"]["sourceArchiveSha256"], r"^[0-9a-f]{64}$")
            self.assertEqual(finalized["provenance"]["reviewerBundleSha256"], hashlib.sha256(b"reviewer").hexdigest())
            self.assertEqual(finalized["provenance"]["validationArchiveSha256"], hashlib.sha256(b"validation").hexdigest())
            self.assertNotEqual(finalized["integrity"]["manifestPayloadSha256"], manifest()["integrity"]["manifestPayloadSha256"])

    def test_unsigned_generator_binds_nonce_addresses_constructor_and_code_hashes(self) -> None:
        definitions = {
            "MultisigGovernanceV3": ["address[]", "uint256", "uint256", "uint256", "address", "uint256", "address[]", "address[]", "bytes32[]"],
            "BasePilotAccessControllerV3": ["address", "uint256", "uint256"], "ThresholdConsensusV3": ["address", "uint8"],
            "DisputeRegistryV3": ["address"],
            "SafeBondFactoryV3": ["address", "address", "address", "address", "uint256", "uint256", "uint256", "uint256", "uint256", "bytes32", "bytes32", "bytes32", "bytes32", "bytes32"],
            "PartnershipBondFactoryV3": ["address", "address", "address", "address", "uint256", "uint256", "uint256", "address"],
            "AccountabilityBondFactoryV3": ["address", "address", "address", "address", "uint256", "uint256", "address", "bytes", "bytes32"],
            "PartnershipBondV3Safe": [], "AccountabilityBondV3Safe": [],
        }
        with tempfile.TemporaryDirectory() as temp:
            out = Path(temp)
            for name, types in definitions.items():
                (out / f"{name}.json").write_text(json.dumps(artifact(name, types)))
            first = generator.generate_package(finalized_manifest(), complete_intake(), out, "0x00000000000000000000000000000000000000AA", 31)
            second = generator.generate_package(finalized_manifest(), complete_intake(), out, "0x00000000000000000000000000000000000000AA", 31)
        self.assertEqual(first, second)
        self.assertTrue(first["unsigned"])
        self.assertFalse(first["broadcast"])
        self.assertEqual(first["addressDerivation"]["method"], "CREATE/deployer-nonce")
        self.assertEqual(len(first["transactions"]), 6)
        self.assertTrue(all(tx["valueWei"] == "0" and tx["creationCodeKeccak256"].startswith("0x") and tx["runtimeCodeKeccak256"].startswith("0x") for tx in first["transactions"]))
        self.assertEqual(first["transactions"][0]["label"], "governance")
        self.assertEqual(first["transactions"][-1]["label"], "accountabilityFactory")
        self.assertEqual(
            first["transactions"][1]["constructorArgs"],
            [first["transactions"][0]["predictedAddress"], 400_000_000_000_000_000, 200_000_000_000_000_000],
        )
        self.assertEqual(
            first["factoryConfiguration"]["arguments"],
            [
                first["transactions"][4]["predictedAddress"],
                first["transactions"][5]["predictedAddress"],
            ],
        )
        self.assertTrue(first["factoryConfiguration"]["directCallForbidden"])
        self.assertTrue(first["factoryConfiguration"]["calldata"].startswith("0x"))
        self.assertEqual(
            first["transactions"][3]["constructorArgs"][-5:],
            [
                first["runtimeIdentity"]["expectedPartnershipFactoryRuntimeCodeHash"],
                first["runtimeIdentity"]["expectedAccountabilityFactoryRuntimeCodeHash"],
                first["runtimeIdentity"]["expectedPartnershipBondRuntimeCodeHash"],
                first["runtimeIdentity"]["expectedAccountabilityBondRuntimeCodeHash"],
                first["runtimeIdentity"]["expectedAccountabilityBondInitCodeHash"],
            ],
        )
        self.assertTrue(all(value.startswith("0x") for value in first["runtimeIdentity"].values()))
        self.assertTrue(all(len(value) == 66 for key, value in first["runtimeIdentity"].items() if key.endswith("Hash")))
        self.assertEqual(first["guardianPauseFlow"]["constructorTargets"], [])

    def test_bonds_only_generator_binds_foundry_child_ast_with_inherited_eip712_immutable(self) -> None:
        """Match Foundry 0.8.24: artifact AST is local; inherited declaration is build-info only."""
        definitions = {
            "MultisigGovernanceV3": ["address[]", "uint256", "uint256", "uint256", "address", "uint256", "address[]", "address[]", "bytes32[]"],
            "BasePilotAccessControllerV3": ["address", "uint256", "uint256"],
            "ThresholdConsensusV3": ["address", "uint8"],
            "DisputeRegistryV3": ["address"],
            "SafeBondFactoryV3": [
                "address", "address", "address", "address", "uint256", "uint256", "uint256", "uint256", "uint256",
                "bytes32", "bytes32", "bytes32", "bytes32", "bytes32",
            ],
            "PartnershipBondFactoryV3": ["address", "address", "address", "address", "uint256", "uint256", "uint256", "address"],
            "AccountabilityBondFactoryV3": ["address", "address", "address", "address", "uint256", "uint256", "address", "bytes", "bytes32"],
            "PartnershipBondV3Safe": [],
            "AccountabilityBondV3Safe": [],
        }
        # IDs and local declarations are the real solc 0.8.24
        # PartnershipBondV3Safe artifact shape. ID 17935 belongs to
        # EIP712BaseV3._VERSION_HASH, which is absent from the child AST.
        partnership_immutables = [
            ("_VERSION_HASH", 17935),
            ("rewardProgram", 3237),
            ("consensus", 3240),
            ("disputeRegistry", 3243),
            ("pilotController", 3247),
            ("maxPartyNativePrincipal", 3249),
            ("maxBondNativePrincipal", 3251),
            ("maxAggregateNativeLiability", 3253),
        ]
        with tempfile.TemporaryDirectory() as temporary:
            out = Path(temporary)
            for name, types in definitions.items():
                value = artifact(
                    name,
                    types,
                    partnership_immutables if name == "PartnershipBondV3Safe" else None,
                    partnership_immutables[1:] if name == "PartnershipBondV3Safe" else None,
                )
                output = out / f"{name}.sol"
                output.mkdir()
                (output / f"{name}.json").write_text(json.dumps(value))
            build_info = out / "build-info"
            build_info.mkdir()
            (build_info / "foundry-0.8.24.json").write_text(
                json.dumps(
                    {
                        "output": {
                            "sources": {
                                "contracts/signatures/EIP712BaseV3.sol": {
                                    "ast": {
                                        "nodeType": "SourceUnit",
                                        "nodes": [
                                            {
                                                "nodeType": "VariableDeclaration",
                                                "id": 17935,
                                                "name": "_VERSION_HASH",
                                                "mutability": "immutable",
                                            }
                                        ],
                                    }
                                }
                            }
                        }
                    }
                )
            )
            package = generator.generate_package(
                finalized_manifest(),
                complete_intake(),
                out,
                "0x00000000000000000000000000000000000000AA",
                31,
            )
            loaded = generator._find_artifacts(out)
            addresses = {
                label: generator.compute_create_address("0x00000000000000000000000000000000000000AA", 31 + sequence)
                for sequence, label in enumerate(manifest_module.DEPLOYMENT_ORDER)
            }
            runtime = generator._expected_runtime(
                loaded["PartnershipBondV3Safe"],
                "PartnershipBondV3Safe",
                "partnershipBond",
                {"caps": finalized_manifest()["caps"], "roles": {"governanceSigners": complete_intake()["roles"]["governanceSigners"], "guardian": complete_intake()["roles"]["guardian"]}},
                addresses,
            )
        self.assertEqual(len(package["runtimeIdentity"]), 6)
        self.assertEqual(runtime[:32].hex(), generator._keccak(b"3.0.0").hex())

    def test_generator_refuses_unapproved_or_abi_drifted_input(self) -> None:
        definitions = {
            "MultisigGovernanceV3": ["address[]", "uint256", "uint256", "uint256", "address", "uint256", "address[]", "address[]", "bytes32[]"],
            "BasePilotAccessControllerV3": ["uint256"], "ThresholdConsensusV3": ["address", "uint8"],
            "DisputeRegistryV3": ["address"],
            "SafeBondFactoryV3": ["address", "address", "address", "address", "uint256", "uint256", "uint256", "uint256", "uint256", "bytes32", "bytes32", "bytes32", "bytes32", "bytes32"],
            "PartnershipBondFactoryV3": ["address", "address", "address", "address", "uint256", "uint256", "uint256", "address"],
            "AccountabilityBondFactoryV3": ["address", "address", "address", "address", "uint256", "uint256", "address", "bytes", "bytes32"],
            "PartnershipBondV3Safe": [], "AccountabilityBondV3Safe": [],
        }
        with tempfile.TemporaryDirectory() as temp:
            out = Path(temp)
            for name, types in definitions.items():
                (out / f"{name}.json").write_text(json.dumps(artifact(name, types)))
            denied = complete_intake()
            denied["approval"] = {"explicitMainnetApproval": False, "approvalEvidence": None}
            with self.assertRaisesRegex(manifest_module.ManifestValidationError, "explicit owner approval"):
                generator.generate_package(finalized_manifest(), denied, out, "0x00000000000000000000000000000000000000AA", 1)
            with self.assertRaisesRegex(manifest_module.ManifestValidationError, "constructor ABI drifted"):
                generator.generate_package(finalized_manifest(), complete_intake(), out, "0x00000000000000000000000000000000000000AA", 1)

    def test_generator_refuses_unfinalized_template(self) -> None:
        with tempfile.TemporaryDirectory() as temp:
            with self.assertRaisesRegex(manifest_module.ManifestValidationError, "finalization-candidate"):
                generator.generate_package(manifest(), complete_intake(), Path(temp), "0x00000000000000000000000000000000000000AA", 1)


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