pragma solidity 0.8.20;

import {Test} from "forge-std/Test.sol";
import "../../src/mocks/SigUtils.sol";
import "@openzeppelin-contracts/token/ERC20/extensions/ERC20Permit.sol";

abstract contract ERC20PermitTest is Test {
    SigUtils sigUtils;
    ERC20Permit permitToken;

    address owner;
    address spender;
    uint256 permitAmount;
    uint256 maxPermitAmount;

    uint256 constant ownerPrivateKey   = 0xA11CE;
    uint256 constant spenderPrivateKey = 0xB0B;

    function initializePermit(
        address token,
        uint256 amount,
        uint256 maxAmount
    ) public {
        permitAmount = amount;
        maxPermitAmount = maxAmount;
        permitToken = ERC20Permit(token);
        sigUtils = new SigUtils(permitToken.DOMAIN_SEPARATOR());

        owner = vm.addr(ownerPrivateKey);
        spender = vm.addr(spenderPrivateKey);
    }

    function test_permit_initialNonceIsZero() public view {
        assertEq(permitToken.nonces(owner), 0);
        assertEq(permitToken.nonces(spender), 0);
    }

    function test_permit_acceptsOwnerSignature() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
            owner: owner,
            spender: spender,
            value: permitAmount,
            nonce: 0,
            deadline: 1 days
        });

        bytes32 digest = sigUtils.getTypedDataHash(permit);

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

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

        assertEq(permitToken.allowance(owner, spender), permitAmount);
        assertEq(permitToken.nonces(owner), 1);
    }

    function test_permit_expiredPermit() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
            owner: owner,
            spender: spender,
            value: permitAmount,
            nonce: permitToken.nonces(owner),
            deadline: 1 days
        });

        bytes32 digest = sigUtils.getTypedDataHash(permit);

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

        vm.warp(1 days + 1 seconds); // fast forward one second past the deadline

        vm.expectRevert();
        permitToken.permit(
            permit.owner,
            permit.spender,
            permit.value,
            permit.deadline,
            v,
            r,
            s
        );
    }

    function test_permit_invalidSigner() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
            owner: owner,
            spender: spender,
            value: permitAmount,
            nonce: permitToken.nonces(owner),
            deadline: 1 days
        });

        bytes32 digest = sigUtils.getTypedDataHash(permit);

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(spenderPrivateKey, digest); // spender signs owner's approval

        vm.expectRevert();
        permitToken.permit(
            permit.owner,
            permit.spender,
            permit.value,
            permit.deadline,
            v,
            r,
            s
        );
    }

    function test_permit_invalidNonce() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
            owner: owner,
            spender: spender,
            value: permitAmount,
            nonce: 1, // owner nonce stored on-chain is 0
            deadline: 1 days
        });

        bytes32 digest = sigUtils.getTypedDataHash(permit);

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

        vm.expectRevert();
        permitToken.permit(
            permit.owner,
            permit.spender,
            permit.value,
            permit.deadline,
            v,
            r,
            s
        );
    }

    function test_permit_signatureReplay() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
            owner: owner,
            spender: spender,
            value: permitAmount,
            nonce: 0,
            deadline: 1 days
        });

        bytes32 digest = sigUtils.getTypedDataHash(permit);

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

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

        vm.expectRevert();
        permitToken.permit(
            permit.owner,
            permit.spender,
            permit.value,
            permit.deadline,
            v,
            r,
            s
        );
    }

    function test_permit_transferFromLimitedPermit() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
            owner: owner,
            spender: spender,
            value: permitAmount,
            nonce: 0,
            deadline: 1 days
        });

        bytes32 digest = sigUtils.getTypedDataHash(permit);

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

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

        vm.prank(spender);
        permitToken.transferFrom(owner, spender, permitAmount);

        assertEq(permitToken.balanceOf(owner), 0);
        assertEq(permitToken.balanceOf(spender), permitAmount);
        assertEq(permitToken.allowance(owner, spender), 0);
    }

    function test_permit_transferFromMaxPermit() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
            owner: owner,
            spender: spender,
            value: maxPermitAmount,
            nonce: 0,
            deadline: 1 days
        });

        bytes32 digest = sigUtils.getTypedDataHash(permit);

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

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

        vm.prank(spender);
        permitToken.transferFrom(owner, spender, permitAmount);

        assertEq(permitToken.balanceOf(owner), 0);
        assertEq(permitToken.balanceOf(spender), permitAmount);
        assertEq(permitToken.allowance(owner, spender), maxPermitAmount);
    }

    function test_permit_invalidAllowance() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
           owner: owner,
           spender: spender,
           value: 1,
           nonce: 0,
           deadline: 1 days
       });

       bytes32 digest = sigUtils.getTypedDataHash(permit);

       (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

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

       vm.expectRevert();
       vm.prank(spender);
       permitToken.transferFrom(owner, spender, permitAmount);
    }

    function test_permit_ivnalidBalance() public {
        _mintPermitTokens(owner, permitAmount);
        SigUtils.Permit memory permit = SigUtils.Permit({
            owner: owner,
            spender: spender,
            value: 2,
            nonce: 0,
            deadline: 1 days
        });

        bytes32 digest = sigUtils.getTypedDataHash(permit);

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, digest);

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

        vm.expectRevert();
        vm.prank(spender);
        permitToken.transferFrom(owner, spender, permitAmount);
    }

    function _mintPermitTokens(address who, uint256 permitAmount) internal virtual;
}
