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

const {
  shouldBehaveLikeERC20,
  shouldBehaveLikeERC20Transfer,
  shouldBehaveLikeERC20Approve,
} = require('../ERC20.behavior.js');
const { shouldSupportInterfaces } = require('../../../utils/introspection/SupportsInterface.behavior');
const { RevertType } = require('../../../helpers/enums.js');

const name = 'My Token';
const symbol = 'MTKN';
const value = 1000n;
const data = '0x123456';

async function fixture() {
  // this.accounts is used by shouldBehaveLikeERC20
  const accounts = await ethers.getSigners();
  const [holder, other] = accounts;

  const receiver = await ethers.deployContract('ERC1363ReceiverMock');
  const spender = await ethers.deployContract('ERC1363SpenderMock');
  const token = await ethers.deployContract('$ERC1363', [name, symbol]);

  await token.$_mint(holder, value);

  return {
    accounts,
    holder,
    other,
    token,
    receiver,
    spender,
    selectors: {
      onTransferReceived: receiver.interface.getFunction('onTransferReceived(address,address,uint256,bytes)').selector,
      onApprovalReceived: spender.interface.getFunction('onApprovalReceived(address,uint256,bytes)').selector,
    },
  };
}

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

  shouldSupportInterfaces(['ERC165', 'ERC1363']);
  shouldBehaveLikeERC20(value);

  describe('transferAndCall', function () {
    describe('as a transfer', function () {
      beforeEach(async function () {
        this.recipient = this.receiver;
        this.transfer = (holder, ...rest) =>
          this.token.connect(holder).getFunction('transferAndCall(address,uint256)')(...rest);
      });

      shouldBehaveLikeERC20Transfer(value);
    });

    it('reverts transferring to an EOA', async function () {
      await expect(this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.other, value))
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
        .withArgs(this.other.address);
    });

    it('succeeds without data', async function () {
      await expect(
        this.token.connect(this.holder).getFunction('transferAndCall(address,uint256)')(this.receiver, value),
      )
        .to.emit(this.token, 'Transfer')
        .withArgs(this.holder.address, this.receiver.target, value)
        .to.emit(this.receiver, 'Received')
        .withArgs(this.holder.address, this.holder.address, value, '0x');
    });

    it('succeeds with data', async function () {
      await expect(
        this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
          this.receiver,
          value,
          data,
        ),
      )
        .to.emit(this.token, 'Transfer')
        .withArgs(this.holder.address, this.receiver.target, value)
        .to.emit(this.receiver, 'Received')
        .withArgs(this.holder.address, this.holder.address, value, data);
    });

    it('reverts with reverting hook (without reason)', async function () {
      await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage);

      await expect(
        this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
          this.receiver,
          value,
          data,
        ),
      )
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
        .withArgs(this.receiver.target);
    });

    it('reverts with reverting hook (with reason)', async function () {
      await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage);

      await expect(
        this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
          this.receiver,
          value,
          data,
        ),
      ).to.be.revertedWith('ERC1363ReceiverMock: reverting');
    });

    it('reverts with reverting hook (with custom error)', async function () {
      const reason = '0x12345678';
      await this.receiver.setUp(reason, RevertType.RevertWithCustomError);

      await expect(
        this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
          this.receiver,
          value,
          data,
        ),
      )
        .to.be.revertedWithCustomError(this.receiver, 'CustomError')
        .withArgs(reason);
    });

    it('panics with reverting hook (with panic)', async function () {
      await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic);

      await expect(
        this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
          this.receiver,
          value,
          data,
        ),
      ).to.be.revertedWithPanic();
    });

    it('reverts with bad return value', async function () {
      await this.receiver.setUp('0x12345678', RevertType.None);

      await expect(
        this.token.connect(this.holder).getFunction('transferAndCall(address,uint256,bytes)')(
          this.receiver,
          value,
          data,
        ),
      )
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
        .withArgs(this.receiver.target);
    });
  });

  describe('transferFromAndCall', function () {
    beforeEach(async function () {
      await this.token.connect(this.holder).approve(this.other, ethers.MaxUint256);
    });

    describe('as a transfer', function () {
      beforeEach(async function () {
        this.recipient = this.receiver;
        this.transfer = this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)');
      });

      shouldBehaveLikeERC20Transfer(value);
    });

    it('reverts transferring to an EOA', async function () {
      await expect(
        this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')(
          this.holder,
          this.other,
          value,
        ),
      )
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
        .withArgs(this.other.address);
    });

    it('succeeds without data', async function () {
      await expect(
        this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256)')(
          this.holder,
          this.receiver,
          value,
        ),
      )
        .to.emit(this.token, 'Transfer')
        .withArgs(this.holder.address, this.receiver.target, value)
        .to.emit(this.receiver, 'Received')
        .withArgs(this.other.address, this.holder.address, value, '0x');
    });

    it('succeeds with data', async function () {
      await expect(
        this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
          this.holder,
          this.receiver,
          value,
          data,
        ),
      )
        .to.emit(this.token, 'Transfer')
        .withArgs(this.holder.address, this.receiver.target, value)
        .to.emit(this.receiver, 'Received')
        .withArgs(this.other.address, this.holder.address, value, data);
    });

    it('reverts with reverting hook (without reason)', async function () {
      await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithoutMessage);

      await expect(
        this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
          this.holder,
          this.receiver,
          value,
          data,
        ),
      )
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
        .withArgs(this.receiver.target);
    });

    it('reverts with reverting hook (with reason)', async function () {
      await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.RevertWithMessage);

      await expect(
        this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
          this.holder,
          this.receiver,
          value,
          data,
        ),
      ).to.be.revertedWith('ERC1363ReceiverMock: reverting');
    });

    it('reverts with reverting hook (with custom error)', async function () {
      const reason = '0x12345678';
      await this.receiver.setUp(reason, RevertType.RevertWithCustomError);

      await expect(
        this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
          this.holder,
          this.receiver,
          value,
          data,
        ),
      )
        .to.be.revertedWithCustomError(this.receiver, 'CustomError')
        .withArgs(reason);
    });

    it('panics with reverting hook (with panic)', async function () {
      await this.receiver.setUp(this.selectors.onTransferReceived, RevertType.Panic);

      await expect(
        this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
          this.holder,
          this.receiver,
          value,
          data,
        ),
      ).to.be.revertedWithPanic();
    });

    it('reverts with bad return value', async function () {
      await this.receiver.setUp('0x12345678', RevertType.None);

      await expect(
        this.token.connect(this.other).getFunction('transferFromAndCall(address,address,uint256,bytes)')(
          this.holder,
          this.receiver,
          value,
          data,
        ),
      )
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidReceiver')
        .withArgs(this.receiver.target);
    });
  });

  describe('approveAndCall', function () {
    describe('as an approval', function () {
      beforeEach(async function () {
        this.recipient = this.spender;
        this.approve = (holder, ...rest) =>
          this.token.connect(holder).getFunction('approveAndCall(address,uint256)')(...rest);
      });

      shouldBehaveLikeERC20Approve(value);
    });

    it('reverts approving an EOA', async function () {
      await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.other, value))
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender')
        .withArgs(this.other.address);
    });

    it('succeeds without data', async function () {
      await expect(this.token.connect(this.holder).getFunction('approveAndCall(address,uint256)')(this.spender, value))
        .to.emit(this.token, 'Approval')
        .withArgs(this.holder.address, this.spender.target, value)
        .to.emit(this.spender, 'Approved')
        .withArgs(this.holder.address, value, '0x');
    });

    it('succeeds with data', async function () {
      await expect(
        this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
      )
        .to.emit(this.token, 'Approval')
        .withArgs(this.holder.address, this.spender.target, value)
        .to.emit(this.spender, 'Approved')
        .withArgs(this.holder.address, value, data);
    });

    it('with reverting hook (without reason)', async function () {
      await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithoutMessage);

      await expect(
        this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
      )
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender')
        .withArgs(this.spender.target);
    });

    it('reverts with reverting hook (with reason)', async function () {
      await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.RevertWithMessage);

      await expect(
        this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
      ).to.be.revertedWith('ERC1363SpenderMock: reverting');
    });

    it('reverts with reverting hook (with custom error)', async function () {
      const reason = '0x12345678';
      await this.spender.setUp(reason, RevertType.RevertWithCustomError);

      await expect(
        this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
      )
        .to.be.revertedWithCustomError(this.spender, 'CustomError')
        .withArgs(reason);
    });

    it('panics with reverting hook (with panic)', async function () {
      await this.spender.setUp(this.selectors.onApprovalReceived, RevertType.Panic);

      await expect(
        this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
      ).to.be.revertedWithPanic();
    });

    it('reverts with bad return value', async function () {
      await this.spender.setUp('0x12345678', RevertType.None);

      await expect(
        this.token.connect(this.holder).getFunction('approveAndCall(address,uint256,bytes)')(this.spender, value, data),
      )
        .to.be.revertedWithCustomError(this.token, 'ERC1363InvalidSpender')
        .withArgs(this.spender.target);
    });
  });
});
