// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;

import "forge-std/Test.sol";
import { SafeBondFactoryV3 } from "../../contracts/bonds/SafeBondFactoryV3.sol";
import { PartnershipBondFactoryV3 } from "../../contracts/bonds/PartnershipBondFactoryV3.sol";
import { AccountabilityBondFactoryV3 } from "../../contracts/bonds/AccountabilityBondFactoryV3.sol";
import { PartnershipBondV3Safe } from "../../contracts/bonds/PartnershipBondV3Safe.sol";
import { AccountabilityBondV3Safe } from "../../contracts/bonds/AccountabilityBondV3Safe.sol";
import { RewardProgramV3 } from "../../contracts/bonds/RewardProgramV3.sol";
import { ThresholdConsensusV3 } from "../../contracts/consensus/ThresholdConsensusV3.sol";
import { DisputeRegistryV3 } from "../../contracts/disputes/DisputeRegistryV3.sol";

/// @notice Deployment-gate and constructor-ownership coverage for the V3 factories.
contract SafeBondFactoryV3Test is Test {
    address private constant GOVERNANCE_MULTISIG = address(0xBEEF);
    address private constant UNAUTHORIZED = address(0xBAD);
    uint256 private constant PARTNERSHIP_PARTY_CAP = 10 ether;
    uint256 private constant PARTNERSHIP_BOND_CAP = 20 ether;
    uint256 private constant PARTNERSHIP_AGGREGATE_CAP = 100 ether;
    uint256 private constant ACCOUNTABILITY_BOND_CAP = 10 ether;
    uint256 private constant ACCOUNTABILITY_AGGREGATE_CAP = 100 ether;

    SafeBondFactoryV3 private factory;
    RewardProgramV3 private rewardProgram;
    ThresholdConsensusV3 private consensus;
    DisputeRegistryV3 private disputes;

    event PartnershipBondDeployedV3(address indexed bond, address indexed designPartner);
    event AccountabilityBondDeployedV3(address indexed bond, address indexed designPartner);
    event DeployAllowedSetV3(bool allowed);

    function setUp() public {
        consensus = new ThresholdConsensusV3(GOVERNANCE_MULTISIG, 2);
        disputes = new DisputeRegistryV3(GOVERNANCE_MULTISIG);
        PartnershipBondV3Safe partnershipIdentity = new PartnershipBondV3Safe(
            GOVERNANCE_MULTISIG,
            address(0),
            address(consensus),
            address(disputes),
            PARTNERSHIP_PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            address(0)
        );
        AccountabilityBondV3Safe accountabilityIdentity = new AccountabilityBondV3Safe(
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(0)
        );
        address predictedCoordinator = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2);
        PartnershipBondFactoryV3 partnershipFactory = new PartnershipBondFactoryV3(
            predictedCoordinator,
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            PARTNERSHIP_PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            address(0)
        );
        AccountabilityBondFactoryV3 accountabilityFactory = new AccountabilityBondFactoryV3(
            predictedCoordinator,
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(0),
            _accountabilityInitCode(),
            address(accountabilityIdentity).codehash
        );
        factory = new SafeBondFactoryV3(
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            address(0),
            PARTNERSHIP_PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(partnershipFactory).codehash,
            address(accountabilityFactory).codehash,
            address(partnershipIdentity).codehash,
            address(accountabilityIdentity).codehash,
            keccak256(_accountabilityInitCode())
        );
        assertEq(address(factory), predictedCoordinator, "coordinator prediction drifted");
        vm.prank(GOVERNANCE_MULTISIG);
        factory.configureFactories(address(partnershipFactory), address(accountabilityFactory));
        rewardProgram = new RewardProgramV3(GOVERNANCE_MULTISIG);
    }

    function test_factoryConfigurationIsOneTimeAndFullyBound() external {
        address partnershipFactory = address(factory.partnershipFactory());
        address accountabilityFactory = address(factory.accountabilityFactory());
        vm.expectRevert(SafeBondFactoryV3.FactoriesAlreadyConfigured.selector);
        vm.prank(GOVERNANCE_MULTISIG);
        factory.configureFactories(partnershipFactory, accountabilityFactory);
    }

    function test_thinFactoryRejectsInitcodeThatDoesNotProducePinnedRuntime() external {
        AccountabilityBondFactoryV3 thin = new AccountabilityBondFactoryV3(
            address(this),
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(0),
            hex"60006000f3",
            bytes32(uint256(1))
        );
        vm.expectRevert(AccountabilityBondFactoryV3.RuntimeCodeHashMismatch.selector);
        thin.deploy();
    }

    function test_coordinatorRejectsAlternateInitcodeEvenWhenExpectedChildRuntimeMatches() external {
        bytes memory honest = _accountabilityInitCode();
        bytes memory alternate = bytes.concat(honest, hex"00");
        address predictedCoordinator = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2);
        PartnershipBondFactoryV3 p = new PartnershipBondFactoryV3(
            predictedCoordinator,
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(0),
            PARTNERSHIP_PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            address(0)
        );
        AccountabilityBondFactoryV3 a = new AccountabilityBondFactoryV3(
            predictedCoordinator,
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(0),
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(0),
            alternate,
            factory.expectedAccountabilityBondRuntimeCodeHash()
        );
        SafeBondFactoryV3 coordinator = new SafeBondFactoryV3(
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(0),
            address(0),
            PARTNERSHIP_PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(p).codehash,
            address(a).codehash,
            factory.expectedPartnershipBondRuntimeCodeHash(),
            factory.expectedAccountabilityBondRuntimeCodeHash(),
            keccak256(honest)
        );
        assertEq(address(coordinator), predictedCoordinator);
        vm.expectRevert(SafeBondFactoryV3.InvalidFactory.selector);
        vm.prank(GOVERNANCE_MULTISIG);
        coordinator.configureFactories(address(p), address(a));
    }

    function test_getterCompatibleCounterfeitFactoryIsRejectedByPinnedRuntimeHash() external {
        // Build the honest reference factories for the *same* predicted
        // coordinator values as the unconfigured coordinator below. This
        // proves the counterfeit passes every getter check and is rejected
        // solely because its runtime identity is not the reviewed factory.
        address predictedUnconfigured = vm.computeCreateAddress(address(this), vm.getNonce(address(this)) + 2);
        PartnershipBondFactoryV3 honestPartnership = new PartnershipBondFactoryV3(
            predictedUnconfigured,
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            PARTNERSHIP_PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            address(0)
        );
        AccountabilityBondFactoryV3 honestAccountability = new AccountabilityBondFactoryV3(
            predictedUnconfigured,
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(0),
            _accountabilityInitCode(),
            factory.expectedAccountabilityBondRuntimeCodeHash()
        );
        SafeBondFactoryV3 unconfigured = new SafeBondFactoryV3(
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            address(0),
            PARTNERSHIP_PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            address(honestPartnership).codehash,
            address(honestAccountability).codehash,
            factory.expectedPartnershipBondRuntimeCodeHash(),
            factory.expectedAccountabilityBondRuntimeCodeHash(),
            honestAccountability.childInitCodeHash()
        );
        assertEq(address(unconfigured), predictedUnconfigured, "counterfeit fixture coordinator prediction drifted");

        GetterCompatibleCounterfeitPartnershipFactory counterfeit = new GetterCompatibleCounterfeitPartnershipFactory(
            address(unconfigured),
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            address(0),
            PARTNERSHIP_PARTY_CAP,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP
        );

        vm.startPrank(GOVERNANCE_MULTISIG);
        vm.expectRevert(SafeBondFactoryV3.FactoryRuntimeCodeHashMismatch.selector);
        unconfigured.configureFactories(address(counterfeit), address(honestAccountability));
        vm.stopPrank();
    }

    function test_unauthorizedCallerCannotDeployOrChangeGate() external {
        vm.startPrank(UNAUTHORIZED);

        vm.expectRevert(SafeBondFactoryV3.NotOwner.selector);
        factory.setDeployAllowed(true);

        vm.expectRevert(SafeBondFactoryV3.NotOwner.selector);
        factory.deployPartnership(address(0xD3516), address(0));

        vm.expectRevert(SafeBondFactoryV3.NotOwner.selector);
        factory.deployAccountability(address(0xD3516));

        vm.stopPrank();
    }

    function test_deployAllowedGateBlocksDeploymentsWhenPaused() external {
        vm.prank(GOVERNANCE_MULTISIG);
        vm.expectRevert(SafeBondFactoryV3.DeployDisabled.selector);
        factory.deployPartnership(address(0xD3516), address(0));

        vm.expectEmit(false, false, false, true, address(factory));
        emit DeployAllowedSetV3(true);
        vm.prank(GOVERNANCE_MULTISIG);
        factory.setDeployAllowed(true);
        assertTrue(factory.deployAllowed(), "deployment gate was not enabled");

        vm.prank(GOVERNANCE_MULTISIG);
        factory.deployAccountability(address(0xD3516));

        vm.expectEmit(false, false, false, true, address(factory));
        emit DeployAllowedSetV3(false);
        vm.prank(GOVERNANCE_MULTISIG);
        factory.setDeployAllowed(false);
        assertFalse(factory.deployAllowed(), "deployment gate was not disabled");

        vm.prank(GOVERNANCE_MULTISIG);
        vm.expectRevert(SafeBondFactoryV3.DeployDisabled.selector);
        factory.deployAccountability(address(0xD3516));
    }

    function test_deployPartnershipEmitsAndReturnsChildWithFinalOwnerAndDisabledRewardProgram() external {
        _enableDeployments();

        PartnershipBondFactoryV3 deployer = factory.partnershipFactory();
        address expectedBond = vm.computeCreateAddress(address(deployer), vm.getNonce(address(deployer)));

        vm.expectEmit(true, false, false, false, address(factory));
        emit PartnershipBondDeployedV3(expectedBond, address(0xD3516));

        vm.prank(GOVERNANCE_MULTISIG);
        address returnedBond = factory.deployPartnership(address(0xD3516), address(0));

        assertEq(returnedBond, expectedBond, "returned partnership address differs from emitted deployment");
        assertEq(PartnershipBondV3Safe(returnedBond).owner(), GOVERNANCE_MULTISIG, "child owner is not governance");
        assertEq(
            address(PartnershipBondV3Safe(returnedBond).consensus()), address(consensus), "child consensus is not bound"
        );
        assertEq(
            address(PartnershipBondV3Safe(returnedBond).disputeRegistry()),
            address(0),
            "retired dispute registry remains bound"
        );
        assertEq(
            PartnershipBondV3Safe(returnedBond).rewardProgram(),
            address(0),
            "initial-pilot child has a reward-program binding"
        );
        assertEq(deployer.coordinator(), address(factory), "specialized deployer is not coordinator-bound");
        assertEq(deployer.bondOwner(), GOVERNANCE_MULTISIG, "specialized deployer has the wrong final owner");
        assertEq(PartnershipBondV3Safe(returnedBond).maxPartyNativePrincipal(), PARTNERSHIP_PARTY_CAP);
        assertEq(PartnershipBondV3Safe(returnedBond).maxBondNativePrincipal(), PARTNERSHIP_BOND_CAP);
        assertEq(PartnershipBondV3Safe(returnedBond).maxAggregateNativeLiability(), PARTNERSHIP_AGGREGATE_CAP);
        assertEq(returnedBond.codehash, factory.expectedPartnershipBondRuntimeCodeHash());

        // The EIP-712 verifying-contract binding remains dynamic, so the
        // reviewed immutable-patched runtime identity is valid for each fresh
        // child rather than only for a precomputed first child address.
        vm.prank(GOVERNANCE_MULTISIG);
        address secondBond = factory.deployPartnership(address(0xD3516), address(0));
        assertTrue(secondBond != returnedBond, "second partnership child address was reused");
        assertEq(secondBond.codehash, factory.expectedPartnershipBondRuntimeCodeHash());
    }

    function test_deployPartnershipRejectsRewardProgramBinding() external {
        _enableDeployments();

        vm.prank(GOVERNANCE_MULTISIG);
        vm.expectRevert(PartnershipBondV3Safe.RewardProgramDisabled.selector);
        factory.deployPartnership(address(0xD3516), address(rewardProgram));
    }

    function test_deployAccountabilityEmitsAndReturnsChildWithFinalOwner() external {
        _enableDeployments();

        AccountabilityBondFactoryV3 deployer = factory.accountabilityFactory();
        address expectedBond = vm.computeCreateAddress(address(deployer), vm.getNonce(address(deployer)));

        vm.expectEmit(true, false, false, false, address(factory));
        emit AccountabilityBondDeployedV3(expectedBond, address(0xD3516));

        vm.prank(GOVERNANCE_MULTISIG);
        address returnedBond = factory.deployAccountability(address(0xD3516));

        assertEq(returnedBond, expectedBond, "returned accountability address differs from emitted deployment");
        assertEq(AccountabilityBondV3Safe(returnedBond).owner(), GOVERNANCE_MULTISIG, "child owner is not governance");
        assertEq(
            address(AccountabilityBondV3Safe(returnedBond).consensus()),
            address(consensus),
            "child consensus is not bound"
        );
        assertEq(
            address(AccountabilityBondV3Safe(returnedBond).disputeRegistry()),
            address(0),
            "retired dispute registry remains bound"
        );
        assertEq(deployer.coordinator(), address(factory), "specialized deployer is not coordinator-bound");
        assertEq(deployer.bondOwner(), GOVERNANCE_MULTISIG, "specialized deployer has the wrong final owner");
        assertEq(AccountabilityBondV3Safe(returnedBond).maxBondNativePrincipal(), ACCOUNTABILITY_BOND_CAP);
        assertEq(AccountabilityBondV3Safe(returnedBond).maxAggregateNativeLiability(), ACCOUNTABILITY_AGGREGATE_CAP);
        assertEq(returnedBond.codehash, factory.expectedAccountabilityBondRuntimeCodeHash());

        vm.prank(GOVERNANCE_MULTISIG);
        address secondBond = factory.deployAccountability(address(0xD3516));
        assertTrue(secondBond != returnedBond, "second accountability child address was reused");
        assertEq(secondBond.codehash, factory.expectedAccountabilityBondRuntimeCodeHash());
    }

    function test_specializedFactoriesRejectDirectDeploymentBypass() external {
        PartnershipBondFactoryV3 partnershipDeployer = factory.partnershipFactory();
        AccountabilityBondFactoryV3 accountabilityDeployer = factory.accountabilityFactory();

        vm.expectRevert(PartnershipBondFactoryV3.NotCoordinator.selector);
        partnershipDeployer.deploy(address(0));

        vm.expectRevert(AccountabilityBondFactoryV3.NotCoordinator.selector);
        accountabilityDeployer.deploy();
    }

    function test_constructorRejectsZeroNativeCaps() external {
        vm.expectRevert(SafeBondFactoryV3.ZeroAddress.selector);
        new SafeBondFactoryV3(
            GOVERNANCE_MULTISIG,
            address(consensus),
            address(disputes),
            address(0),
            0,
            PARTNERSHIP_BOND_CAP,
            PARTNERSHIP_AGGREGATE_CAP,
            ACCOUNTABILITY_BOND_CAP,
            ACCOUNTABILITY_AGGREGATE_CAP,
            bytes32(uint256(1)),
            bytes32(uint256(2)),
            bytes32(uint256(3)),
            bytes32(uint256(4)),
            bytes32(uint256(5))
        );
    }

    function _enableDeployments() private {
        vm.prank(GOVERNANCE_MULTISIG);
        factory.setDeployAllowed(true);
    }

    function _accountabilityInitCode() private view returns (bytes memory) {
        return abi.encodePacked(
            type(AccountabilityBondV3Safe).creationCode,
            abi.encode(
                GOVERNANCE_MULTISIG,
                address(consensus),
                address(0),
                ACCOUNTABILITY_BOND_CAP,
                ACCOUNTABILITY_AGGREGATE_CAP,
                address(0)
            )
        );
    }
}

