// Hardhat tests for AereDestinationSettler (ERC-7683 IDestinationSettler).
//
// The DESTINATION / filling side of AERE's cross-chain intents. A solver calls
// fill(orderId, originData, fillerData) to deliver the order's promised output to
// the recipient on AERE; the settler records the fill and emits Filled so the origin
// chain can repay the solver behind a QBFT-finality proof.
//
// Cases:
//   - valid fill: delivers output to recipient, records the fill, emits Filled,
//     exposes the record + fillCommitment
//   - duplicate fill reverts (AlreadyFilled)
//   - mismatched-output fill reverts (OutputMismatch)
//   - zero-recipient reverts (ZeroRecipient)
//
// Run: npx hardhat test test/destination-settler.test.js

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

const ONE = 10n ** 18n;
const AERE_CHAIN_ID = 2800n;

function b32(addr) {
  return ethers.zeroPadValue(addr, 32);
}

// originData: the origin settler's declared output leg (matches AereSpokePool).
function encodeOriginData(orderId, outputToken, outputAmount, recipient32) {
  return ethers.AbiCoder.defaultAbiCoder().encode(
    ["bytes32", "address", "uint256", "bytes32"],
    [orderId, outputToken, outputAmount, recipient32]
  );
}

// fillerData: the solver's actual delivery + origin-chain repayment address.
function encodeFillerData(deliveredToken, deliveredAmount, deliveredRecipient, repaymentAddress32) {
  return ethers.AbiCoder.defaultAbiCoder().encode(
    ["address", "uint256", "address", "bytes32"],
    [deliveredToken, deliveredAmount, deliveredRecipient, repaymentAddress32]
  );
}

async function deployFixture() {
  const [deployer, solver, recipient, other] = await ethers.getSigners();

  // Output token the solver delivers on AERE.
  const Token = await ethers.getContractFactory("MockERC20Lending");
  const token = await Token.deploy("USD Coin (bridged)", "USDC.e", 6);
  await token.waitForDeployment();

  const Settler = await ethers.getContractFactory(
    "contracts/intents/AereDestinationSettler.sol:AereDestinationSettler"
  );
  const settler = await Settler.deploy();
  await settler.waitForDeployment();

  // Fund the solver with output tokens and approve the settler to pull them.
  const funded = 1_000_000n * ONE;
  await token.mint(solver.address, funded);
  await token.connect(solver).approve(await settler.getAddress(), funded);

  return { deployer, solver, recipient, other, token, settler };
}

