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

import "forge-std/Test.sol";
import { IERC8004IdentityRegistry } from "../../contracts/identity/IERC8004IdentityRegistry.sol";
import { ERC8004IdentityAdapterV3 } from "../../contracts/identity/ERC8004IdentityAdapterV3.sol";
import { CanonicalIdentityRegistryV3 } from "../../contracts/identity/CanonicalIdentityRegistryV3.sol";
import { MandateRegistryV3 } from "../../contracts/mandate/MandateRegistryV3.sol";
import { MandateExecutionAdapterV3 } from "../../contracts/mandate/MandateExecutionAdapterV3.sol";

contract MockIdentityRegistry is IERC8004IdentityRegistry {
    mapping(uint256 agentId => Agent agent) internal _agents;
    mapping(uint256 agentId => mapping(address operator => bool allowed)) internal _authorized;

    function setAgent(Agent calldata agent) external {
        _agents[agent.agentId] = agent;
    }

    function setAuthorizedOperator(uint256 agentId, address operator, bool allowed) external {
        _authorized[agentId][operator] = allowed;
    }

    function getAgent(uint256 agentId) external view returns (Agent memory) {
        return _agents[agentId];
    }

    function isAuthorizedOperator(uint256 agentId, address caller) external view returns (bool) {
        return _authorized[agentId][caller];
    }

    function isActive(uint256 agentId) external view returns (bool) {
        return _agents[agentId].active;
    }
}

contract MockERC1271Wallet {
    bytes4 internal constant MAGIC = 0x1626ba7e;
    bytes32 public approvedDigest;

    function approveDigest(bytes32 digest) external {
        approvedDigest = digest;
    }

    function registerIdentity(
        CanonicalIdentityRegistryV3 identities,
        uint256 agentId,
        address agentWallet,
        address operator,
        address beneficiary
    ) external {
        identities.register(agentId, agentWallet, operator, beneficiary);
    }

    function isValidSignature(bytes32 digest, bytes calldata) external view returns (bytes4) {
        return digest == approvedDigest ? MAGIC : bytes4(0xffffffff);
    }
}

contract MandateTargetV3Mock {
    uint256 public calls;
    uint256 public totalValue;

    function createBond() external payable {
        ++calls;
        totalValue += msg.value;
    }
}

/// @dev Deliberately minimal module. Production modules must authenticate their external caller
///      before forwarding that caller as `operator` to the registry.
contract MandateModuleV3Mock {
    function consume(
        MandateRegistryV3 registry,
        MandateRegistryV3.Mandate memory mandate,
        bytes32 action,
        address target,
        bytes4 selector,
        uint256 amount,
        address operator
    ) external returns (bytes32) {
        return registry.consume(mandate, action, target, selector, amount, operator);
    }
}

