import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers";
import { expect } from "chai";
import { ethers } from "hardhat";
import {
  EquippableTokenMock,
  ERC721ReceiverMock,
  MultiAssetRenderUtils,
  NonReceiverMock,
} from "../typechain-types";

describe("MultiAsset", async () => {
  let token: EquippableTokenMock;
  let renderUtils: MultiAssetRenderUtils;
  let nonReceiver: NonReceiverMock;
  let receiver721: ERC721ReceiverMock;

  let owner: SignerWithAddress;
  let addrs: SignerWithAddress[];

  const metaURIDefault = "metaURI";

  beforeEach(async () => {
    [owner, ...addrs] = await ethers.getSigners();

    const equppableFactory = await ethers.getContractFactory(
      "EquippableTokenMock"
    );
    token = await equppableFactory.deploy();
    await token.deployed();

    const renderFactory = await ethers.getContractFactory(
      "MultiAssetRenderUtils"
    );
    renderUtils = await renderFactory.deploy();
    await renderUtils.deployed();
  });

  describe("ERC165 check", async function () {
    it("can support IERC165", async function () {
      expect(await token.supportsInterface("0x01ffc9a7")).to.equal(true);
    });

    it("can support IERC721", async function () {
      expect(await token.supportsInterface("0x80ac58cd")).to.equal(true);
    });

    it("can support IMultiAsset", async function () {
      expect(await token.supportsInterface("0xd1526708")).to.equal(true);
    });

    it("cannot support other interfaceId", async function () {
      expect(await token.supportsInterface("0xffffffff")).to.equal(false);
    });
  });

  describe("Check OnReceived ERC721 and Multiasset", async function () {
    it("Revert on transfer to non onERC721/onMultiasset implementer", async function () {
      const tokenId = 1;
      await token.mint(owner.address, tokenId);

      const NonReceiver = await ethers.getContractFactory("NonReceiverMock");
      nonReceiver = await NonReceiver.deploy();
      await nonReceiver.deployed();

      await expect(
        token
          .connect(owner)
          ["safeTransferFrom(address,address,uint256)"](
            owner.address,
            nonReceiver.address,
            1
          )
      ).to.be.revertedWithCustomError(
        token,
        "ERC721TransferToNonReceiverImplementer"
      );
    });

    it("onERC721Received callback on transfer", async function () {
      const tokenId = 1;
      await token.mint(owner.address, tokenId);

      const ERC721Receiver = await ethers.getContractFactory(
        "ERC721ReceiverMock"
      );
      receiver721 = await ERC721Receiver.deploy();
      await receiver721.deployed();

      await token
        .connect(owner)
        ["safeTransferFrom(address,address,uint256)"](
          owner.address,
          receiver721.address,
          1
        );
      expect(await token.ownerOf(1)).to.equal(receiver721.address);
    });
  });

  describe("Asset storage", async function () {
    it("can add asset", async function () {
      const id = 10;

      await expect(token.addAssetEntry(id, metaURIDefault))
        .to.emit(token, "AssetSet")
        .withArgs(id);
    });

    it("cannot get non existing asset", async function () {
      const tokenId = 1;
      const resId = 10;
      await token.mint(owner.address, tokenId);
      await expect(
        token.getAssetMetadata(tokenId, resId)
      ).to.be.revertedWithCustomError(token, "TokenDoesNotHaveAsset");
    });

    it("cannot add asset entry if not issuer", async function () {
      const id = 10;
      await expect(
        token.connect(addrs[1]).addAssetEntry(id, metaURIDefault)
      ).to.be.revertedWith("RMRK: Only issuer");
    });

    it("can set and get issuer", async function () {
      const newIssuerAddr = addrs[1].address;
      expect(await token.getIssuer()).to.equal(owner.address);

      await token.setIssuer(newIssuerAddr);
      expect(await token.getIssuer()).to.equal(newIssuerAddr);
    });

    it("cannot set issuer if not issuer", async function () {
      const newIssuer = addrs[1];
      await expect(
        token.connect(newIssuer).setIssuer(newIssuer.address)
      ).to.be.revertedWith("RMRK: Only issuer");
    });

    it("cannot overwrite asset", async function () {
      const id = 10;

      await token.addAssetEntry(id, metaURIDefault);
      await expect(
        token.addAssetEntry(id, metaURIDefault)
      ).to.be.revertedWithCustomError(token, "AssetAlreadyExists");
    });

    it("cannot add asset with id 0", async function () {
      const id = ethers.utils.hexZeroPad("0x0", 8);

      await expect(
        token.addAssetEntry(id, metaURIDefault)
      ).to.be.revertedWithCustomError(token, "IdZeroForbidden");
    });

    it("cannot add same asset twice", async function () {
      const id = 10;

      await expect(token.addAssetEntry(id, metaURIDefault))
        .to.emit(token, "AssetSet")
        .withArgs(id);

      await expect(
        token.addAssetEntry(id, metaURIDefault)
      ).to.be.revertedWithCustomError(token, "AssetAlreadyExists");
    });
  });

  describe("Adding assets", async function () {
    it("can add asset to token", async function () {
      const resId = 1;
      const resId2 = 2;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId, resId2]);
      await expect(token.addAssetToToken(tokenId, resId, 0)).to.emit(
        token,
        "AssetAddedToTokens"
      );
      await expect(token.addAssetToToken(tokenId, resId2, 0)).to.emit(
        token,
        "AssetAddedToTokens"
      );

      const pendingIds = await token.getPendingAssets(tokenId);
      expect(
        await renderUtils.getAssetsById(token.address, tokenId, pendingIds)
      ).to.be.eql([metaURIDefault, metaURIDefault]);
    });

    it("cannot add non existing asset to token", async function () {
      const resId = 1;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await expect(
        token.addAssetToToken(tokenId, resId, 0)
      ).to.be.revertedWithCustomError(token, "NoAssetMatchingId");
    });

    it("can add asset to non existing token and it is pending when minted", async function () {
      const resId = 1;
      const tokenId = 1;
      await addAssets([resId]);

      await token.addAssetToToken(tokenId, resId, 0);
      await token.mint(owner.address, tokenId);
      expect(await token.getPendingAssets(tokenId)).to.eql([
        ethers.BigNumber.from(resId),
      ]);
    });

    it("cannot add asset twice to the same token", async function () {
      const resId = 1;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId]);
      await token.addAssetToToken(tokenId, resId, 0);
      await expect(
        token.addAssetToToken(tokenId, ethers.BigNumber.from(resId), 0)
      ).to.be.revertedWithCustomError(token, "AssetAlreadyExists");
    });

    it("cannot add too many assets to the same token", async function () {
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      for (let i = 1; i <= 128; i++) {
        await addAssets([i]);
        await token.addAssetToToken(tokenId, i, 0);
      }

      // Now it's full, next should fail
      const resId = 129;
      await addAssets([resId]);
      await expect(
        token.addAssetToToken(tokenId, resId, 0)
      ).to.be.revertedWithCustomError(token, "MaxPendingAssetsReached");
    });

    it("can add same asset to 2 different tokens", async function () {
      const resId = 1;
      const tokenId1 = 1;
      const tokenId2 = 2;

      await token.mint(owner.address, tokenId1);
      await token.mint(owner.address, tokenId2);
      await addAssets([resId]);
      await token.addAssetToToken(tokenId1, resId, 0);
      await token.addAssetToToken(tokenId2, resId, 0);
    });
  });

  describe("Accepting assets", async function () {
    it("can accept asset if owner", async function () {
      const { tokenOwner, tokenId } = await mintSampleToken();
      const approved = tokenOwner;

      await checkAcceptFromAddress(approved, tokenId);
    });

    it("can accept asset if approved for assets", async function () {
      const { tokenId } = await mintSampleToken();
      const approved = addrs[1];

      await token.approveForAssets(approved.address, tokenId);
      await checkAcceptFromAddress(approved, tokenId);
    });

    it("can accept asset if approved for assets for all", async function () {
      const { tokenId } = await mintSampleToken();
      const approved = addrs[2];

      await token.setApprovalForAllForAssets(approved.address, true);
      await checkAcceptFromAddress(approved, tokenId);
    });

    it("can accept multiple assets", async function () {
      const resId = 1;
      const resId2 = 2;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId, resId2]);
      await token.addAssetToToken(tokenId, resId, 0);
      await token.addAssetToToken(tokenId, resId2, 0);
      await expect(token.acceptAsset(tokenId, 1, resId2))
        .to.emit(token, "AssetAccepted")
        .withArgs(tokenId, resId2, 0);
      await expect(token.acceptAsset(tokenId, 0, resId))
        .to.emit(token, "AssetAccepted")
        .withArgs(tokenId, resId, 0);

      expect(await token.getPendingAssets(tokenId)).to.be.eql([]);

      const activeIds = await token.getActiveAssets(tokenId);
      expect(
        await renderUtils.getAssetsById(token.address, tokenId, activeIds)
      ).to.eql([metaURIDefault, metaURIDefault]);
    });

    it("cannot accept asset twice", async function () {
      const resId = 1;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId]);
      await token.addAssetToToken(tokenId, resId, 0);
      await token.acceptAsset(tokenId, 0, resId);
    });

    it("cannot accept asset if not owner", async function () {
      const resId = 1;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId]);
      await token.addAssetToToken(tokenId, resId, 0);
      await expect(
        token.connect(addrs[1]).acceptAsset(tokenId, 0, resId)
      ).to.be.revertedWithCustomError(token, "NotApprovedForAssetsOrOwner");
    });

    it("cannot accept non existing asset", async function () {
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await expect(
        token.acceptAsset(tokenId, 0, 1)
      ).to.be.revertedWithCustomError(token, "IndexOutOfRange");
    });
  });

  describe("Overwriting assets", async function () {
    it("can add asset to token overwritting an existing one", async function () {
      const resId = 1;
      const resId2 = 2;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId, resId2]);
      await token.addAssetToToken(tokenId, resId, 0);
      await token.acceptAsset(tokenId, 0, resId);

      // Add new asset to overwrite the first, and accept
      const activeAssets = await token.getActiveAssets(tokenId);
      await expect(token.addAssetToToken(tokenId, resId2, activeAssets[0]))
        .to.emit(token, "AssetAddedToTokens")
        .withArgs([tokenId], resId2, resId);
      const pendingAssets = await token.getPendingAssets(tokenId);

      expect(
        await token.getAssetReplacements(tokenId, pendingAssets[0])
      ).to.eql(activeAssets[0]);
      await expect(token.acceptAsset(tokenId, 0, resId2))
        .to.emit(token, "AssetAccepted")
        .withArgs(tokenId, resId2, resId);

      const activeIds = await token.getActiveAssets(tokenId);
      expect(
        await renderUtils.getAssetsById(token.address, tokenId, activeIds)
      ).to.eql([metaURIDefault]);
      // Overwrite should be gone
      expect(
        await token.getAssetReplacements(tokenId, pendingAssets[0])
      ).to.eql(ethers.BigNumber.from(0));
    });

    it("can overwrite non existing asset to token, it could have been deleted", async function () {
      const resId = 1;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId]);
      await token.addAssetToToken(
        tokenId,
        resId,
        ethers.utils.hexZeroPad("0x1", 8)
      );
      await token.acceptAsset(tokenId, 0, resId);

      const activeIds = await token.getActiveAssets(tokenId);
      expect(
        await renderUtils.getAssetsById(token.address, tokenId, activeIds)
      ).to.eql([metaURIDefault]);
    });
  });

  describe("Rejecting assets", async function () {
    it("can reject asset if owner", async function () {
      const { tokenOwner, tokenId } = await mintSampleToken();
      const approved = tokenOwner;

      await checkRejectFromAddress(approved, tokenId);
    });

    it("can reject asset if approved for assets", async function () {
      const { tokenId } = await mintSampleToken();
      const approved = addrs[1];

      await token.approveForAssets(approved.address, tokenId);
      await checkRejectFromAddress(approved, tokenId);
    });

    it("can reject asset if approved for assets for all", async function () {
      const { tokenId } = await mintSampleToken();
      const approved = addrs[2];

      await token.setApprovalForAllForAssets(approved.address, true);
      await checkRejectFromAddress(approved, tokenId);
    });

    it("can reject all assets if owner", async function () {
      const { tokenOwner, tokenId } = await mintSampleToken();
      const approved = tokenOwner;

      await checkRejectAllFromAddress(approved, tokenId);
    });

    it("can reject all assets if approved for assets", async function () {
      const { tokenId } = await mintSampleToken();
      const approved = addrs[1];

      await token.approveForAssets(approved.address, tokenId);
      await checkRejectAllFromAddress(approved, tokenId);
    });

    it("can reject all assets if approved for assets for all", async function () {
      const { tokenId } = await mintSampleToken();
      const approved = addrs[2];

      await token.setApprovalForAllForAssets(approved.address, true);
      await checkRejectAllFromAddress(approved, tokenId);
    });

    it("can reject asset and overwrites are cleared", async function () {
      const resId = 1;
      const resId2 = 2;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId, resId2]);
      await token.addAssetToToken(tokenId, resId, 0);
      await token.acceptAsset(tokenId, 0, resId);

      // Will try to overwrite but we reject it
      await token.addAssetToToken(tokenId, resId2, resId);
      await token.rejectAsset(tokenId, 0, resId2);

      expect(await token.getAssetReplacements(tokenId, resId2)).to.eql(
        ethers.BigNumber.from(0)
      );
    });

    it("can reject all assets and overwrites are cleared", async function () {
      const resId = 1;
      const resId2 = 2;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId, resId2]);
      await token.addAssetToToken(tokenId, resId, 0);
      await token.acceptAsset(tokenId, 0, resId);

      // Will try to overwrite but we reject all
      await token.addAssetToToken(tokenId, resId2, resId);
      await token.rejectAllAssets(tokenId, 1);

      expect(await token.getAssetReplacements(tokenId, resId2)).to.eql(
        ethers.BigNumber.from(0)
      );
    });

    it("can reject all pending assets at max capacity", async function () {
      const tokenId = 1;
      const resArr = [];

      for (let i = 1; i < 128; i++) {
        resArr.push(i);
      }

      await token.mint(owner.address, tokenId);
      await addAssets(resArr);

      for (let i = 1; i < 128; i++) {
        await token.addAssetToToken(tokenId, i, 1);
      }
      await token.rejectAllAssets(tokenId, 128);

      expect(await token.getAssetReplacements(1, 2)).to.eql(
        ethers.BigNumber.from(0)
      );
    });

    it("cannot reject asset twice", async function () {
      const resId = 1;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId]);
      await token.addAssetToToken(tokenId, resId, 0);
      await token.rejectAsset(tokenId, 0, resId);
    });

    it("cannot reject asset nor reject all if not owner", async function () {
      const resId = 1;
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await addAssets([resId]);
      await token.addAssetToToken(tokenId, resId, 0);

      await expect(
        token.connect(addrs[1]).rejectAsset(tokenId, 0, resId)
      ).to.be.revertedWithCustomError(token, "NotApprovedForAssetsOrOwner");
      await expect(
        token.connect(addrs[1]).rejectAllAssets(tokenId, 1)
      ).to.be.revertedWithCustomError(token, "NotApprovedForAssetsOrOwner");
    });

    it("cannot reject non existing asset", async function () {
      const tokenId = 1;

      await token.mint(owner.address, tokenId);
      await expect(
        token.rejectAsset(tokenId, 0, 1)
      ).to.be.revertedWithCustomError(token, "IndexOutOfRange");
    });
  });

  describe("Priorities", async function () {
    it("can set and get priorities", async function () {
      const tokenId = 1;
      await addAssetsToToken(tokenId);

      expect(await token.getActiveAssetPriorities(tokenId)).to.be.eql([0, 1]);
      await expect(token.setPriority(tokenId, [2, 1]))
        .to.emit(token, "AssetPrioritySet")
        .withArgs(tokenId);
      expect(await token.getActiveAssetPriorities(tokenId)).to.be.eql([2, 1]);
    });

    it("cannot set priorities for non owned token", async function () {
      const tokenId = 1;
      await addAssetsToToken(tokenId);
      await expect(
        token.connect(addrs[1]).setPriority(tokenId, [2, 1])
      ).to.be.revertedWithCustomError(token, "NotApprovedForAssetsOrOwner");
    });

    it("cannot set different number of priorities", async function () {
      const tokenId = 1;
      await addAssetsToToken(tokenId);
      await expect(
        token.setPriority(tokenId, [1])
      ).to.be.revertedWithCustomError(token, "BadPriorityListLength");
      await expect(
        token.setPriority(tokenId, [2, 1, 3])
      ).to.be.revertedWithCustomError(token, "BadPriorityListLength");
    });

    it("cannot set priorities for non existing token", async function () {
      const tokenId = 1;
      await expect(
        token.connect(addrs[1]).setPriority(tokenId, [])
      ).to.be.revertedWithCustomError(token, "ERC721InvalidTokenId");
    });
  });

  describe("Approval Cleaning", async function () {
    it("cleans token and assets approvals on transfer", async function () {
      const tokenId = 1;
      const tokenOwner = addrs[1];
      const newOwner = addrs[2];
      const approved = addrs[3];
      await token.mint(tokenOwner.address, tokenId);
      await token.connect(tokenOwner).approve(approved.address, tokenId);
      await token
        .connect(tokenOwner)
        .approveForAssets(approved.address, tokenId);

      expect(await token.getApproved(tokenId)).to.eql(approved.address);
      expect(await token.getApprovedForAssets(tokenId)).to.eql(
        approved.address
      );

      await token.connect(tokenOwner).transfer(newOwner.address, tokenId);

      expect(await token.getApproved(tokenId)).to.eql(
        ethers.constants.AddressZero
      );
      expect(await token.getApprovedForAssets(tokenId)).to.eql(
        ethers.constants.AddressZero
      );
    });

    it("cleans token and assets approvals on burn", async function () {
      const tokenId = 1;
      const tokenOwner = addrs[1];
      const approved = addrs[3];
      await token.mint(tokenOwner.address, tokenId);
      await token.connect(tokenOwner).approve(approved.address, tokenId);
      await token
        .connect(tokenOwner)
        .approveForAssets(approved.address, tokenId);

      expect(await token.getApproved(tokenId)).to.eql(approved.address);
      expect(await token.getApprovedForAssets(tokenId)).to.eql(
        approved.address
      );

      await token.connect(tokenOwner)["burn(uint256)"](tokenId);

      await expect(token.getApproved(tokenId)).to.be.revertedWithCustomError(
        token,
        "ERC721InvalidTokenId"
      );
      await expect(
        token.getApprovedForAssets(tokenId)
      ).to.be.revertedWithCustomError(token, "ERC721InvalidTokenId");
    });
  });

  async function mintSampleToken(): Promise<{
    tokenOwner: SignerWithAddress;
    tokenId: number;
  }> {
    const tokenOwner = owner;
    const tokenId = 1;
    await token.mint(tokenOwner.address, tokenId);

    return { tokenOwner, tokenId };
  }

  async function addAssets(ids: number[]): Promise<void> {
    ids.forEach(async (resId) => {
      await token.addAssetEntry(resId, metaURIDefault);
    });
  }

  async function addAssetsToToken(tokenId: number): Promise<void> {
    const resId = 1;
    const resId2 = 2;
    await token.mint(owner.address, tokenId);
    await addAssets([resId, resId2]);
    await token.addAssetToToken(tokenId, resId, 0);
    await token.addAssetToToken(tokenId, resId2, 0);
    await token.acceptAsset(tokenId, 0, resId);
    await token.acceptAsset(tokenId, 0, resId2);
  }

  async function checkAcceptFromAddress(
    accepter: SignerWithAddress,
    tokenId: number
  ): Promise<void> {
    const resId = 1;

    await addAssets([resId]);
    await token.addAssetToToken(tokenId, resId, 0);
    await expect(token.connect(accepter).acceptAsset(tokenId, 0, resId))
      .to.emit(token, "AssetAccepted")
      .withArgs(tokenId, resId, 0);

    expect(await token.getPendingAssets(tokenId)).to.be.eql([]);

    const activeIds = await token.getActiveAssets(tokenId);
    expect(
      await renderUtils.getAssetsById(token.address, tokenId, activeIds)
    ).to.eql([metaURIDefault]);
  }

  async function checkRejectFromAddress(
    rejecter: SignerWithAddress,
    tokenId: number
  ): Promise<void> {
    const resId = 1;

    await addAssets([resId]);
    await token.addAssetToToken(tokenId, resId, 0);

    await expect(
      token.connect(rejecter).rejectAsset(tokenId, 0, resId)
    ).to.emit(token, "AssetRejected");

    expect(await token.getPendingAssets(tokenId)).to.be.eql([]);
    expect(await token.getActiveAssets(tokenId)).to.be.eql([]);
  }

  async function checkRejectAllFromAddress(
    rejecter: SignerWithAddress,
    tokenId: number
  ): Promise<void> {
    const resId = 1;
    const resId2 = 2;

    await addAssets([resId, resId2]);
    await token.addAssetToToken(tokenId, resId, 0);
    await token.addAssetToToken(tokenId, resId2, 0);

    await expect(token.connect(rejecter).rejectAllAssets(tokenId, 2)).to.emit(
      token,
      "AssetRejected"
    );

    expect(await token.getPendingAssets(tokenId)).to.be.eql([]);
    expect(await token.getActiveAssets(tokenId)).to.be.eql([]);
  }
});