describe("AereDestinationSettler - ERC-7683 IDestinationSettler", function () {
  it("valid fill: delivers output to recipient, records the fill, emits Filled, exposes commitment", async function () {
    const { solver, recipient, token, settler } = await deployFixture();

    const orderId = ethers.id("order-1");
    const outputAmount = 250n * ONE;
    const tokenAddr = await token.getAddress();
    const recipient32 = b32(recipient.address);
    const repayment32 = b32(solver.address);

    const originData = encodeOriginData(orderId, tokenAddr, outputAmount, recipient32);
    const fillerData = encodeFillerData(tokenAddr, outputAmount, recipient.address, repayment32);

    const recipBefore = await token.balanceOf(recipient.address);

    await expect(settler.connect(solver).fill(orderId, originData, fillerData))
      .to.emit(settler, "Filled");

    // Recipient received exactly the delivered output; settler custodies nothing.
    expect(await token.balanceOf(recipient.address) - recipBefore).to.equal(outputAmount);
    expect(await token.balanceOf(await settler.getAddress())).to.equal(0n);

    // Fill recorded and readable.
    expect(await settler.isFilled(orderId)).to.equal(true);
    const rec = await settler.getFill(orderId);
    expect(rec.filler).to.equal(solver.address);
    expect(rec.outputToken).to.equal(tokenAddr);
    expect(rec.outputAmount).to.equal(outputAmount);
    expect(rec.recipient).to.equal(recipient.address);
    expect(rec.repaymentAddress).to.equal(repayment32);
    expect(await settler.fillCount()).to.equal(1n);

    // fillCommitment binds the exact fields the origin-side verifier re-derives.
    const expectedCommitment = ethers.keccak256(
      ethers.AbiCoder.defaultAbiCoder().encode(
        ["uint256", "address", "bytes32", "address", "address", "uint256", "address", "bytes32"],
        [
          AERE_CHAIN_ID,
          await settler.getAddress(),
          orderId,
          solver.address,
          tokenAddr,
          outputAmount,
          recipient.address,
          repayment32,
        ]
      )
    );
    expect(await settler.fillCommitment(orderId)).to.equal(expectedCommitment);
  });

  it("duplicate fill reverts (AlreadyFilled)", async function () {
    const { solver, recipient, token, settler } = await deployFixture();

    const orderId = ethers.id("order-dup");
    const outputAmount = 100n * ONE;
    const tokenAddr = await token.getAddress();
    const originData = encodeOriginData(orderId, tokenAddr, outputAmount, b32(recipient.address));
    const fillerData = encodeFillerData(tokenAddr, outputAmount, recipient.address, b32(solver.address));

    await settler.connect(solver).fill(orderId, originData, fillerData);

    await expect(settler.connect(solver).fill(orderId, originData, fillerData))
      .to.be.revertedWithCustomError(settler, "AlreadyFilled")
      .withArgs(orderId);
  });

  it("mismatched-output fill reverts (OutputMismatch)", async function () {
    const { solver, recipient, other, token, settler } = await deployFixture();

    const orderId = ethers.id("order-mismatch");
    const outputAmount = 100n * ONE;
    const tokenAddr = await token.getAddress();
    const originData = encodeOriginData(orderId, tokenAddr, outputAmount, b32(recipient.address));

    // Short delivery (less than the declared output) must revert.
    const shortFiller = encodeFillerData(tokenAddr, outputAmount - 1n, recipient.address, b32(solver.address));
    await expect(settler.connect(solver).fill(orderId, originData, shortFiller))
      .to.be.revertedWithCustomError(settler, "OutputMismatch");

    // Wrong recipient (delivering to someone other than the declared recipient) must revert.
    const wrongRecipFiller = encodeFillerData(tokenAddr, outputAmount, other.address, b32(solver.address));
    await expect(settler.connect(solver).fill(orderId, originData, wrongRecipFiller))
      .to.be.revertedWithCustomError(settler, "OutputMismatch");

    // Wrong token (delivering a different token than declared) must revert.
    const OtherToken = await ethers.getContractFactory("MockERC20Lending");
    const otherToken = await OtherToken.deploy("Wrapped Ether", "WETH", 18);
    await otherToken.waitForDeployment();
    await otherToken.mint(solver.address, outputAmount);
    await otherToken.connect(solver).approve(await settler.getAddress(), outputAmount);
    const wrongTokenFiller = encodeFillerData(await otherToken.getAddress(), outputAmount, recipient.address, b32(solver.address));
    await expect(settler.connect(solver).fill(orderId, originData, wrongTokenFiller))
      .to.be.revertedWithCustomError(settler, "OutputMismatch");

    // Order stayed unfilled through all three rejected attempts.
    expect(await settler.isFilled(orderId)).to.equal(false);
  });

  it("zero-recipient reverts (ZeroRecipient)", async function () {
    const { solver, token, settler } = await deployFixture();

    const orderId = ethers.id("order-zero-recip");
    const outputAmount = 100n * ONE;
    const tokenAddr = await token.getAddress();
    // Declared recipient is zero.
    const originData = encodeOriginData(orderId, tokenAddr, outputAmount, ethers.ZeroHash);
    const fillerData = encodeFillerData(tokenAddr, outputAmount, ethers.ZeroAddress, b32(solver.address));

    await expect(settler.connect(solver).fill(orderId, originData, fillerData))
      .to.be.revertedWithCustomError(settler, "ZeroRecipient");
  });
});