contract MandateRegistryV3Test is Test {
    uint256 internal constant PRINCIPAL_KEY = 0xA11CE;
    address internal principal = vm.addr(PRINCIPAL_KEY);
    address internal operator = address(0x0F3A7001);
    address internal beneficiary = address(0xBEEF1C1A);
    address internal owner = address(0xA0A0);
    address internal attacker = address(0xBAD);
    address internal target;
    bytes32 internal constant ACTION = keccak256("createBond");
    bytes4 internal constant SELECTOR = bytes4(keccak256("createBond()"));

    CanonicalIdentityRegistryV3 internal identities;
    ERC8004IdentityAdapterV3 internal identityAdapter;
    MandateRegistryV3 internal registry;
    MandateModuleV3Mock internal module;
    MandateExecutionAdapterV3 internal adapter;
    MandateTargetV3Mock internal mandateTarget;

    event ModuleAuthorizationUpdatedV3(address indexed module, bool allowed, address indexed updatedBy);

    function setUp() public {
        identities = new CanonicalIdentityRegistryV3();
        identityAdapter = new ERC8004IdentityAdapterV3(address(this), address(identities), address(identities).codehash);
        registry = new MandateRegistryV3(owner, address(identityAdapter), address(identityAdapter).codehash);
        module = new MandateModuleV3Mock();
        adapter = new MandateExecutionAdapterV3(address(registry));
        mandateTarget = new MandateTargetV3Mock();
        target = address(mandateTarget);
        _setIdentity(principal, operator, beneficiary);
    }

    function test_registersEOAMandateAndSnapshotsIdentityMetadata() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(1);
        bytes32 mandateHash = _register(mandate, PRINCIPAL_KEY);

        MandateRegistryV3.MandateState memory stored = registry.state(mandateHash);
        assertTrue(stored.registered);
        assertFalse(stored.revoked);
        assertEq(stored.principal, principal);
        assertEq(stored.operator, operator);
        assertEq(stored.beneficiary, beneficiary);
        assertEq(stored.identityRegistry, address(identityAdapter));
        assertEq(stored.identityRegistryChainId, block.chainid);
        assertEq(stored.principalAgentId, 1);
        assertEq(stored.operatorAgentId, 2);
        assertEq(stored.nonce, 1);
        assertEq(stored.notBefore, mandate.notBefore);
        assertEq(stored.notAfter, mandate.notAfter);
        assertEq(registry.mandateForNonce(principal, 1), mandateHash);
        assertTrue(registry.isActive(mandateHash));
    }

    function test_registersERC1271PrincipalMandate() public {
        MockERC1271Wallet wallet = new MockERC1271Wallet();
        wallet.registerIdentity(identities, 3, address(wallet), operator, beneficiary);
        wallet.registerIdentity(identities, 4, operator, operator, beneficiary);

        MandateRegistryV3.Mandate memory mandate = _mandate(2);
        mandate.principal = address(wallet);
        mandate.principalAgentId = 3;
        mandate.operatorAgentId = 4;
        wallet.approveDigest(_digest(mandate));

        bytes32 mandateHash = registry.registerMandate(mandate, hex"1271");
        assertEq(registry.state(mandateHash).principal, address(wallet));
    }

    function test_arbitraryRevocationAttackIsRejected() public {
        bytes32 mandateHash = _register(_mandate(3), PRINCIPAL_KEY);

        vm.expectRevert(MandateRegistryV3.NotPrincipal.selector);
        vm.prank(attacker);
        registry.revokeMandate(mandateHash, principal);

        vm.expectRevert(abi.encodeWithSelector(MandateRegistryV3.PrincipalMismatch.selector, principal, attacker));
        vm.prank(attacker);
        registry.revokeMandate(mandateHash, attacker);

        assertFalse(registry.state(mandateHash).revoked);
    }

    function test_principalAndOwnerCanRevoke() public {
        bytes32 principalMandateHash = _register(_mandate(4), PRINCIPAL_KEY);
        vm.prank(principal);
        registry.revokeMandate(principalMandateHash);
        assertTrue(registry.state(principalMandateHash).revoked);

        bytes32 ownerMandateHash = _register(_mandate(5), PRINCIPAL_KEY);
        vm.prank(owner);
        registry.revokeMandate(ownerMandateHash);
        assertTrue(registry.state(ownerMandateHash).revoked);
    }

    function test_unauthorizedDirectConsumeCannotGriefMandate() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(6);
        _register(mandate, PRINCIPAL_KEY);

        vm.expectRevert(MandateRegistryV3.LegacyConsumeDisabled.selector);
        vm.prank(attacker);
        registry.consume(mandate, ACTION, target, SELECTOR, 1, operator);
    }

    function test_allowlistedModuleConsumesForBoundOperatorOnly() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(7);
        bytes32 mandateHash = _register(mandate, PRINCIPAL_KEY);
        _allowModule(true);

        bytes32 result = _execute(mandate, 40, operator);
        assertEq(result, mandateHash);
        assertEq(registry.state(mandateHash).spentSoFar, 40);

        vm.expectRevert(MandateRegistryV3.UnauthorizedCaller.selector);
        _execute(mandate, 1, attacker);
    }

    function test_expiredMandateCannotBeConsumedAndIsNotActive() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(8);
        mandate.notAfter = uint64(block.timestamp + 1 hours);
        bytes32 mandateHash = _register(mandate, PRINCIPAL_KEY);
        _allowModule(true);

        vm.warp(mandate.notAfter);
        assertFalse(registry.isActive(mandateHash));
        vm.expectRevert(MandateRegistryV3.Expired.selector);
        _execute(mandate, 1, operator);
    }

    function test_enforcesSpendCapAndHourlyRateLimit() public {
        MandateRegistryV3.Mandate memory capMandate = _mandate(9);
        capMandate.spendCap = 100;
        capMandate.allowedActions[0] = _tuple(capMandate);
        _register(capMandate, PRINCIPAL_KEY);
        _allowModule(true);
        _execute(capMandate, 60, operator);
        vm.expectRevert(abi.encodeWithSelector(MandateRegistryV3.SpendCapExceeded.selector, 41, 40));
        _execute(capMandate, 41, operator);

        MandateRegistryV3.Mandate memory rateMandate = _mandate(10);
        rateMandate.rateLimitPerHour = 1;
        _register(rateMandate, PRINCIPAL_KEY);
        _execute(rateMandate, 0, operator);
        vm.expectRevert(MandateRegistryV3.RateLimitExceeded.selector);
        _execute(rateMandate, 0, operator);
    }

    function test_acceptsArraysAtExplicitMaximums() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(11);
        mandate.allowedActions = _actions(registry.MAX_ALLOWED_ACTIONS());
        mandate.prohibitedActions = _actions(registry.MAX_PROHIBITED_ACTIONS());
        mandate.allowedContracts = _contracts(registry.MAX_ALLOWED_CONTRACTS());
        mandate.allowedSelectors = _selectors(registry.MAX_ALLOWED_SELECTORS());
        _register(mandate, PRINCIPAL_KEY);
    }

    function test_rejectsAllowedActionsAboveMaximum() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(12);
        mandate.allowedActions = _actions(registry.MAX_ALLOWED_ACTIONS() + 1);
        vm.expectRevert(
            abi.encodeWithSelector(
                MandateRegistryV3.TooManyAllowedActions.selector,
                registry.MAX_ALLOWED_ACTIONS() + 1,
                registry.MAX_ALLOWED_ACTIONS()
            )
        );
        registry.registerMandate(mandate, "");
    }

    function test_rejectsProhibitedActionsAboveMaximum() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(13);
        mandate.prohibitedActions = _actions(registry.MAX_PROHIBITED_ACTIONS() + 1);
        vm.expectRevert(
            abi.encodeWithSelector(
                MandateRegistryV3.TooManyProhibitedActions.selector,
                registry.MAX_PROHIBITED_ACTIONS() + 1,
                registry.MAX_PROHIBITED_ACTIONS()
            )
        );
        registry.registerMandate(mandate, "");
    }

    function test_rejectsAllowedContractsAboveMaximum() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(14);
        mandate.allowedContracts = _contracts(registry.MAX_ALLOWED_CONTRACTS() + 1);
        vm.expectRevert(
            abi.encodeWithSelector(
                MandateRegistryV3.TooManyAllowedContracts.selector,
                registry.MAX_ALLOWED_CONTRACTS() + 1,
                registry.MAX_ALLOWED_CONTRACTS()
            )
        );
        registry.registerMandate(mandate, "");
    }

    function test_rejectsAllowedSelectorsAboveMaximum() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(15);
        mandate.allowedSelectors = _selectors(registry.MAX_ALLOWED_SELECTORS() + 1);
        vm.expectRevert(
            abi.encodeWithSelector(
                MandateRegistryV3.TooManyAllowedSelectors.selector,
                registry.MAX_ALLOWED_SELECTORS() + 1,
                registry.MAX_ALLOWED_SELECTORS()
            )
        );
        registry.registerMandate(mandate, "");
    }

    function test_emptyScopesAndZeroCapsFailClosed() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(23);
        mandate.allowedContracts = new address[](0);
        vm.expectRevert(
            abi.encodeWithSelector(
                MandateRegistryV3.TooManyAllowedContracts.selector, uint256(0), registry.MAX_ALLOWED_CONTRACTS()
            )
        );
        registry.registerMandate(mandate, "");

        mandate = _mandate(24);
        mandate.spendCap = 0;
        vm.expectRevert(MandateRegistryV3.InvalidScope.selector);
        registry.registerMandate(mandate, "");
    }

    function test_replayAndNonceReuseAreRejectedButNewNonceWorks() public {
        MandateRegistryV3.Mandate memory first = _mandate(16);
        bytes32 firstMandateHash = _register(first, PRINCIPAL_KEY);

        bytes memory firstSig = _sign(first, PRINCIPAL_KEY);
        vm.expectRevert(MandateRegistryV3.AlreadyRegistered.selector);
        registry.registerMandate(first, firstSig);

        MandateRegistryV3.Mandate memory sameNonceDifferentMandate = _mandate(16);
        sameNonceDifferentMandate.spendCap = 1;
        bytes memory sameNonceSig = _sign(sameNonceDifferentMandate, PRINCIPAL_KEY);
        vm.expectRevert(
            abi.encodeWithSelector(MandateRegistryV3.NonceAlreadyUsed.selector, principal, uint96(16), firstMandateHash)
        );
        registry.registerMandate(sameNonceDifferentMandate, sameNonceSig);

        MandateRegistryV3.Mandate memory newNonce = _mandate(17);
        _register(newNonce, PRINCIPAL_KEY);
    }

    function test_rejectsWrongProtocolTimeWindowAndIdentityBinding() public {
        MandateRegistryV3.Mandate memory badVersion = _mandate(18);
        badVersion.protocolVersion = "2.0.0";
        vm.expectRevert(MandateRegistryV3.UnsupportedProtocolVersion.selector);
        registry.registerMandate(badVersion, "");

        MandateRegistryV3.Mandate memory badTime = _mandate(19);
        badTime.notAfter = badTime.notBefore;
        vm.expectRevert(MandateRegistryV3.InvalidTimeWindow.selector);
        registry.registerMandate(badTime, "");

        MandateRegistryV3.Mandate memory badIdentity = _mandate(20);
        badIdentity.identityRegistryChainId = block.chainid + 1;
        vm.expectRevert(MandateRegistryV3.InvalidIdentityRegistry.selector);
        registry.registerMandate(badIdentity, "");

        MandateRegistryV3.Mandate memory mismatchedOperator = _mandate(21);
        mismatchedOperator.operator = attacker;
        vm.expectRevert(MandateRegistryV3.InvalidPrincipalIdentity.selector);
        registry.registerMandate(mismatchedOperator, "");
    }

    function test_onlyOwnerMayManageModuleAllowlist() public {
        vm.expectRevert(MandateRegistryV3.NotOwner.selector);
        registry.setModuleAuthorization(address(adapter), true);

        vm.prank(owner);
        vm.expectEmit(true, false, true, true);
        emit ModuleAuthorizationUpdatedV3(address(adapter), true, owner);
        registry.setModuleAuthorization(address(adapter), true);
        assertTrue(registry.allowedModules(address(adapter)));

        vm.prank(owner);
        vm.expectRevert(abi.encodeWithSelector(MandateRegistryV3.InvalidExecutionAdapter.selector, address(module)));
        registry.setModuleAuthorization(address(module), true);
    }

    function test_executionAdapterRejectsResourceBindingMismatchBeforeTargetCall() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(22);
        bytes32 mandateHash = _register(mandate, PRINCIPAL_KEY);
        _allowModule(true);

        MandateExecutionAdapterV3.Execution memory execution = MandateExecutionAdapterV3.Execution({
            action: ACTION,
            resource: keccak256("different-resource"),
            target: target,
            amount: 1,
            data: abi.encodeWithSelector(SELECTOR)
        });
        vm.deal(operator, 1);
        vm.expectRevert(MandateRegistryV3.BadExecutionBinding.selector);
        vm.prank(operator);
        adapter.execute{ value: 1 }(abi.encode(mandate), execution);

        assertEq(registry.state(mandateHash).spentSoFar, 0);
        assertEq(mandateTarget.calls(), 0);
    }

    function test_signedTupleRejectsCrossCombinationAndTrailingCalldata() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(25);
        _register(mandate, PRINCIPAL_KEY);
        _allowModule(true);

        MandateExecutionAdapterV3.Execution memory trailing = MandateExecutionAdapterV3.Execution({
            action: ACTION,
            resource: ACTION,
            target: target,
            amount: 0,
            data: abi.encodePacked(abi.encodeWithSelector(SELECTOR), bytes32(uint256(1)))
        });
        vm.expectRevert(abi.encodeWithSelector(MandateRegistryV3.ActionNotAllowed.selector, ACTION));
        vm.prank(operator);
        adapter.execute(abi.encode(mandate), trailing);

        MandateTargetV3Mock other = new MandateTargetV3Mock();
        mandate.allowedContracts = new address[](2);
        mandate.allowedContracts[0] = address(other) < target ? address(other) : target;
        mandate.allowedContracts[1] = address(other) < target ? target : address(other);
        mandate.nonce = 26;
        _register(mandate, PRINCIPAL_KEY);
        MandateExecutionAdapterV3.Execution memory crossed = MandateExecutionAdapterV3.Execution({
            action: ACTION, resource: ACTION, target: address(other), amount: 0, data: abi.encodeWithSelector(SELECTOR)
        });
        vm.expectRevert(abi.encodeWithSelector(MandateRegistryV3.ActionNotAllowed.selector, ACTION));
        vm.prank(operator);
        adapter.execute(abi.encode(mandate), crossed);
    }

    function test_unpinnedIdentityRegistryCannotAuthorizeMandate() public {
        MandateRegistryV3.Mandate memory mandate = _mandate(27);
        mandate.identityRegistry = address(identities);
        vm.expectRevert(MandateRegistryV3.InvalidIdentityRegistry.selector);
        registry.registerMandate(mandate, "");
    }

    function _allowModule(bool allowed) internal {
        vm.prank(owner);
        registry.setModuleAuthorization(address(adapter), allowed);
    }

    function _execute(MandateRegistryV3.Mandate memory mandate, uint256 amount, address caller)
        internal
        returns (bytes32)
    {
        MandateExecutionAdapterV3.Execution memory execution = MandateExecutionAdapterV3.Execution({
            action: ACTION, resource: ACTION, target: target, amount: amount, data: abi.encodeWithSelector(SELECTOR)
        });
        vm.deal(caller, amount);
        vm.prank(caller);
        (bytes32 mandateHash,) = adapter.execute{ value: amount }(abi.encode(mandate), execution);
        return mandateHash;
    }

    function _setIdentity(address principal_, address operator_, address beneficiary_) internal {
        vm.startPrank(principal_);
        identities.register(1, principal_, operator_, beneficiary_);
        identities.register(2, operator_, operator_, beneficiary_);
        vm.stopPrank();
    }

    function _mandate(uint96 nonce) internal view returns (MandateRegistryV3.Mandate memory mandate) {
        mandate.identityRegistryChainId = block.chainid;
        mandate.identityRegistry = address(identityAdapter);
        mandate.principalAgentId = 1;
        mandate.principal = principal;
        mandate.operatorAgentId = 2;
        mandate.operator = operator;
        mandate.beneficiary = beneficiary;
        mandate.allowedActions = _actions(1);
        mandate.prohibitedActions = new bytes32[](0);
        mandate.allowedContracts = new address[](1);
        mandate.allowedContracts[0] = target;
        mandate.allowedSelectors = new bytes4[](1);
        mandate.allowedSelectors[0] = SELECTOR;
        mandate.spendAsset = address(0);
        mandate.spendCap = 1_000_000 ether;
        mandate.rateLimitPerHour = 100;
        mandate.evidencePolicyId = bytes32(0);
        mandate.disputePolicyId = bytes32(0);
        mandate.notBefore = uint64(block.timestamp);
        mandate.notAfter = uint64(block.timestamp + 7 days);
        mandate.nonce = nonce;
        mandate.chainId = block.chainid;
        mandate.protocolVersion = "3.0.0";
        mandate.allowedActions[0] = _tuple(mandate);
    }

    function _tuple(MandateRegistryV3.Mandate memory mandate) internal view returns (bytes32) {
        return keccak256(
            abi.encode(ACTION, target, SELECTOR, mandate.spendCap, keccak256(abi.encodeWithSelector(SELECTOR)))
        );
    }

    function _register(MandateRegistryV3.Mandate memory mandate, uint256 signerKey) internal returns (bytes32) {
        return registry.registerMandate(mandate, _sign(mandate, signerKey));
    }

    function _digest(MandateRegistryV3.Mandate memory mandate) internal view returns (bytes32) {
        return keccak256(abi.encodePacked("\x19\x01", registry.DOMAIN_SEPARATOR(), registry.hashMandate(mandate)));
    }

    function _sign(MandateRegistryV3.Mandate memory mandate, uint256 signerKey) internal view returns (bytes memory) {
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerKey, _digest(mandate));
        return abi.encodePacked(r, s, v);
    }

    function _actions(uint256 length) internal pure returns (bytes32[] memory values) {
        values = new bytes32[](length);
        for (uint256 i; i < length; ++i) {
            values[i] = bytes32(i + 1);
        }
    }

    function _contracts(uint256 length) internal pure returns (address[] memory values) {
        values = new address[](length);
        for (uint256 i; i < length; ++i) {
            // The loop is bounded to MAX_ALLOWED_CONTRACTS + 1 (33), so this cast is safe.
            // forge-lint: disable-next-line(unsafe-typecast)
            values[i] = address(uint160(i + 1));
        }
    }

    function _selectors(uint256 length) internal pure returns (bytes4[] memory values) {
        values = new bytes4[](length);
        for (uint256 i; i < length; ++i) {
            // The loop is bounded to MAX_ALLOWED_SELECTORS + 1 (33), so this cast is safe.
            // forge-lint: disable-next-line(unsafe-typecast)
            values[i] = bytes4(uint32(i + 1));
        }
    }
}
