const { ethers } = require('hardhat');
const { expect } = require('chai');
const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers');

const { getDomain, domainSeparator, Permit } = require('../../../helpers/eip712');
const time = require('../../../helpers/time');

const name = 'My Token';
const symbol = 'MTKN';
const initialSupply = 100n;

async function fixture() {
  const [holder, spender, owner, other] = await ethers.getSigners();

  const token = await ethers.deployContract('$ERC20Permit', [name, symbol, name]);
  await token.$_mint(holder, initialSupply);

  return {
    holder,
    spender,
    owner,
    other,
    token,
  };
}

describe('ERC20Permit', function () {
  beforeEach(async function () {
    Object.assign(this, await loadFixture(fixture));
  });

  it('initial nonce is 0', async function () {
    expect(await this.token.nonces(this.holder)).to.equal(0n);
  });

  it('domain separator', async function () {
    expect(await this.token.DOMAIN_SEPARATOR()).to.equal(await getDomain(this.token).then(domainSeparator));
  });

  describe('permit', function () {
    const value = 42n;
    const nonce = 0n;
    const maxDeadline = ethers.MaxUint256;

    beforeEach(function () {
      this.buildData = (contract, deadline = maxDeadline) =>
        getDomain(contract).then(domain => ({
          domain,
          types: { Permit },
          message: {
            owner: this.owner.address,
            spender: this.spender.address,
            value,
            nonce,
            deadline,
          },
        }));
    });

    it('accepts owner signature', async function () {
      const { v, r, s } = await this.buildData(this.token)
        .then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message))
        .then(ethers.Signature.from);

      await this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s);

      expect(await this.token.nonces(this.owner)).to.equal(1n);
      expect(await this.token.allowance(this.owner, this.spender)).to.equal(value);
    });

    it('rejects reused signature', async function () {
      const { v, r, s, serialized } = await this.buildData(this.token)
        .then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message))
        .then(ethers.Signature.from);

      await this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s);

      const recovered = await this.buildData(this.token).then(({ domain, types, message }) =>
        ethers.verifyTypedData(domain, types, { ...message, nonce: nonce + 1n, deadline: maxDeadline }, serialized),
      );

      await expect(this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s))
        .to.be.revertedWithCustomError(this.token, 'ERC2612InvalidSigner')
        .withArgs(recovered, this.owner);
    });

    it('rejects other signature', async function () {
      const { v, r, s } = await this.buildData(this.token)
        .then(({ domain, types, message }) => this.other.signTypedData(domain, types, message))
        .then(ethers.Signature.from);

      await expect(this.token.permit(this.owner, this.spender, value, maxDeadline, v, r, s))
        .to.be.revertedWithCustomError(this.token, 'ERC2612InvalidSigner')
        .withArgs(this.other, this.owner);
    });

    it('rejects expired permit', async function () {
      const deadline = (await time.clock.timestamp()) - time.duration.weeks(1);

      const { v, r, s } = await this.buildData(this.token, deadline)
        .then(({ domain, types, message }) => this.owner.signTypedData(domain, types, message))
        .then(ethers.Signature.from);

      await expect(this.token.permit(this.owner, this.spender, value, deadline, v, r, s))
        .to.be.revertedWithCustomError(this.token, 'ERC2612ExpiredSignature')
        .withArgs(deadline);
    });
  });
});
