import "@nomicfoundation/hardhat-chai-matchers";
import {
  loadFixture,
  mine,
} from "@nomicfoundation/hardhat-network-helpers";
import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import {
  BigNumber,
  BytesLike,
  ContractReceipt,
  ContractTransaction,
  Wallet,
} from "ethers";
import {
  ethers,
  network,
  /* tracer */
} from "hardhat";

import {
  MockToken,
  Multicall,
  P2PIX,
  Reputation,
} from "../src/types";
import { P2PixErrors } from "./utils/errors";
import {
  Call,
  Lock,
  Result,
  getBnFrom,
  getLockData,
  getSignerAddrs,
  p2pixFixture,
  randomSigners,
} from "./utils/fixtures";

describe("P2PIX", () => {
  type WalletWithAddress = Wallet & SignerWithAddress;

  // contract deployer/admin
  let owner: WalletWithAddress;

  // extra EOAs
  let acc01: WalletWithAddress;
  let acc02: WalletWithAddress;
  let acc03: WalletWithAddress;

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let res: any;

  let p2pix: P2PIX; // Contract instance
  let erc20: MockToken; // Token instance
  let reputation: Reputation; // Reputation Interface instance
  let multicall: Multicall; // Multicall contract instance
  let merkleRoot: string; // MerkleRoot from seller's allowlist
  let proof: string[]; // Owner's proof as whitelisted address

  const fundAmount: BigNumber =
    ethers.utils.parseEther("10000");
  const price: BigNumber = ethers.utils.parseEther("100");

  const zero = ethers.constants.AddressZero;

  before("Set signers and reset network", async () => {
    [owner, acc01, acc02, acc03] =
      await // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (ethers as any).getSigners();

    await network.provider.send("hardhat_reset");
  });
  beforeEach("Load deployment fixtures", async () => {
    ({
      erc20,
      p2pix,
      reputation,
      multicall,
      merkleRoot,
      proof,
    } = await loadFixture(p2pixFixture));
  });

  describe("Init", async () => {
    it("P2PIX, Reputation and ERC20 should initialize", async () => {
      // tracer.enabled = true;
      await p2pix.deployed();
      // tracer.enabled = false;
      await erc20.deployed();
      await reputation.deployed();
      expect(p2pix).to.be.ok;
      expect(erc20).to.be.ok;
      expect(reputation).to.be.ok;
      const ownerKey = await p2pix._castAddrToKey(
        owner.address,
      );
      const acc01Key = await p2pix._castAddrToKey(
        acc01.address,
      );

      // storage checks
      expect(
        await p2pix.callStatic.defaultLockBlocks(),
      ).to.eq(10);
      expect(await p2pix.callStatic.reputation()).to.eq(
        reputation.address,
      );
      expect(await p2pix.callStatic.lockCounter()).to.eq(0);
      expect(
        await p2pix.callStatic.validBacenSigners(ownerKey),
      ).to.eq(true);
      expect(
        await p2pix.callStatic.validBacenSigners(acc01Key),
      ).to.eq(true);
      expect(
        await p2pix.callStatic.allowedERC20s(erc20.address),
      ).to.eq(true);

      // event emission
      await expect(await p2pix.deployTransaction)
        .to.emit(p2pix, "OwnerUpdated")
        .withArgs(zero, owner.address)
        .and.to.emit(p2pix, "LockBlocksUpdated")
        .withArgs(10)
        .and.to.emit(p2pix, "ReputationUpdated")
        .withArgs(reputation.address)
        .and.to.emit(p2pix, "ValidSignersUpdated")
        .withArgs([owner.address, acc01.address])
        .and.to.emit(p2pix, "AllowedERC20Updated")
        .withArgs(erc20.address, true);
    });

    it("accounts have been funded", async () => {
      // can't be eq to fundAmount due to contract deployment cost
      res = await ethers.provider.getBalance(owner.address);
      expect(res.toString()).to.have.lengthOf(22);
      // console.log(res); // lengthOf = 22
      // console.log(fundAmount); // lengthOf = 23

      // those should eq to hardhat prefunded account's value
      expect(
        await ethers.provider.getBalance(acc01.address),
      ).to.eq(fundAmount);
      expect(
        await ethers.provider.getBalance(acc02.address),
      ).to.eq(fundAmount);
      expect(
        await ethers.provider.getBalance(acc03.address),
      ).to.eq(fundAmount);
    });
  });

  // each describe tests a set of functionalities of the contract's behavior
  describe("Owner Functions", async () => {
    it("should allow owner to withdraw contract's balance", async () => {
      const oldBal = await p2pix.provider.getBalance(
        p2pix.address,
      );
      // this call also tests p2pix's receive() fallback mechanism.
      const tx1 = await acc01.sendTransaction({
        to: p2pix.address,
        value: price,
      });
      const newBal = await p2pix.provider.getBalance(
        p2pix.address,
      );

      expect(tx1).to.be.ok;
      expect(oldBal).to.eq(0);
      expect(newBal).to.eq(price);

      await expect(p2pix.withdrawBalance())
        .to.changeEtherBalances(
          [owner.address, p2pix.address],
          [price, "-100000000000000000000"],
        )
        .and.to.emit(p2pix, "FundsWithdrawn")
        .withArgs(owner.address, price);

      await expect(
        p2pix.connect(acc01).withdrawBalance(),
      ).to.be.revertedWith(P2PixErrors.UNAUTHORIZED);
    });
    it("should allow owner to change reputation instance", async () => {
      const tx = await p2pix.setReputation(acc03.address);
      const newRep = await p2pix.callStatic.reputation();
      const fail = p2pix
        .connect(acc02)
        .setReputation(owner.address);

      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "ReputationUpdated")
        .withArgs(acc03.address);
      expect(newRep).to.eq(acc03.address);
      await expect(fail).to.be.revertedWith(
        P2PixErrors.UNAUTHORIZED,
      );
    });
    it("should allow owner to change defaultLockBlocks ", async () => {
      const magicVal = 1337;
      const tx = await p2pix.setDefaultLockBlocks(magicVal);
      const newVal =
        await p2pix.callStatic.defaultLockBlocks();
      const fail = p2pix
        .connect(acc02)
        .setDefaultLockBlocks(0);

      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "LockBlocksUpdated")
        .withArgs(magicVal);
      expect(newVal).to.eq(magicVal);
      await expect(fail).to.be.revertedWith(
        P2PixErrors.UNAUTHORIZED,
      );
    });
    it("should allow owner to add valid Bacen signers", async () => {
      const newSigners = randomSigners(2);
      const bob = await newSigners[0].getAddress();
      const alice = await newSigners[1].getAddress();
      const bobCasted = await p2pix._castAddrToKey(bob);
      const aliceCasted = await p2pix._castAddrToKey(alice);
      const tx = await p2pix.setValidSigners([bob, alice]);
      const newSigner1 =
        await p2pix.callStatic.validBacenSigners(bobCasted);
      const newSigner2 =
        await p2pix.callStatic.validBacenSigners(aliceCasted);
      const fail = p2pix
        .connect(acc03)
        .setValidSigners([owner.address, acc02.address]);

      expect(tx).to.be.ok;
      expect(newSigner1).to.eq(true);
      expect(newSigner2).to.eq(true);
      await expect(tx)
        .to.emit(p2pix, "ValidSignersUpdated")
        .withArgs([bob, alice]);
      await expect(fail).to.be.revertedWith(
        P2PixErrors.UNAUTHORIZED,
      );
    });
    it("should allow owner to adjust tokenSettings", async () => {
      const tx = await p2pix.tokenSettings(
        [erc20.address, owner.address],
        [false, true],
      );
      const newTokenState1 =
        await p2pix.callStatic.allowedERC20s(erc20.address);
      const newTokenState2 =
        await p2pix.callStatic.allowedERC20s(owner.address);
      const funcSig = "0xd6e8b973";
      const args = ethers.utils.defaultAbiCoder.encode(
        ["address[]", "bool[]"],
        [[acc01.address], [false]],
      );
      const cd = funcSig + args.substring(2);
      const callStruct: Call = {
        target: p2pix.address,
        callData: cd,
      };
      const fail = p2pix
        .connect(acc01)
        .tokenSettings([acc01.address], [false]);
      const fail2 = p2pix.tokenSettings([], [true, false]);
      const fail3 = p2pix.tokenSettings([zero], [true, true]);
      const mtcFail = multicall.mtc1([callStruct]);

      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "AllowedERC20Updated")
        .withArgs(erc20.address, false)
        .and.to.emit(p2pix, "AllowedERC20Updated")
        .withArgs(owner.address, true);
      expect(newTokenState1).to.eq(false);
      expect(newTokenState2).to.eq(true);
      await expect(fail).to.be.revertedWith(
        P2PixErrors.UNAUTHORIZED,
      );
      await expect(fail).to.be.revertedWith(
        P2PixErrors.UNAUTHORIZED,
      );
      await expect(fail2).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.NoTokens,
      );
      await expect(fail3).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.LengthMismatch,
      );
      await expect(mtcFail)
        .to.be.revertedWithCustomError(
          multicall,
          P2PixErrors.CallFailed,
        )
        .withArgs(P2PixErrors.UNAUTHORIZED);
    });
  });
  describe("Deposit", async () => {
    it("should revert if ERC20 is not allowed", async () => {
      const pTarget = ethers.BigNumber.from(7331);
      const root = ethers.utils.keccak256(
        ethers.utils.toUtf8Bytes("root"),
      );
      const tx = p2pix.deposit(
        owner.address,
        1,
        pTarget,
        true,
        root,
      );

      await expect(tx).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.TokenDenied,
      );
    });
    it("should revert if pixTarget is empty", async () => {
      const root = ethers.utils.keccak256(
        ethers.utils.toUtf8Bytes("root"),
      );
      const tx = p2pix.deposit(
        erc20.address,
        1,
        0,
        true,
        root,
      );

      await expect(tx).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.EmptyPixTarget,
      );
    });
    it("should revert if amount exceeds the balance limit", async () => {
      const pTarget = ethers.BigNumber.from(7331);
      const root = ethers.utils.keccak256(
        ethers.utils.toUtf8Bytes("root"),
      );
      const tx = p2pix.deposit(
        erc20.address,
        ethers.utils.parseEther("100000001"),
        pTarget,
        true,
        root,
      );

      await expect(tx).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.MaxBalExceeded,
      );
    });
    it("should create deposit, update storage and emit event", async () => {
      const pTarget = ethers.BigNumber.from(7331);
      // we use `hashZero` to avoid updating seller's allowlist settings
      const root = ethers.constants.HashZero;
      await erc20.approve(p2pix.address, price);
      const tx = await p2pix.deposit(
        erc20.address,
        price,
        pTarget,
        true,
        root,
      );
      const storage = await p2pix.callStatic.getBalance(
        owner.address,
        erc20.address,
      );
      const pixTarget = await p2pix.callStatic.getPixTarget(
        owner.address,
        erc20.address,
      );
      const valid = await p2pix.callStatic.getValid(
        owner.address,
        erc20.address,
      );
      const ownerKey = await p2pix.callStatic._castAddrToKey(
        owner.address,
      );
      const allowList = await p2pix.sellerAllowList(ownerKey);
      const balances = await p2pix.callStatic.getBalances(
        [owner.address, acc01.address],
        erc20.address,
      );

      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "DepositAdded")
        .withArgs(owner.address, erc20.address, price);
      await expect(tx).to.changeTokenBalances(
        erc20,
        [owner.address, p2pix.address],
        ["-100000000000000000000", price],
      );
      expect(storage).to.eq(price);
      expect(pixTarget).to.eq(pTarget);
      expect(valid).to.eq(true);
      expect(allowList).to.eq(root);
      expect(balances[0]).to.eq(price);
      expect(balances[1]).to.eq(zero);
    });
    // edge case test
    it("should create multiple deposits", async () => {
      const ownerKey = await p2pix.callStatic._castAddrToKey(
        owner.address,
      );
      const acc01Key = await p2pix.callStatic._castAddrToKey(
        acc01.address,
      );
      const acc02Key = await p2pix.callStatic._castAddrToKey(
        acc02.address,
      );
      const acc03Key = await p2pix.callStatic._castAddrToKey(
        acc03.address,
      );

      const pTarget = ethers.BigNumber.from(7331);
      const pTarget2 = ethers.BigNumber.from(1337);
      const pTarget3 = ethers.BigNumber.from(3731);
      // we mock the allowlist root here only to test storage update. In depth
      // allowlist test coverage in both "Lock" and "Allowlist Settings" unit tests.
      const root = ethers.utils.keccak256(
        ethers.utils.toUtf8Bytes("root"),
      );
      const nullRoot = ethers.constants.HashZero;
      const price2 = price.mul(ethers.BigNumber.from(2));
      const price3 = price.mul(ethers.BigNumber.from(3));
      const price4 = price.mul(ethers.BigNumber.from(4));
      const prices: BigNumber[] = [
        price,
        price2,
        price3,
        price4,
      ];
      await erc20.mint(
        getSignerAddrs(4, await ethers.getSigners()),
        price4,
      );
      await erc20
        .connect(owner)
        .approve(p2pix.address, price);
      await erc20
        .connect(acc01)
        .approve(p2pix.address, price2);
      await erc20
        .connect(acc02)
        .approve(p2pix.address, price3);
      await erc20
        .connect(acc03)
        .approve(p2pix.address, price4);

      const tx = await p2pix
        .connect(owner)
        .deposit(erc20.address, price, pTarget, true, root);
      const tx2 = await p2pix
        .connect(acc01)
        .deposit(
          erc20.address,
          price2,
          pTarget2,
          false,
          nullRoot,
        );
      const tx3 = await p2pix
        .connect(acc02)
        .deposit(erc20.address, price3, pTarget3, true, root);
      const tx4 = await p2pix
        .connect(acc03)
        .deposit(
          erc20.address,
          price4,
          pTarget,
          false,
          nullRoot,
        );

      const balances = await p2pix.callStatic.getBalances(
        [
          owner.address,
          acc01.address,
          acc02.address,
          acc03.address,
        ],
        erc20.address,
      );

      const storage1 = await p2pix.callStatic.getBalance(
        owner.address,
        erc20.address,
      );
      const storage2 = await p2pix.callStatic.getBalance(
        acc01.address,
        erc20.address,
      );
      const storage3 = await p2pix.callStatic.getBalance(
        acc02.address,
        erc20.address,
      );
      const storage4 = await p2pix.callStatic.getBalance(
        acc03.address,
        erc20.address,
      );

      const pixTarget1 = await p2pix.callStatic.getPixTarget(
        owner.address,
        erc20.address,
      );
      const pixTarget2 = await p2pix.callStatic.getPixTarget(
        acc01.address,
        erc20.address,
      );
      const pixTarget3 = await p2pix.callStatic.getPixTarget(
        acc02.address,
        erc20.address,
      );
      const pixTarget4 = await p2pix.callStatic.getPixTarget(
        acc03.address,
        erc20.address,
      );

      const valid1 = await p2pix.callStatic.getValid(
        owner.address,
        erc20.address,
      );
      const valid2 = await p2pix.callStatic.getValid(
        acc01.address,
        erc20.address,
      );
      const valid3 = await p2pix.callStatic.getValid(
        acc02.address,
        erc20.address,
      );
      const valid4 = await p2pix.callStatic.getValid(
        acc03.address,
        erc20.address,
      );

      const allowList1 = await p2pix.sellerAllowList(
        ownerKey,
      );
      const allowList2 = await p2pix.sellerAllowList(
        acc01Key,
      );
      const allowList3 = await p2pix.sellerAllowList(
        acc02Key,
      );
      const allowList4 = await p2pix.sellerAllowList(
        acc03Key,
      );

      expect(tx).to.be.ok;
      expect(tx2).to.be.ok;
      expect(tx3).to.be.ok;
      expect(tx4).to.be.ok;

      const transactions = [tx, tx2, tx3, tx4];
      const addresses = [
        owner.address,
        acc01.address,
        acc02.address,
        acc03.address,
      ];
      const depositPrices = [price, price2, price3, price4];

      for (let i = 0; i < transactions.length; i++) {
        const tx = transactions[i];
        const addr = addresses[i];
        const depositPrice = depositPrices[i];
        const amount = `-${(i + 1) * 100000000000000000000}`;

        await expect(tx)
          .to.emit(p2pix, "DepositAdded")
          .withArgs(addr, erc20.address, depositPrice);
        await expect(tx).to.changeTokenBalances(
          erc20,
          [addr, p2pix.address],
          [amount, depositPrice],
        );
      }

      expect(prices[0]).to.eq(balances[0]);
      expect(prices[1]).to.eq(balances[1]);
      expect(prices[2]).to.eq(balances[2]);
      expect(prices[3]).to.eq(balances[3]);

      expect(storage1).to.eq(price);
      expect(pixTarget1).to.eq(pTarget);
      expect(valid1).to.eq(true);
      expect(allowList1).to.eq(root);

      expect(storage2).to.eq(price2);
      expect(pixTarget2).to.eq(pTarget2);
      expect(valid2).to.eq(false);
      expect(allowList2).to.eq(nullRoot);

      expect(storage3).to.eq(price3);
      expect(pixTarget3).to.eq(pTarget3);
      expect(valid3).to.eq(true);
      expect(allowList3).to.eq(root);

      expect(storage4).to.eq(price4);
      expect(pixTarget4).to.eq(pTarget);
      expect(valid4).to.eq(false);
      expect(allowList4).to.eq(nullRoot);
    });
  });
  describe("Lock", async () => {
    it("should revert if deposit is invalid", async () => {
      const pTarget = ethers.BigNumber.from(7331);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        pTarget,
        true,
        ethers.constants.HashZero,
      );
      await p2pix.setValidState(erc20.address, false);
      const fail = p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          price,
          [],
          [],
        );
      const fail2 = p2pix.lock(
        zero,
        zero,
        zero,
        zero,
        0,
        price,
        [],
        [],
      );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.InvalidDeposit,
      );
      await expect(fail2).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.InvalidDeposit,
      );
    });
    it("should revert if wished amount is greater than balance's remaining amount", async () => {
      await erc20.approve(p2pix.address, price);
      const pTarget = ethers.BigNumber.from(1337);
      await p2pix.deposit(
        erc20.address,
        price,
        pTarget,
        true,
        ethers.constants.HashZero,
      );
      const fail = p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          price.mul(ethers.BigNumber.from(2)),
          [],
          [],
        );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.NotEnoughTokens,
      );
    });

    it("should revert if an invalid allowlist merkleproof is provided", async () => {
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        ethers.BigNumber.from("1337"),
        true,
        merkleRoot,
      );
      const fail = p2pix
        .connect(acc02)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          1000,
          [
            ethers.utils.keccak256(
              ethers.utils.toUtf8Bytes("wrong"),
            ),
          ],
          [],
        );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.AddressDenied,
      );
    });
    it("should revert if msg.sender does not have enough credit in his spend limit", async () => {
      await erc20.approve(
        p2pix.address,
        price.mul(BigNumber.from("3")),
      );
      await p2pix.deposit(
        erc20.address,
        price.mul(BigNumber.from("3")),
        ethers.BigNumber.from("1"),
        true,
        merkleRoot,
      );
      const fail = p2pix
        .connect(acc02)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          price.mul(BigNumber.from("2")),
          [],
          [],
        );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.AmountNotAllowed,
      );
    });
    it("should create a lock, update storage and emit events via the allowlist path", async () => {
      const target = ethers.BigNumber.from(101);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      const tx = await p2pix
        .connect(acc01)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          price,
          proof,
          [],
        );
      const storage: Lock = await p2pix.callStatic.mapLocks(
        1,
      );

      const rc: ContractReceipt = await tx.wait();
      const expiration = rc.blockNumber + 10;
      const key = await p2pix.callStatic._castAddrToKey(
        owner.address,
      );

      await expect(tx)
        .to.emit(p2pix, "LockAdded")
        .withArgs(
          acc02.address,
          ethers.constants.One,
          key,
          price,
        );
      expect(tx).to.be.ok;
      expect(storage.sellerKey).to.eq(key);
      expect(storage.counter).to.eq(1);
      expect(storage.relayerPremium).to.eq(
        ethers.constants.Zero,
      );
      expect(storage.amount).to.eq(price);
      expect(storage.expirationBlock).to.eq(expiration);
      expect(storage.pixTarget).to.eq(target);
      expect(storage.buyerAddress).to.eq(acc02.address);
      expect(storage.relayerTarget).to.eq(acc03.address);
      expect(storage.relayerAddress).to.eq(acc01.address);
      expect(storage.token).to.eq(erc20.address);
    });
    it("should create a lock, update storage and emit events via the reputation path 1", async () => {
      const root = ethers.constants.HashZero;
      const target = ethers.BigNumber.from(101);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        root,
      );
      const tx = await p2pix
        .connect(acc01)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          price,
          [],
          [],
        );
      const storage: Lock = await p2pix.callStatic.mapLocks(
        1,
      );

      const rc: ContractReceipt = await tx.wait();
      const expiration = rc.blockNumber + 10;
      const key = await p2pix.callStatic._castAddrToKey(
        owner.address,
      );
      const castBack = await p2pix.callStatic._castKeyToAddr(
        key,
      );

      expect(tx).to.be.ok;
      expect(castBack).to.eq(owner.address);
      expect(storage.sellerKey).to.eq(key);
      expect(storage.counter).to.eq(1);
      expect(storage.relayerPremium).to.eq(
        ethers.constants.Zero,
      );
      expect(storage.amount).to.eq(price);
      expect(storage.expirationBlock).to.eq(expiration);
      expect(storage.pixTarget).to.eq(target);
      expect(storage.buyerAddress).to.eq(acc02.address);
      expect(storage.relayerTarget).to.eq(acc03.address);
      expect(storage.relayerAddress).to.eq(acc01.address);
      expect(storage.token).to.eq(erc20.address);

      await expect(tx)
        .to.emit(p2pix, "LockAdded")
        .withArgs(acc02.address, 1, key, storage.amount);
    });
    it("should create a lock, update storage and emit events via the reputation path 2", async () => {
      const root = ethers.constants.HashZero;
      const newPrice = price
        .mul(ethers.constants.Two)
        .add(ethers.constants.One);
      const endtoendID = ethers.constants.HashZero;
      const target = ethers.BigNumber.from(101);
      const messageToSign = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [target, price, endtoendID],
      );
      const messageHashBytes =
        ethers.utils.arrayify(messageToSign);
      const flatSig = await acc01.signMessage(
        messageHashBytes,
      );
      const sig = ethers.utils.splitSignature(flatSig);
      await erc20.approve(p2pix.address, newPrice);
      await p2pix.deposit(
        erc20.address,
        newPrice,
        target,
        true,
        root,
      );
      await p2pix
        .connect(acc01)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          price,
          price,
          [],
          [],
        );
      await p2pix
        .connect(acc01)
        .release(
          1,
          acc02.address,
          endtoendID,
          sig.r,
          sig.s,
          sig.v,
        );
      const tx = await p2pix
        .connect(acc01)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          price.add(ethers.constants.One),
          [],
          [],
        );
      const storage: Lock = await p2pix.callStatic.mapLocks(
        2,
      );

      const rc: ContractReceipt = await tx.wait();
      const expiration = rc.blockNumber + 10;
      const key = await p2pix.callStatic._castAddrToKey(
        owner.address,
      );
      const castBack = await p2pix.callStatic._castKeyToAddr(
        key,
      );

      expect(tx).to.be.ok;
      expect(castBack).to.eq(owner.address);
      expect(storage.sellerKey).to.eq(key);
      expect(storage.counter).to.eq(2);
      expect(storage.relayerPremium).to.eq(
        ethers.constants.Zero,
      );
      expect(storage.amount).to.eq(
        price.add(ethers.constants.One),
      );
      expect(storage.expirationBlock).to.eq(expiration);
      expect(storage.pixTarget).to.eq(target);
      expect(storage.buyerAddress).to.eq(acc02.address);
      expect(storage.relayerTarget).to.eq(acc03.address);
      expect(storage.relayerAddress).to.eq(acc01.address);
      expect(storage.token).to.eq(erc20.address);

      await expect(tx)
        .to.emit(p2pix, "LockAdded")
        .withArgs(acc02.address, 2, key, storage.amount);
    });
    // edge case test
    it("should create multiple locks", async () => {
      const newPrice = price.div(ethers.BigNumber.from(2));
      const target = ethers.BigNumber.from(101);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      const tx1 = await p2pix
        .connect(acc01)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          newPrice,
          proof,
          [],
        );
      const storage1: Lock = await p2pix.callStatic.mapLocks(
        1,
      );

      const rc1: ContractReceipt = await tx1.wait();
      const expiration1 = rc1.blockNumber + 10;

      const tx2 = await p2pix
        .connect(acc01)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          100,
          [],
          [],
        );
      const storage2: Lock = await p2pix.callStatic.mapLocks(
        2,
      );

      const rc2: ContractReceipt = await tx2.wait();
      const expiration2 = rc2.blockNumber + 10;

      const tx3 = await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc03.address,
          acc03.address,
          0,
          100,
          [],
          [],
        );
      const storage3: Lock = await p2pix.callStatic.mapLocks(
        3,
      );

      const rc3: ContractReceipt = await tx3.wait();
      const expiration3 = rc3.blockNumber + 10;

      const key = await p2pix.callStatic._castAddrToKey(
        owner.address,
      );

      // const lockStatus1 = await p2pix.callStatic.getLocksStatus([1,7,7,2,3,4,5,5,2,3]);
      // const lockStatus2 = await p2pix.callStatic.getLocksStatus([0,1,2,3]);
      // const lockStatus3 = await p2pix.callStatic.getLocksStatus([7,7,333,14,777]);
      // const lockStatus4 = await p2pix.callStatic.getLocksStatus([]);

      // All getLocksStatus calls were batched via the Multicall contract.
      const ls1: [BigNumber[], BigNumber[]] = [
        getBnFrom([1, 7, 7, 2, 3, 4, 5, 5, 2, 3]),
        getBnFrom([1, 0, 0, 1, 1, 0, 0, 0, 1, 1]),
      ];

      const ls2: [BigNumber[], BigNumber[]] = [
        getBnFrom([0, 1, 2, 3]),
        getBnFrom([0, 1, 1, 1]),
      ];

      const ls3: [BigNumber[], BigNumber[]] = [
        getBnFrom([7, 7, 333, 14, 777]),
        getBnFrom([0, 0, 0, 0, 0]),
      ];

      const ls4 = [[], []];

      const batchedLocks: Array<BigNumber[]> = [
        ls1,
        ls2,
        ls3,
        ls4,
      ].map(arr => arr[0]);

      const cData: Call[] = getLockData(
        p2pix.address,
        batchedLocks,
      );

      const batchCall = await multicall.callStatic.mtc1(
        cData,
      );
      const blockNumber = batchCall[0];

      const result: Array<BytesLike> = batchCall[1].slice(
        0,
        4,
      );

      const decodedData = result.map(r =>
        ethers.utils.defaultAbiCoder.decode(
          ["uint256[]", "uint8[]"],
          r,
        ),
      );

      const [ls1Res, ls2Res, ls3Res, ls4Res] = decodedData;

      expect(tx1).to.be.ok;
      expect(tx2).to.be.ok;
      expect(tx3).to.be.ok;

      expect(blockNumber).to.eq(9);
      expect(ls1Res).to.deep.equal(ls1);
      expect(ls2Res).to.deep.equal(ls2);
      expect(ls3Res).to.deep.equal(ls3);
      expect(ls4Res).to.deep.equal(ls4);

      expect(key)
        .to.eq(storage1.sellerKey)
        .and.to.eq(storage2.sellerKey)
        .and.to.eq(storage3.sellerKey);

      expect(storage1.counter).to.eq(1);
      expect(storage2.counter).to.eq(2);
      expect(storage3.counter).to.eq(3);

      expect(ethers.constants.Zero)
        .to.eq(storage1.relayerPremium)
        .and.to.eq(storage2.relayerPremium)
        .and.to.eq(storage3.relayerPremium);

      expect(storage1.amount).to.eq(newPrice);
      expect(ethers.BigNumber.from(100))
        .to.eq(storage2.amount)
        .and.to.eq(storage3.amount);

      expect(storage1.expirationBlock).to.eq(expiration1);
      expect(storage2.expirationBlock).to.eq(expiration2);
      expect(storage3.expirationBlock).to.eq(expiration3);

      expect(target)
        .to.eq(storage1.pixTarget)
        .and.to.eq(storage2.pixTarget)
        .and.to.eq(storage3.pixTarget);

      expect(acc02.address)
        .to.eq(storage1.buyerAddress)
        .and.to.eq(storage2.buyerAddress);
      expect(storage3.buyerAddress).to.eq(acc03.address);

      expect(acc03.address)
        .to.eq(storage1.relayerTarget)
        .and.to.eq(storage2.relayerTarget)
        .and.to.eq(storage3.relayerTarget);

      expect(acc01.address)
        .to.eq(storage1.relayerAddress)
        .and.to.eq(storage2.relayerAddress);
      expect(storage3.relayerAddress).to.eq(acc03.address);

      expect(erc20.address)
        .to.eq(storage1.token)
        .and.to.eq(storage2.token)
        .and.to.eq(storage3.token);

      await expect(tx1)
        .to.emit(p2pix, "LockAdded")
        .withArgs(acc02.address, 1, key, storage1.amount);
      await expect(tx2)
        .to.emit(p2pix, "LockAdded")
        .withArgs(acc02.address, 2, key, storage2.amount);
      await expect(tx3)
        .to.emit(p2pix, "LockAdded")
        .withArgs(acc03.address, 3, key, storage3.amount);
    });
  });
  describe("Set sellerBalance Valid State", async () => {
    it("should revert if sellerBalance hasn't been initialized", async () => {
      const fail = p2pix.setValidState(erc20.address, false);

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.NotInitialized,
      );
    });
    it("should setValidState, update storage and emit events", async () => {
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        ethers.BigNumber.from(10101),
        true,
        merkleRoot,
      );
      const state1 = await p2pix.callStatic.getValid(
        owner.address,
        erc20.address,
      );
      const tx = await p2pix.setValidState(
        erc20.address,
        false,
      );
      const state2 = await p2pix.callStatic.getValid(
        owner.address,
        erc20.address,
      );

      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "ValidSet")
        .withArgs(owner.address, erc20.address, false);
      expect(state1).to.be.true;
      expect(state2).to.be.false;
    });
    it("should cancel multiple balances", async () => {
      const hashZero = ethers.constants.HashZero;
      await erc20.mint([acc01.address, acc02.address], price);
      const target = ethers.BigNumber.from("1");
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        hashZero,
      );
      await erc20
        .connect(acc01)
        .approve(p2pix.address, price);
      await p2pix
        .connect(acc01)
        .deposit(
          erc20.address,
          price,
          target,
          false,
          hashZero,
        );
      await erc20
        .connect(acc02)
        .approve(p2pix.address, price);
      await p2pix
        .connect(acc02)
        .deposit(
          erc20.address,
          price,
          target,
          true,
          hashZero,
        );
      const oldState1 = await p2pix.callStatic.getValid(
        owner.address,
        erc20.address,
      );
      const oldState2 = await p2pix.callStatic.getValid(
        acc01.address,
        erc20.address,
      );
      const oldState3 = await p2pix.callStatic.getValid(
        acc02.address,
        erc20.address,
      );
      const tx1 = await p2pix.setValidState(
        erc20.address,
        false,
      );
      const tx2 = await p2pix
        .connect(acc01)
        .setValidState(erc20.address, true);
      const tx3 = await p2pix
        .connect(acc02)
        .setValidState(erc20.address, true);
      const newState1 = await p2pix.callStatic.getValid(
        owner.address,
        erc20.address,
      );
      const newState2 = await p2pix.callStatic.getValid(
        acc01.address,
        erc20.address,
      );
      const newState3 = await p2pix.callStatic.getValid(
        acc02.address,
        erc20.address,
      );

      expect(tx1).to.be.ok;
      expect(tx2).to.be.ok;
      expect(tx3).to.be.ok;
      await expect(tx1)
        .to.emit(p2pix, "ValidSet")
        .withArgs(owner.address, erc20.address, false);
      await expect(tx2)
        .to.emit(p2pix, "ValidSet")
        .withArgs(acc01.address, erc20.address, true);
      await expect(tx3)
        .to.emit(p2pix, "ValidSet")
        .withArgs(acc02.address, erc20.address, true);
      expect(oldState1).to.be.true;
      expect(oldState2).to.be.false;
      expect(oldState3).to.be.true;
      expect(newState1).to.be.false;
      expect(newState2).to.be.true;
      expect(newState3).to.be.true;
    });
  });
  describe("Release", async () => {
    it("should revert if lock has expired", async () => {
      const target = ethers.BigNumber.from(101);
      const messageToSign = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [target, 100, ethers.constants.HashZero],
      );
      const flatSig = await acc01.signMessage(
        ethers.utils.arrayify(messageToSign),
      );
      const sig = ethers.utils.splitSignature(flatSig);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          6,
          100,
          [],
          [],
        );
      const lockID = ethers.constants.One;
      await mine(13);
      const fail = p2pix.release(
        lockID,
        acc03.address,
        ethers.constants.HashZero,
        sig.r,
        sig.s,
        sig.v,
      );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.LockExpired,
      );
    });
    it("should revert if lock has already been released", async () => {
      const target = ethers.BigNumber.from("1");
      const hashZero = ethers.constants.HashZero;
      const messageToSign = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [target, 100, hashZero],
      );
      const flatSig = await acc01.signMessage(
        ethers.utils.arrayify(messageToSign),
      );
      const sig = ethers.utils.splitSignature(flatSig);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          6,
          100,
          [],
          [],
        );
      const lockID = ethers.constants.One;
      await p2pix.release(
        lockID,
        acc03.address,
        ethers.constants.HashZero,
        sig.r,
        sig.s,
        sig.v,
      );
      const fail = p2pix.release(
        lockID,
        acc03.address,
        ethers.constants.HashZero,
        sig.r,
        sig.s,
        sig.v,
      );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.AlreadyReleased,
      );
    });
    it("should revert if signed message has already been used", async () => {
      const target = ethers.BigNumber.from(101);
      const messageToSign = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [target, 100, ethers.constants.HashZero],
      );
      const flatSig = await owner.signMessage(
        ethers.utils.arrayify(messageToSign),
      );
      const sig = ethers.utils.splitSignature(flatSig);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        ethers.constants.HashZero,
      );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          6,
          100,
          [],
          [],
        );

      await p2pix
        .connect(acc01)
        .release(
          1,
          acc02.address,
          ethers.constants.HashZero,
          sig.r,
          sig.s,
          sig.v,
        );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          6,
          100,
          [],
          [],
        );
      const fail = p2pix
        .connect(acc01)
        .release(
          2,
          acc02.address,
          ethers.constants.HashZero,
          sig.r,
          sig.s,
          sig.v,
        );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.TxAlreadyUsed,
      );
    });
    it("should revert if ecrecovered signer is invalid", async () => {
      const target = ethers.BigNumber.from(101);
      const messageToSign = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [target, 100, ethers.constants.HashZero],
      );
      const flatSig = await acc03.signMessage(
        ethers.utils.arrayify(messageToSign),
      );
      const sig = ethers.utils.splitSignature(flatSig);

      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        ethers.constants.HashZero,
      );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          6,
          100,
          [],
          [],
        );
      const fail = p2pix
        .connect(acc01)
        .release(
          1,
          acc02.address,
          ethers.constants.HashZero,
          sig.r,
          sig.s,
          sig.v,
        );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.InvalidSigner,
      );
    });
    it("should release lock, update storage and emit events", async () => {
      const zero = ethers.constants.Zero;
      const endtoendID = ethers.constants.HashZero;
      const pixTarget = ethers.BigNumber.from(101);
      const messageToSign = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [pixTarget, 100, endtoendID],
      );
      // Note: messageToSign is a string, that is 66-bytes long, to sign the
      //       binary value, we must convert it to the 32 byte Array that
      //       the string represents
      //
      // i.e.,
      //    66-byte string
      //   "0x592fa743889fc7f92ac2a37bb1f5ba1daf2a5c84741ca0e0061d243a2e6707ba"
      //   ... vs ...
      //   32 entry Uint8Array
      //  [ 89, 47, 167, 67, 136, 159, ... 103, 7, 186]
      const messageHashBytes =
        ethers.utils.arrayify(messageToSign);
      const flatSig = await acc01.signMessage(
        messageHashBytes,
      );
      const sig = ethers.utils.splitSignature(flatSig);
      const root = ethers.constants.HashZero;

      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        pixTarget,
        true,
        root,
      );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          6,
          100,
          [],
          [],
        );
      const acc01Key = await p2pix.callStatic._castAddrToKey(
        acc01.address,
      );
      const acc03Key = await p2pix.callStatic._castAddrToKey(
        acc03.address,
      );
      const userRecordA = await p2pix.callStatic.userRecord(
        acc01Key,
      );
      const userRecord1 = await p2pix.callStatic.userRecord(
        acc03Key,
      );
      const storage1: Lock = await p2pix.callStatic.mapLocks(
        1,
      );
      const tx = await p2pix
        .connect(acc01)
        .release(
          1,
          acc02.address,
          endtoendID,
          sig.r,
          sig.s,
          sig.v,
        );

      const lockStatus1 =
        await p2pix.callStatic.getLocksStatus([1]);
      const ls1: [BigNumber[], number[]] = [
        [ethers.constants.One],
        [3],
      ];
      const funcSig = "0xd6e8b973";
      const args = ethers.utils.defaultAbiCoder.encode(
        ["address[]", "bool[]"],
        [[acc01.address], [false]],
      );
      const cd1 = funcSig + args.substring(2);
      const cd2: Call[] = getLockData(p2pix.address, [
        ls1[0],
      ]);
      const mtcCalls = [
        { target: p2pix.address, callData: cd1 },
      ];
      mtcCalls.push(cd2[0]);
      const mtc2 = await multicall.callStatic.mtc2(mtcCalls);
      const blockNumber: BigNumber = mtc2[0];
      const blockhash: BytesLike = mtc2[1];
      const result = mtc2.slice(2).flat(1) as Result[];
      const res1: BytesLike[] = [result[1].returnData];
      const decodedLockData = res1.map(r =>
        ethers.utils.defaultAbiCoder.decode(
          ["uint256[]", "uint8[]"],
          r,
        ),
      );

      const storage2: Lock = await p2pix.callStatic.mapLocks(
        1,
      );
      const userRecordB = await p2pix.callStatic.userRecord(
        acc01Key,
      );
      const userRecord2 = await p2pix.callStatic.userRecord(
        acc03Key,
      );
      const used = await p2pix.callStatic.usedTransactions(
        messageHashBytes,
      );
      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "LockReleased")
        .withArgs(
          acc02.address,
          ethers.constants.One,
          storage1.amount,
        );
      expect(storage1.expirationBlock).to.eq(
        ethers.BigNumber.from(17),
      );
      expect(storage1.amount).to.eq(
        ethers.BigNumber.from(100),
      );
      expect(lockStatus1[0].toString()).to.equal(
        ls1[0].toString(),
      );
      expect(lockStatus1[1].toString()).to.equal(
        ls1[1].toString(),
      );
      expect(blockNumber).to.eq(8);
      expect(blockhash).to.deep.equal(
        ethers.constants.HashZero,
      );
      expect(result[0].success).to.eq(false);
      expect(result[1].success).to.eq(true);
      expect(decodedLockData.flat(1)).to.deep.eq(ls1);
      expect(storage2.expirationBlock).to.eq(zero);
      expect(storage2.amount).to.eq(zero);
      expect(used).to.eq(true);
      expect(userRecordA).to.eq(zero);
      expect(userRecord1).to.eq(zero);
      expect(userRecordB).to.eq(ethers.BigNumber.from(6));
      expect(userRecord2).to.eq(ethers.BigNumber.from(100));
      await expect(tx).to.changeTokenBalances(
        erc20,
        [acc03.address, acc02.address],
        [3, 97],
        // acc02 is acting both as buyer and relayerTarget
        // (i.e., 94 + 3 = 97)
      );
    });
    // edge case test
    it("should release multiple locks", async () => {
      const endtoendID = ethers.constants.HashZero;
      const pixTarget = ethers.BigNumber.from(101);
      const root = ethers.constants.HashZero;
      const acc01Key = await p2pix.callStatic._castAddrToKey(
        acc01.address,
      );
      const acc03Key = await p2pix.callStatic._castAddrToKey(
        acc03.address,
      );
      const acc01Record1 = await p2pix.callStatic.userRecord(
        acc01Key,
      );
      const acc03Record1 = await p2pix.callStatic.userRecord(
        acc03Key,
      );
      const messageToSign1 = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [pixTarget, 100, endtoendID],
      );
      const flatSig1 = await owner.signMessage(
        ethers.utils.arrayify(messageToSign1),
      );
      const sig1 = ethers.utils.splitSignature(flatSig1);
      const messageToSign2 = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [pixTarget, 50, endtoendID],
      );
      const flatSig2 = await owner.signMessage(
        ethers.utils.arrayify(messageToSign2),
      );
      const sig2 = ethers.utils.splitSignature(flatSig2);
      const messageToSign3 = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [pixTarget, 25, endtoendID],
      );
      const flatSig3 = await owner.signMessage(
        ethers.utils.arrayify(messageToSign3),
      );
      const sig3 = ethers.utils.splitSignature(flatSig3);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        pixTarget,
        true,
        root,
      );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          100,
          [],
          [],
        );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          6,
          50,
          [],
          [],
        );
      await p2pix
        .connect(acc03)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          10,
          25,
          [],
          [],
        );

      const lockStatus1 =
        await p2pix.callStatic.getLocksStatus([1, 2, 3, 44]);
      const ls1: [BigNumber[], BigNumber[]] = [
        [
          ethers.constants.One,
          ethers.constants.Two,
          ethers.BigNumber.from(3),
          ethers.BigNumber.from(44),
        ],
        getBnFrom([1, 1, 1, 0]),
      ];

      const lockID = ethers.constants.One;
      const lockID2 = ethers.constants.Two;
      const lockID3 = ethers.BigNumber.from(3);
      const storage1: Lock = await p2pix.callStatic.mapLocks(
        lockID,
      );
      const storage2: Lock = await p2pix.callStatic.mapLocks(
        lockID2,
      );
      const storage3: Lock = await p2pix.callStatic.mapLocks(
        lockID3,
      );
      // relayerPremium == 0
      const tx = await p2pix
        .connect(acc01)
        .release(
          lockID,
          acc02.address,
          endtoendID,
          sig1.r,
          sig1.s,
          sig1.v,
        );
      // relayerPremium != 0 &&
      // lock's msg.sender != release's msg.sender
      const tx1 = await p2pix
        .connect(acc01)
        .release(
          lockID2,
          acc02.address,
          endtoendID,
          sig2.r,
          sig2.s,
          sig2.v,
        );
      // relayerPremium != 0 &&
      // lock's msg.sender == release's msg.sender
      const tx2 = await p2pix
        .connect(acc03)
        .release(
          lockID3,
          acc02.address,
          endtoendID,
          sig3.r,
          sig3.s,
          sig3.v,
        );
      const used1 = await p2pix.callStatic.usedTransactions(
        ethers.utils.arrayify(messageToSign1),
      );
      const used2 = await p2pix.callStatic.usedTransactions(
        ethers.utils.arrayify(messageToSign2),
      );
      const used3 = await p2pix.callStatic.usedTransactions(
        ethers.utils.arrayify(messageToSign3),
      );
      const acc01Record2 = await p2pix.callStatic.userRecord(
        acc01Key,
      );
      const acc03Record2 = await p2pix.callStatic.userRecord(
        acc03Key,
      );

      const lockStatus2 =
        await p2pix.callStatic.getLocksStatus([1, 2, 3, 44]);
      const ls2: [BigNumber[], BigNumber[]] = [
        [
          ethers.constants.One,
          ethers.constants.Two,
          ethers.BigNumber.from(3),
          ethers.BigNumber.from(44),
        ],
        getBnFrom([3, 3, 3, 0]),
      ];

      const batchedLocks: Array<BigNumber[]> = [
        ls1.slice(0, 1)[0],
        ls2.slice(0, 1)[0],
      ];
      const cData: Call[] = getLockData(
        erc20.address,
        batchedLocks,
      );

      expect(tx).to.be.ok;
      expect(tx1).to.be.ok;
      expect(tx2).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "LockReleased")
        .withArgs(acc02.address, lockID, storage1.amount);
      await expect(tx1)
        .to.emit(p2pix, "LockReleased")
        .withArgs(acc02.address, lockID2, storage2.amount);
      await expect(tx2)
        .to.emit(p2pix, "LockReleased")
        .withArgs(acc02.address, lockID3, storage3.amount);
      expect(used1).to.eq(true);
      expect(used2).to.eq(true);
      expect(used3).to.eq(true);
      expect(0).to.eq(acc01Record1).and.to.eq(acc03Record1);
      expect(acc01Record2).to.eq(6); // 0 + 6
      expect(acc03Record2).to.eq(185); // 100 + 50 + 25 + 10

      const addresses = [
        acc01.address,
        acc02.address,
        acc03.address,
        p2pix.address,
      ];

      const balances = [
        [0, 100, 0, "-100"],
        [0, 47, 3, "-50"],
        [0, 20, 5, "-25"],
      ];

      for (let i = 0; i < 3; i++) {
        const txs = [tx, tx1, tx2][i];
        await expect(txs).to.changeTokenBalances(
          erc20,
          addresses,
          balances[i],
        );
      }

      expect(lockStatus1[0].toString()).to.equal(
        ls1[0].toString(),
      );
      expect(lockStatus1[1].toString()).to.equal(
        ls1[1].toString(),
      );
      expect(lockStatus2[0].toString()).to.equal(
        ls2[0].toString(),
      );
      expect(lockStatus2[1].toString()).to.equal(
        ls2[1].toString(),
      );
      await expect(
        multicall.callStatic.mtc1(cData),
      ).to.be.revertedWithCustomError(
        multicall,
        P2PixErrors.CallFailed,
      );
    });
  });
  describe("Unexpire Locks", async () => {
    it("should revert if lock isn't expired", async () => {
      const target = ethers.BigNumber.from(101);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      await p2pix
        .connect(acc02)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          1,
          [],
          [],
        );
      const lockID = ethers.constants.One;
      const fail = p2pix.unlockExpired([lockID]);

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.NotExpired,
      );
    });
    it("should revert if lock has already been released", async () => {
      const endtoendID = ethers.constants.HashZero;
      const pixTarget = ethers.BigNumber.from(101);
      const messageToSign = ethers.utils.solidityKeccak256(
        ["uint160", "uint256", "bytes32"],
        [pixTarget, 1, endtoendID],
      );
      const messageHashBytes =
        ethers.utils.arrayify(messageToSign);
      const flatSig = await acc01.signMessage(
        messageHashBytes,
      );
      const sig = ethers.utils.splitSignature(flatSig);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        pixTarget,
        true,
        merkleRoot,
      );
      await p2pix
        .connect(acc02)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          1,
          [],
          [],
        );
      const lockID = ethers.constants.One;
      // await mine(10);
      await p2pix.release(
        lockID,
        acc03.address,
        endtoendID,
        sig.r,
        sig.s,
        sig.v,
      );
      const fail = p2pix.unlockExpired([lockID]);

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.AlreadyReleased,
      );
    });
    it("should unlock expired locks, update storage and emit events", async () => {
      const target = ethers.BigNumber.from(101);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      await p2pix
        .connect(acc02)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          1,
          [],
          [],
        );
      const lockID = ethers.constants.One;
      await mine(11);

      const lockStatus1 =
        await p2pix.callStatic.getLocksStatus([11, 1, 777]);
      const ls1: [BigNumber[], BigNumber[]] = [
        [
          ethers.BigNumber.from(11),
          ethers.constants.One,
          ethers.BigNumber.from(777),
        ],
        getBnFrom([0, 2, 0]),
      ];

      const storage: Lock = await p2pix.callStatic.mapLocks(
        lockID,
      );
      const userKey = await p2pix.callStatic._castAddrToKey(
        acc02.address,
      );
      const record1 = await p2pix.callStatic.userRecord(
        userKey,
      );
      const tx = await p2pix.unlockExpired([lockID]);
      const storage2: Lock = await p2pix.callStatic.mapLocks(
        lockID,
      );
      const record2 = await p2pix.callStatic.userRecord(
        userKey,
      );

      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "LockReturned")
        .withArgs(acc02.address, lockID);
      expect(storage.amount).to.eq(ethers.constants.One);
      expect(storage2.amount).to.eq(ethers.constants.Zero);
      expect(record1).to.eq(0);
      expect(record2).to.eq(price);
      expect(lockStatus1[0].toString()).to.equal(
        ls1[0].toString(),
      );
    });
    it("should unlock expired through lock function", async () => {
      const target = ethers.BigNumber.from(101);
      // test method through lock fx
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      const lock1: ContractTransaction = await p2pix
        .connect(acc01)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          price,
          proof,
          [],
        );
      // as return values of non view functions can't be accessed
      // outside the evm, we fetch the lockID from the emitted event.
      const rc: ContractReceipt = await lock1.wait();
      const event = rc.events?.find(
        event => event.event === "LockAdded",
      );
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      const emittedLockID = event?.args!["lockID"];
      const lockID = ethers.constants.One;

      // mine blocks to expire lock
      await mine(12);
      // const blocknum = await p2pix.provider.getBlockNumber();
      // console.log("bn = 6 + 12 ( i.e., %s )", blocknum);
      // console.log(
      //   "\n",
      //   " 2 blocks past the expiration block",
      // );
      // const struct2: Lock = await p2pix.callStatic.mapLocks(
      //   emittedLockID,
      // );
      // console.log(
      //   "\n",
      //   "Current state of the lock:",
      //   "\n",
      //   struct2,
      // );

      expect(emittedLockID).to.eq(lockID);

      // create another lock by freeing the price value
      // back to `l.remamining` and lock 100 again.
      const tx1 = await p2pix.lock(
        owner.address,
        erc20.address,
        acc02.address,
        acc03.address,
        0,
        100,
        [],
        [lockID],
      );
      const remaining = await p2pix.callStatic.getBalance(
        owner.address,
        erc20.address,
      );

      expect(tx1).to.be.ok;
      await expect(tx1)
        .to.emit(p2pix, "LockReturned")
        .withArgs(acc02.address, lockID);
      expect(remaining).to.eq(
        price.sub(ethers.BigNumber.from(100)),
      );
    });
    it("should unlock expired through withdraw function", async () => {
      const target = ethers.constants.One;
      // test method through withdraw fx
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      await p2pix
        .connect(acc01)
        .lock(
          owner.address,
          erc20.address,
          acc02.address,
          acc03.address,
          0,
          price,
          proof,
          [],
        );
      const lockID = ethers.constants.One;
      // mine blocks to expire lock
      await mine(11);
      const tx = await p2pix.withdraw(erc20.address, price, [
        lockID,
      ]);
      const remaining = await p2pix.callStatic.getBalance(
        owner.address,
        erc20.address,
      );

      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "LockReturned")
        .withArgs(acc02.address, lockID);
      expect(remaining).to.eq(0);
    });
  });

  describe("Seller Withdraw", async () => {
    it("should revert if the wished amount is invalid", async () => {
      const target = ethers.BigNumber.from(101);
      await erc20.approve(p2pix.address, price);
      await p2pix.deposit(
        erc20.address,
        price,
        target,
        true,
        merkleRoot,
      );
      const fail = p2pix
        .connect(acc02)
        .withdraw(
          erc20.address,
          price.mul(ethers.constants.Two),
          [],
        );

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.DecOverflow,
      );
    });
    it("should withdraw remaining funds from deposit, update storage and emit event", async () => {
      const newPrice = price.div(ethers.constants.Two);
      await erc20.approve(p2pix.address, price);
      const dep = await p2pix.deposit(
        erc20.address,
        price,
        ethers.BigNumber.from(101),
        true,
        merkleRoot,
      );
      const tx = await p2pix.withdraw(
        erc20.address,
        price.div(ethers.constants.Two),
        [],
      );

      expect(tx).to.be.ok;
      await expect(dep)
        .to.changeTokenBalance(
          erc20,
          owner.address,
          "-100000000000000000000",
        )
        .and.to.changeTokenBalance(
          erc20,
          p2pix.address,
          price,
        );
      await expect(tx)
        .to.changeTokenBalance(erc20, owner.address, newPrice)
        .and.to.changeTokenBalance(
          erc20,
          p2pix.address,
          "-50000000000000000000",
        );

      await expect(tx)
        .to.emit(p2pix, "DepositWithdrawn")
        .withArgs(owner.address, erc20.address, newPrice);
    });
  });

  describe("Allowlist Settings", async () => {
    it("should revert if the msg.sender differs from deposit's seller", async () => {
      const root = ethers.utils.keccak256(
        ethers.utils.toUtf8Bytes("root"),
      );
      const fail = p2pix
        .connect(acc02)
        .setRoot(owner.address, root);

      await expect(fail).to.be.revertedWithCustomError(
        p2pix,
        P2PixErrors.OnlySeller,
      );
    });
    it("should set root of seller's allowlist, update storage and emit event", async () => {
      const ownerKey = await p2pix.callStatic._castAddrToKey(
        owner.address,
      );
      const oldState = await p2pix.callStatic.sellerAllowList(
        ownerKey,
      );
      const tx = await p2pix
        .connect(owner)
        .setRoot(owner.address, merkleRoot);
      const newState = await p2pix.callStatic.sellerAllowList(
        ownerKey,
      );

      expect(tx).to.be.ok;
      await expect(tx)
        .to.emit(p2pix, "RootUpdated")
        .withArgs(owner.address, merkleRoot);
      expect(oldState).to.eq(ethers.constants.HashZero);
      expect(newState).to.eq(merkleRoot);
    });
  });
});
