"""Focused regression tests for the Base read-only bootstrap profile."""
from __future__ import annotations

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

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


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


bootstrap = _load("bootstrap_profile")
generator = _load("generate_bootstrap_package")


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


def deployable_profile() -> dict:
    value = copy.deepcopy(template())
    value["status"] = "deployable-read-only"
    value["approval"]["bootstrapDeploymentAuthorized"] = True
    signers = [f"0x{number:040x}" for number in range(1, 6)]
    value["roles"]["governanceSigners"] = signers
    value["roles"]["guardian"] = "0x0000000000000000000000000000000000000006"
    value["roles"]["treasury"] = "0x0000000000000000000000000000000000000007"
    value["roles"]["signerCustodyDisclosures"] = [
        {
            "address": signer,
            "custody": "founder-controlled-non-independent",
            "nonIndependent": True,
            "rotationRequired": True,
        }
        for signer in signers
    ]
    return value


def artifact(
    name: str, constructor_types: list[str], immutables: list[tuple[str, int]] | None = None, *, include_contract_name: bool = True
) -> dict:
    result = {
        "abi": [{"type": "constructor", "inputs": [{"name": f"p{i}", "type": t} for i, t in enumerate(constructor_types)]}],
        "bytecode": {"object": "0x60006000"},
        "deployedBytecode": {"object": "0x6000"},
    }
    if include_contract_name:
        result["contractName"] = name
    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)
            },
        }
        result["ast"] = {
            "nodeType": "SourceUnit",
            "nodes": [
                {"nodeType": "VariableDeclaration", "id": node_id, "name": immutable_name, "mutability": "immutable"}
                for immutable_name, node_id in immutables
            ],
        }
    return result


