// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Test} from "forge-std/Test.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {CompliantFiatTokenV2} from "../../contracts/tokens/CompliantFiatTokenV2.sol";

contract CompliantFiatTokenV2Test is Test {
    using ECDSA for bytes32;

    bytes32 private constant PERMIT_TYPEHASH =
        keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
    bytes32 private constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH =
        keccak256(
            "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
        );
    bytes32 private constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH =
        keccak256(
            "ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
        );
    bytes32 private constant CANCEL_AUTHORIZATION_TYPEHASH =
        keccak256("CancelAuthorization(address authorizer,bytes32 nonce)");

    event CompliantOperationDeclared(
        bytes32 indexed operationType,
        address indexed initiator,
        address indexed authorizer,
        address executor,
        address from,
        address to,
        uint256 value,
        bytes32 reasonHash,
        bytes32 accountingRef,
        bytes32 messageCorrelationId,
        bytes32 legalReferenceHash
    );

    CompliantFiatTokenV2 internal token;

    uint256 internal ownerPk;
    uint256 internal adminPk;
    address internal owner;
    address internal admin;
    address internal spender;
    address internal recipient;
    address internal burner;
    address internal governanceExecutor;

    function setUp() public {
        ownerPk = 0xA11CE;
        adminPk = 0xB0B;
        owner = vm.addr(ownerPk);
        admin = vm.addr(adminPk);
        spender = address(0xCAFE);
        recipient = address(0xBEEF);
        burner = address(0xD00D);
        governanceExecutor = address(0x7777);

        token = new CompliantFiatTokenV2(
            "Euro Coin (Compliant V2)",
            "cEURC",
            6,
            "EUR",
            "2",
            owner,
            admin,
            1_000_000 * 10 ** 6,
            true
        );

        bytes32 burnerRole = token.BURNER_ROLE();
        vm.prank(admin);
        token.grantRole(burnerRole, burner);
        vm.prank(admin);
        token.setGovernanceController(governanceExecutor);
    }

    function testPermitWorks() public {
        uint256 value = 25_000 * 10 ** 6;
        uint256 deadline = block.timestamp + 1 days;

        bytes32 structHash = keccak256(
            abi.encode(PERMIT_TYPEHASH, owner, spender, value, token.nonces(owner), deadline)
        );
        bytes32 digest = keccak256(
            abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)
        );
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, digest);

        token.permit(owner, spender, value, deadline, v, r, s);

        assertEq(token.allowance(owner, spender), value);
        assertEq(token.nonces(owner), 1);
    }

    function testTransferWithAuthorizationWorks() public {
        uint256 value = 5_000 * 10 ** 6;
        uint256 validAfter = block.timestamp - 1;
        uint256 validBefore = block.timestamp + 1 days;
        bytes32 nonce = keccak256("auth-1");

        bytes32 structHash = keccak256(
            abi.encode(
                TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
                owner,
                recipient,
                value,
                validAfter,
                validBefore,
                nonce
            )
        );
        bytes32 digest = keccak256(
            abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)
        );
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, digest);

        vm.prank(spender);
        token.transferWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s);

        assertEq(token.balanceOf(recipient), value);
        assertEq(token.balanceOf(owner), 1_000_000 * 10 ** 6 - value);
        assertTrue(token.authorizationState(owner, nonce));
    }

    function testReceiveWithAuthorizationRequiresPayeeCallerAndWorks() public {
        uint256 value = 7_500 * 10 ** 6;
        uint256 validAfter = block.timestamp - 1;
        uint256 validBefore = block.timestamp + 1 days;
        bytes32 nonce = keccak256("receive-auth-1");

        bytes32 structHash = keccak256(
            abi.encode(
                RECEIVE_WITH_AUTHORIZATION_TYPEHASH,
                owner,
                recipient,
                value,
                validAfter,
                validBefore,
                nonce
            )
        );
        bytes32 digest = keccak256(
            abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), structHash)
        );
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, digest);

        vm.prank(spender);
        vm.expectRevert(
            abi.encodeWithSelector(
                CompliantFiatTokenV2.AuthorizationMustBeUsedByPayee.selector,
                recipient,
                spender
            )
        );
        token.receiveWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s);

        vm.prank(recipient);
        token.receiveWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s);

        assertEq(token.balanceOf(recipient), value);
        assertTrue(token.authorizationState(owner, nonce));
    }

    function testCancelAuthorizationBlocksFutureUse() public {
        uint256 value = 1_000 * 10 ** 6;
        uint256 validAfter = block.timestamp - 1;
        uint256 validBefore = block.timestamp + 1 days;
        bytes32 nonce = keccak256("cancel-auth-1");

        bytes32 cancelStructHash = keccak256(
            abi.encode(CANCEL_AUTHORIZATION_TYPEHASH, owner, nonce)
        );
        bytes32 cancelDigest = keccak256(
            abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), cancelStructHash)
        );
        (uint8 cancelV, bytes32 cancelR, bytes32 cancelS) = vm.sign(ownerPk, cancelDigest);

        token.cancelAuthorization(owner, nonce, cancelV, cancelR, cancelS);
        assertTrue(token.authorizationState(owner, nonce));

        bytes32 transferStructHash = keccak256(
            abi.encode(
                TRANSFER_WITH_AUTHORIZATION_TYPEHASH,
                owner,
                recipient,
                value,
                validAfter,
                validBefore,
                nonce
            )
        );
        bytes32 transferDigest = keccak256(
            abi.encodePacked("\x19\x01", token.DOMAIN_SEPARATOR(), transferStructHash)
        );
        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPk, transferDigest);

        vm.prank(spender);
        vm.expectRevert(
            abi.encodeWithSelector(
                CompliantFiatTokenV2.AuthorizationAlreadyUsed.selector,
                owner,
                nonce
            )
        );
        token.transferWithAuthorization(owner, recipient, value, validAfter, validBefore, nonce, v, r, s);
    }

    function testMintAndBurnReasonHashesEmitStructuredEvents() public {
        bytes32 mintReason = keccak256("mint-reason");
        bytes32 burnReason = keccak256("burn-reason");
        uint256 mintAmount = 10_000 * 10 ** 6;

        vm.expectEmit(true, true, true, false);
        emit CompliantOperationDeclared(
            token.OPERATION_MINT(),
            admin,
            admin,
            admin,
            address(0),
            recipient,
            mintAmount,
            mintReason,
            bytes32(0),
            bytes32(0),
            bytes32(0)
        );

        vm.prank(admin);
        token.mint(recipient, mintAmount, mintReason);

        assertEq(token.balanceOf(recipient), mintAmount);

        vm.expectEmit(true, true, true, false);
        emit CompliantOperationDeclared(
            token.OPERATION_BURN(),
            recipient,
            recipient,
            burner,
            recipient,
            address(0),
            mintAmount / 2,
            burnReason,
            bytes32(0),
            bytes32(0),
            bytes32(0)
        );

        vm.prank(burner);
        token.burn(recipient, mintAmount / 2, burnReason);

        assertEq(token.balanceOf(recipient), mintAmount / 2);
    }

    function testPauseIsRoleGated() public {
        vm.prank(spender);
        vm.expectRevert();
        token.pause();

        vm.prank(admin);
        token.pause();
        assertTrue(token.paused());
    }

    function testSupplyControlsEnforceCap() public {
        uint256 configuredCap = token.totalSupply() + (1_000 * 10 ** 6);

        vm.prank(admin);
        token.setSupplyControls(configuredCap, 0, 0);

        vm.prank(admin);
        token.mint(recipient, 1_000 * 10 ** 6, keccak256("under-cap"));

        uint256 attemptedTotalSupply = token.totalSupply() + 1;
        vm.prank(admin);
        vm.expectRevert(
            abi.encodeWithSelector(
                CompliantFiatTokenV2.SupplyCapExceeded.selector,
                configuredCap,
                attemptedTotalSupply
            )
        );
        token.mint(recipient, 1, keccak256("over-cap"));
    }

    function testLegacyAliasAndForwardCanonicalMetadata() public {
        vm.prank(admin);
        vm.expectRevert();
        token.setForwardCanonical(false);

        vm.startPrank(governanceExecutor);
        token.addLegacyAlias("cEURC-legacy");
        token.setForwardCanonical(false);
        token.setSymbolDisplay("cEURC");
        vm.stopPrank();

        string[] memory aliases = token.legacyAliases();
        assertEq(aliases.length, 1);
        assertEq(aliases[0], "cEURC-legacy");
        assertFalse(token.forwardCanonical());
        assertEq(token.symbolDisplay(), "cEURC");
    }

    function testGovernanceAndSupervisionMetadataCanBeManaged() public {
        bytes32 governanceProfileId = keccak256("gov-profile");
        bytes32 supervisionProfileId = keccak256("supervision-profile");
        bytes32 storageNamespace = keccak256("storage-namespace");

        vm.prank(admin);
        vm.expectRevert();
        token.setGovernanceProfileId(governanceProfileId);

        vm.startPrank(governanceExecutor);
        token.setGovernanceProfileId(governanceProfileId);
        token.setSupervisionProfileId(supervisionProfileId);
        token.setStorageNamespace(storageNamespace);
        token.setPrimaryJurisdiction("EU");
        token.setRegulatoryDisclosureURI("ipfs://disclosure");
        token.setReportingURI("ipfs://reporting");
        token.setCanonicalUnderlyingAsset(address(0x1234));
        token.setSupervisionConfiguration(true, true, 14 days);
        vm.stopPrank();

        assertEq(token.governanceProfileId(), governanceProfileId);
        assertEq(token.supervisionProfileId(), supervisionProfileId);
        assertEq(token.storageNamespace(), storageNamespace);
        assertEq(token.primaryJurisdiction(), "EU");
        assertEq(token.regulatoryDisclosureURI(), "ipfs://disclosure");
        assertEq(token.reportingURI(), "ipfs://reporting");
        assertEq(token.canonicalUnderlyingAsset(), address(0x1234));
        assertTrue(token.supervisionRequired());
        assertTrue(token.governmentApprovalRequired());
        assertEq(token.minimumUpgradeNoticePeriod(), 14 days);
        assertFalse(token.wrappedTransport());
    }

    function testEip5267DomainIntrospectionMatchesTokenMetadata() public view {
        (
            bytes1 fields,
            string memory name,
            string memory version,
            uint256 chainId,
            address verifyingContract,
            bytes32 salt,
            uint256[] memory extensions
        ) = token.eip712Domain();

        assertEq(uint8(fields), uint8(0x0f));
        assertEq(name, "Euro Coin (Compliant V2)");
        assertEq(version, "2");
        assertEq(chainId, block.chainid);
        assertEq(verifyingContract, address(token));
        assertEq(salt, bytes32(0));
        assertEq(extensions.length, 0);
    }

    function testEmergencyMetadataOverridesRemainAvailableOutsideGovernance() public {
        vm.prank(admin);
        token.emergencySetPresentationMetadata(true, "ipfs://emergency-token", "cEURC-emergency");

        vm.prank(admin);
        token.emergencySetGovernanceMetadata(
            keccak256("emergency-gov"),
            keccak256("emergency-sup"),
            keccak256("emergency-storage"),
            "Emergency Jurisdiction",
            address(0x9999),
            true,
            true,
            30 days
        );

        vm.prank(admin);
        token.emergencySetDisclosureMetadata("ipfs://emergency-disclosure", "ipfs://emergency-reporting");

        assertTrue(token.forwardCanonical());
        assertEq(token.tokenURI(), "ipfs://emergency-token");
        assertEq(token.symbolDisplay(), "cEURC-emergency");
        assertEq(token.primaryJurisdiction(), "Emergency Jurisdiction");
        assertEq(token.canonicalUnderlyingAsset(), address(0x9999));
        assertEq(token.regulatoryDisclosureURI(), "ipfs://emergency-disclosure");
        assertEq(token.reportingURI(), "ipfs://emergency-reporting");
        assertEq(token.minimumUpgradeNoticePeriod(), 30 days);
    }
}