/// @dev Deliberately exposes every getter SafeBondFactoryV3 checks, but has
///      unrelated runtime code and an arbitrary deploy path.
contract GetterCompatibleCounterfeitPartnershipFactory {
    address public immutable coordinator;
    address public immutable bondOwner;
    address public immutable consensus;
    address public immutable disputeRegistry;
    address public immutable pilotController;
    uint256 public immutable maxPartyNativePrincipal;
    uint256 public immutable maxBondNativePrincipal;
    uint256 public immutable maxAggregateNativeLiability;

    constructor(
        address _coordinator,
        address _bondOwner,
        address _consensus,
        address _disputeRegistry,
        address _pilotController,
        uint256 _maxPartyNativePrincipal,
        uint256 _maxBondNativePrincipal,
        uint256 _maxAggregateNativeLiability
    ) {
        coordinator = _coordinator;
        bondOwner = _bondOwner;
        consensus = _consensus;
        disputeRegistry = _disputeRegistry;
        pilotController = _pilotController;
        maxPartyNativePrincipal = _maxPartyNativePrincipal;
        maxBondNativePrincipal = _maxBondNativePrincipal;
        maxAggregateNativeLiability = _maxAggregateNativeLiability;
    }

    function deploy(address) external pure returns (address) {
        return address(0xBADC0DE);
    }
}
