const { expect } = require("chai");
const { ethers } = require("hardhat");

// ERC-6551 token-bound accounts: canonical registry + AereTokenBoundAccount.
// Verifies that an NFT can own a wallet that (1) is deployed at the registry's
// deterministic address, (2) is controlled by the current NFT holder, (3) holds
// and moves native value via execute(), (4) produces EIP-1271 signatures for the
// holder, (5) tracks its ERC-6551 state, and (6) rejects non-holders and
// non-CALL operations.

describe("AereTokenBoundAccount (ERC-6551)", function () {
  let registry, impl, nft, owner, other;
  const SALT = ethers.ZeroHash;
  const TOKEN_ID = 1n;
  let chainId;

  before(async function () {
    [owner, other] = await ethers.getSigners();
    chainId = (await ethers.provider.getNetwork()).chainId;

    registry = await (await ethers.getContractFactory("ERC6551Registry")).deploy();
    await registry.waitForDeployment();

    impl = await (await ethers.getContractFactory("AereTokenBoundAccount")).deploy();
    await impl.waitForDeployment();

    nft = await (await ethers.getContractFactory("MockERC721")).deploy();
    await nft.waitForDeployment();
    await (await nft.mint(owner.address, TOKEN_ID)).wait();
  });

  it("account() is deterministic and createAccount() deploys there", async function () {
    const predicted = await registry.account.staticCall(
      await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID
    );
    // no code yet
    expect(await ethers.provider.getCode(predicted)).to.equal("0x");

    const created = await registry.createAccount.staticCall(
      await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID
    );
    expect(created).to.equal(predicted);

    await (await registry.createAccount(
      await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID
    )).wait();
    expect(await ethers.provider.getCode(predicted)).to.not.equal("0x");

    // idempotent: same address on repeat
    const again = await registry.account.staticCall(
      await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID
    );
    expect(again).to.equal(predicted);
  });

  async function boundAccount() {
    const addr = await registry.account.staticCall(
      await impl.getAddress(), SALT, chainId, await nft.getAddress(), TOKEN_ID
    );
    return ethers.getContractAt("AereTokenBoundAccount", addr);
  }

  it("token() returns the exact binding and owner() tracks the NFT holder", async function () {
    const acct = await boundAccount();
    const [cid, tc, tid] = await acct.token();
    expect(cid).to.equal(chainId);
    expect(tc).to.equal(await nft.getAddress());
    expect(tid).to.equal(TOKEN_ID);
    expect(await acct.owner()).to.equal(owner.address);
  });

  it("holds native value and executes a CALL as the holder; non-holder reverts", async function () {
    const acct = await boundAccount();
    const acctAddr = await acct.getAddress();

    // fund the account
    await (await owner.sendTransaction({ to: acctAddr, value: ethers.parseEther("1") })).wait();
    expect(await ethers.provider.getBalance(acctAddr)).to.equal(ethers.parseEther("1"));

    expect(await acct.state()).to.equal(0n);

    // holder moves 0.4 AERE out to `other`
    const before = await ethers.provider.getBalance(other.address);
    await (await acct.connect(owner).execute(other.address, ethers.parseEther("0.4"), "0x", 0)).wait();
    const after = await ethers.provider.getBalance(other.address);
    expect(after - before).to.equal(ethers.parseEther("0.4"));
    expect(await acct.state()).to.equal(1n); // state bumped

    // a non-holder cannot execute
    await expect(
      acct.connect(other).execute(other.address, 0, "0x", 0)
    ).to.be.revertedWithCustomError(acct, "InvalidSigner");

    // only CALL (operation 0) is supported
    await expect(
      acct.connect(owner).execute(other.address, 0, "0x", 1)
    ).to.be.revertedWithCustomError(acct, "UnsupportedOperation");
  });

  it("isValidSignature (EIP-1271) accepts the holder's signature and rejects others", async function () {
    const acct = await boundAccount();
    const digest = ethers.hexlify(ethers.randomBytes(32));

    const goodSig = await owner.signMessage(ethers.getBytes(digest));
    const ethDigest = ethers.hashMessage(ethers.getBytes(digest));
    expect(await acct.isValidSignature(ethDigest, goodSig)).to.equal("0x1626ba7e");

    const badSig = await other.signMessage(ethers.getBytes(digest));
    expect(await acct.isValidSignature(ethDigest, badSig)).to.equal("0x00000000");
  });

  it("control follows the NFT: transferring it re-points owner()", async function () {
    const acct = await boundAccount();
    await (await nft.transferFrom(owner.address, other.address, TOKEN_ID)).wait();
    expect(await acct.owner()).to.equal(other.address);

    // now `other` controls it, `owner` does not
    await (await acct.connect(other).execute(owner.address, 0, "0x", 0)).wait();
    await expect(
      acct.connect(owner).execute(owner.address, 0, "0x", 0)
    ).to.be.revertedWithCustomError(acct, "InvalidSigner");

    // restore for isolation
    await (await nft.connect(other).transferFrom(other.address, owner.address, TOKEN_ID)).wait();
  });

  it("supportsInterface reports ERC-165 / ERC-6551 / ERC-1271", async function () {
    const acct = await boundAccount();
    expect(await acct.supportsInterface("0x01ffc9a7")).to.equal(true); // ERC-165
    expect(await acct.supportsInterface("0x6faff5f1")).to.equal(true); // IERC6551Account
    expect(await acct.supportsInterface("0x51945447")).to.equal(true); // IERC6551Executable
    expect(await acct.supportsInterface("0x1626ba7e")).to.equal(true); // IERC1271
    expect(await acct.supportsInterface("0xffffffff")).to.equal(false);
  });
});