class BaseReadOnlyBootstrapProfileTests(unittest.TestCase):
    def test_public_role_collection_template_is_complete_but_contains_no_addresses(self) -> None:
        template = json.loads(
            (PILOT_DIR / "base_v3_read_only_public_roles.template.json").read_text(encoding="utf-8")
        )
        self.assertEqual(template["status"], "awaiting-owner-input")
        self.assertEqual(template["chain"], {"name": "base", "chainId": 8453})
        self.assertEqual(template["frozenSourceCommit"], bootstrap.BOOTSTRAP_CONTRACT_SOURCE_COMMIT)
        signers = template["governance"]["signers"]
        self.assertEqual([record["slot"] for record in signers], [1, 2, 3, 4, 5])
        self.assertTrue(all(record["address"] is None for record in signers))
        self.assertTrue(all(record["custody"]["classification"] is None for record in signers))
        self.assertIsNone(template["guardian"]["address"])
        self.assertIsNone(template["treasury"]["address"])
        self.assertEqual(
            template["allowedCustodyClassifications"]["founder-controlled-non-independent"],
            {
                "requiredNonIndependent": True,
                "requiredRotationRequired": True,
                "meaning": (
                    "Founder-controlled bootstrap signer. It is not represented as an independent partner "
                    "and must be rotated before partner value, pilot writes, or production activation."
                ),
            },
        )
        self.assertEqual(
            template["authorization"],
            {
                "bootstrapDeploymentAuthorized": False,
                "mainnetBroadcastApproved": False,
                "pilotReady": False,
            },
        )

    def test_template_is_read_only_and_never_claims_pilot_readiness(self) -> None:
        profile = template()
        bootstrap.validate_bootstrap_profile(profile)
        self.assertFalse(profile["pilotReady"])
        self.assertEqual(profile["roles"]["governanceSigners"], [None] * 5)
        self.assertEqual(profile["roles"]["attesters"], [])
        self.assertEqual(profile["roles"]["adjudicators"], [])
        self.assertEqual(profile["roles"]["designPartners"], [])
        self.assertEqual(profile["roles"]["pilotModules"], [])
        self.assertFalse(profile["controller"]["deployEnabled"])
        self.assertFalse(profile["controller"]["writeEnabled"])
        self.assertFalse(profile["bondFactory"]["deployAllowed"])
        self.assertFalse(profile["discoveryAndPayments"]["x402Enabled"])
        self.assertFalse(profile["discoveryAndPayments"]["bazaarDiscoveryEnabled"])
        self.assertFalse(profile["discoveryAndPayments"]["publicValueAcceptanceEnabled"])
        self.assertEqual(profile["valueAcceptance"]["deploymentTransactionValueWei"], "0")

    def test_deployable_requires_five_distinct_signers_separate_guardian_treasury_and_truthful_custody(self) -> None:
        profile = deployable_profile()
        bootstrap.validate_bootstrap_profile(profile, deployable=True)
        profile["roles"]["governanceSigners"][4] = profile["roles"]["governanceSigners"][0]
        with self.assertRaisesRegex(bootstrap.BootstrapProfileValidationError, "distinct"):
            bootstrap.validate_bootstrap_profile(profile, deployable=True)

        profile = deployable_profile()
        profile["roles"]["guardian"] = profile["roles"]["governanceSigners"][0]
        with self.assertRaisesRegex(bootstrap.BootstrapProfileValidationError, "guardian must be separate"):
            bootstrap.validate_bootstrap_profile(profile, deployable=True)

        profile = deployable_profile()
        profile["roles"]["signerCustodyDisclosures"][0]["nonIndependent"] = False
        with self.assertRaisesRegex(bootstrap.BootstrapProfileValidationError, "non-independent"):
            bootstrap.validate_bootstrap_profile(profile, deployable=True)

    def test_profile_rejects_every_bootstrap_enablement_or_fabricated_operating_role(self) -> None:
        mutations = (
            (lambda p: p["controller"].__setitem__("deployEnabled", True), "deployEnabled"),
            (lambda p: p["controller"].__setitem__("writeEnabled", True), "writeEnabled"),
            (lambda p: p["controller"]["modules"].append("0x0000000000000000000000000000000000000008"), "modules"),
            (lambda p: p["roles"]["attesters"].append("0x0000000000000000000000000000000000000008"), "attesters"),
            (lambda p: p["roles"]["designPartners"].append("0x0000000000000000000000000000000000000008"), "designPartners"),
            (lambda p: p["consensus"]["authorizedAttesters"].append("0x0000000000000000000000000000000000000008"), "authorizedAttesters"),
            (lambda p: p["bondFactory"].__setitem__("deployAllowed", True), "deployAllowed"),
            (lambda p: p["valueAcceptance"].__setitem__("deploymentTransactionValueWei", "1"), "transaction value"),
            (lambda p: p["approval"]["independentReviewEvidence"].append("unverified-readiness-claim"), "independentReviewEvidence"),
            (lambda p: p.__setitem__("sourceCommit", "cbdd90c3a6887436d566364d0687a30c7522a7a8"), "source commit"),
            (lambda p: p.__setitem__("pilotReady", True), "pilotReady"),
        )
        for mutate, expected in mutations:
            with self.subTest(expected=expected):
                profile = deployable_profile()
                mutate(profile)
                with self.assertRaisesRegex(bootstrap.BootstrapProfileValidationError, expected):
                    bootstrap.validate_bootstrap_profile(profile, deployable=True)

    def test_unsigned_package_is_deterministic_zero_value_and_contains_runtime_hash_evidence(self) -> None:
        profile = deployable_profile()
        with tempfile.TemporaryDirectory() as temporary:
            artifacts = Path(temporary)
            definitions = {
                "ThresholdConsensusV3": ["address", "uint8"],
                "DisputeRegistryV3": ["address"],
                "BasePilotAccessControllerV3": ["address", "uint256", "uint256"],
                "MultisigGovernanceV3": ["address[]", "uint256", "uint256", "uint256", "address", "uint256", "address[]", "address[]", "bytes32[]"],
                "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": [],
            }
            for name, types in definitions.items():
                immutables = [("governance", 1), ("maxGlobalCap", 2), ("maxPerPositionCap", 3)] if name == "BasePilotAccessControllerV3" else None
                (artifacts / f"{name}.json").write_text(json.dumps(artifact(name, types, immutables)))
            first = generator.generate_package(profile, artifacts, "0x00000000000000000000000000000000000000AA", 17)
            second = generator.generate_package(profile, artifacts, "0x00000000000000000000000000000000000000AA", 17)
        self.assertEqual(first, second)
        self.assertTrue(first["unsigned"])
        self.assertFalse(first["broadcast"])
        self.assertFalse(first["pilotReady"])
        self.assertEqual(len(first["transactions"]), 4)
        self.assertEqual(first["constructorInternalCreates"], [])
        self.assertTrue(all(tx["valueWei"] == "0" and tx["to"] is None for tx in first["transactions"]))
        self.assertTrue(all(tx["expectedRuntimeCodeKeccak256"].startswith("0x") for tx in first["transactions"]))
        self.assertEqual(
            first["postDeployExpectedState"]["bondFactory.partnershipFactory"],
            "0x0000000000000000000000000000000000000000",
        )
        self.assertEqual(
            first["postDeployExpectedState"]["bondFactory.accountabilityFactory"],
            "0x0000000000000000000000000000000000000000",
        )
        self.assertEqual(first["transactions"][0]["label"], "governance")
        self.assertEqual(
            first["transactions"][1]["constructorArgs"][1:],
            [400_000_000_000_000_000, 200_000_000_000_000_000],
        )
        self.assertEqual(len(first["transactions"][3]["constructorArgs"]), 14)
        self.assertTrue(
            all(
                value.startswith("0x") and len(value) == 66
                for key, value in first["postDeployExpectedState"]["runtimeIdentity"].items()
                if key.endswith("CodeHash")
            )
        )
        controller_runtime_hash = first["transactions"][1]["expectedRuntimeCodeKeccak256"]
        self.assertNotEqual(controller_runtime_hash, "0x" + generator._keccak(bytes(32)).hex())

    def test_package_generation_fails_closed_on_constructor_abi_drift(self) -> None:
        profile = deployable_profile()
        definitions = {
            "ThresholdConsensusV3": ["address", "uint8"],
            "DisputeRegistryV3": ["address"],
            "BasePilotAccessControllerV3": ["uint256"],
            "MultisigGovernanceV3": ["address[]", "uint256", "uint256", "uint256", "address", "uint256", "address[]", "address[]", "bytes32[]"],
            "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 temporary:
            artifacts = Path(temporary)
            for name, types in definitions.items():
                (artifacts / f"{name}.json").write_text(json.dumps(artifact(name, types)))
            with self.assertRaisesRegex(bootstrap.BootstrapProfileValidationError, "controller constructor ABI drifted"):
                generator.generate_package(profile, artifacts, "0x00000000000000000000000000000000000000AA", 17)

    def test_package_generation_accepts_only_canonical_foundry_artifact_layout_without_contract_name(self) -> None:
        profile = deployable_profile()
        definitions = {
            "ThresholdConsensusV3": ["address", "uint8"],
            "DisputeRegistryV3": ["address"],
            "BasePilotAccessControllerV3": ["address", "uint256", "uint256"],
            "MultisigGovernanceV3": ["address[]", "uint256", "uint256", "uint256", "address", "uint256", "address[]", "address[]", "bytes32[]"],
            "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 temporary:
            artifacts = Path(temporary)
            for name, types in definitions.items():
                output = artifacts / f"{name}.sol"
                output.mkdir()
                (output / f"{name}.json").write_text(
                    json.dumps(artifact(name, types, include_contract_name=False))
                )
            package = generator.generate_package(profile, artifacts, "0x00000000000000000000000000000000000000AA", 17)
        self.assertEqual(len(package["transactions"]), 4)

    def test_foundry_style_artifacts_bind_every_specialized_and_inherited_immutable(self) -> None:
        """A full runtime-identity fixture catches omitted inherited immutables."""
        profile = deployable_profile()
        definitions = {
            "ThresholdConsensusV3": ["address", "uint8"],
            "DisputeRegistryV3": ["address"],
            "BasePilotAccessControllerV3": ["address", "uint256", "uint256"],
            "MultisigGovernanceV3": ["address[]", "uint256", "uint256", "uint256", "address", "uint256", "address[]", "address[]", "bytes32[]"],
            "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": [],
        }
        immutables = {
            "BasePilotAccessControllerV3": [("governance", 101), ("maxGlobalCap", 102), ("maxPerPositionCap", 103)],
            "MultisigGovernanceV3": [("minimumTimelockSeconds", 104), ("guardianRotationDelay", 105)],
            "SafeBondFactoryV3": [
                ("consensus", 106), ("disputeRegistry", 107), ("pilotController", 108),
                ("partnershipMaxPartyNativePrincipal", 109), ("partnershipMaxBondNativePrincipal", 110),
                ("partnershipMaxAggregateNativeLiability", 111), ("accountabilityMaxBondNativePrincipal", 112),
                ("accountabilityMaxAggregateNativeLiability", 113),
                ("expectedPartnershipFactoryRuntimeCodeHash", 114),
                ("expectedAccountabilityFactoryRuntimeCodeHash", 115),
                ("expectedPartnershipBondRuntimeCodeHash", 116),
                ("expectedAccountabilityBondRuntimeCodeHash", 117),
                ("expectedAccountabilityBondInitCodeHash", 147),
            ],
            "PartnershipBondFactoryV3": [
                ("coordinator", 118), ("bondOwner", 119), ("consensus", 120), ("disputeRegistry", 121),
                ("pilotController", 122), ("maxPartyNativePrincipal", 123), ("maxBondNativePrincipal", 124),
                ("maxAggregateNativeLiability", 125),
            ],
            "AccountabilityBondFactoryV3": [
                ("coordinator", 126), ("bondOwner", 127), ("consensus", 128), ("disputeRegistry", 129),
                ("pilotController", 130), ("maxBondNativePrincipal", 131), ("maxAggregateNativeLiability", 132),
                ("childInitCodeHash", 148), ("expectedChildRuntimeCodeHash", 149),
            ],
            "PartnershipBondV3Safe": [
                ("_VERSION_HASH", 133), ("rewardProgram", 134), ("consensus", 135), ("disputeRegistry", 136),
                ("pilotController", 137), ("maxPartyNativePrincipal", 138), ("maxBondNativePrincipal", 139),
                ("maxAggregateNativeLiability", 140),
            ],
            "AccountabilityBondV3Safe": [
                ("_VERSION_HASH", 141), ("consensus", 142), ("disputeRegistry", 143), ("pilotController", 144),
                ("maxBondNativePrincipal", 145), ("maxAggregateNativeLiability", 146),
            ],
        }
        with tempfile.TemporaryDirectory() as temporary:
            artifacts = Path(temporary)
            source_asts = [
                {
                    "ast": {
                        "nodeType": "SourceUnit",
                        "nodes": [
                            {
                                "nodeType": "VariableDeclaration",
                                "id": 133,
                                "name": "_VERSION_HASH",
                                "mutability": "immutable",
                            },
                            {
                                "nodeType": "VariableDeclaration",
                                "id": 141,
                                "name": "_VERSION_HASH",
                                "mutability": "immutable",
                            },
                        ],
                    }
                }
            ]
            for name, types in definitions.items():
                artifact_immutables = immutables.get(name)
                output = artifacts / f"{name}.sol"
                output.mkdir()
                value = artifact(name, types, artifact_immutables, include_contract_name=False)
                if name in ("PartnershipBondV3Safe", "AccountabilityBondV3Safe"):
                    # Actual Foundry 0.8.24 artifacts carry the child source
                    # unit AST but not inherited EIP712BaseV3 declarations.
                    value["ast"]["nodes"] = [
                        node
                        for node in value["ast"]["nodes"]
                        if node["name"] != "_VERSION_HASH"
                    ]
                (output / f"{name}.json").write_text(json.dumps(value))
                if artifact_immutables:
                    source_immutables = [
                        (immutable_name, node_id)
                        for immutable_name, node_id in artifact_immutables
                        if immutable_name != "_VERSION_HASH"
                    ]
                    source_asts.append(
                        {
                            "ast": {
                                "nodeType": "SourceUnit",
                                "nodes": [
                                    {"nodeType": "VariableDeclaration", "id": node_id, "name": immutable_name, "mutability": "immutable"}
                                    for immutable_name, node_id in source_immutables
                                ],
                            }
                        }
                    )
            build_info = artifacts / "build-info"
            build_info.mkdir()
            (build_info / "clean-foundry-build.json").write_text(
                json.dumps({"output": {"sources": {f"source-{index}": ast for index, ast in enumerate(source_asts)}}})
            )
            package = generator.generate_package(profile, artifacts, "0x00000000000000000000000000000000000000AA", 17)
            loaded = generator._artifact_contracts(artifacts)
            addresses = {
                label: generator.compute_create_address("0x00000000000000000000000000000000000000AA", 17 + index)
                for index, label in enumerate(bootstrap.BOOTSTRAP_DEPLOYMENTS)
            }
            runtime = generator._expected_runtime(
                loaded["PartnershipBondV3Safe"],
                "PartnershipBondV3Safe",
                "partnershipBond",
                {"caps": {"globalWei": "400000000000000000", "perPositionWei": "200000000000000000", "partnershipPartyWei": "10000000000000000", "partnershipBondWei": "20000000000000000", "partnershipAggregateWei": "200000000000000000", "accountabilityBondWei": "20000000000000000", "accountabilityAggregateWei": "200000000000000000"}},
                addresses,
            )
        self.assertEqual(len(package["postDeployExpectedState"]["runtimeIdentity"]), 9)
        self.assertEqual(
            generator._immutable_names(loaded["PartnershipBondV3Safe"], "PartnershipBondV3Safe")["133"],
            "_VERSION_HASH",
        )
        self.assertEqual(runtime[:32].hex(), generator.EIP712_V3_VERSION_HASH[2:])
        self.assertEqual(generator.EIP712_V3_VERSION_HASH, "0xd7a1ce683065975771bedf401ecab037f4f4c62cc51fefdc8b39dd246ff0343a")

    def test_package_generation_refuses_draft_or_enabled_profile(self) -> None:
        with tempfile.TemporaryDirectory() as temporary:
            artifacts = Path(temporary)
            with self.assertRaisesRegex(bootstrap.BootstrapProfileValidationError, "status=deployable-read-only"):
                generator.generate_package(template(), artifacts, "0x00000000000000000000000000000000000000AA", 1)


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