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

describe("AereBestExReceipt", () => {
  let foundation, casp1, casp2, outsider;
  let reg;

  const LICENCE_REF = ethers.keccak256(ethers.toUtf8Bytes("MICA-XYZ-12345||DE-BAFIN||LEI-1234"));

  beforeEach(async () => {
    [foundation, casp1, casp2, outsider] = await ethers.getSigners();
    const F = await ethers.getContractFactory("AereBestExReceipt");
    reg = await F.deploy(foundation.address);
    await reg.waitForDeployment();
  });

  function blankReceipt(caspAddr, overrides = {}) {
    return {
      casp: caspAddr,
      clientHash: ethers.keccak256(ethers.toUtf8Bytes("client-42")),
      clientClass: 0, // Retail
      orderId: ethers.keccak256(ethers.toUtf8Bytes("order-1")),
      orderType: 0, // Market
      instrumentSymbol: ethers.keccak256(ethers.toUtf8Bytes("BTC/EUR")),
      requestedQty: ethers.parseUnits("1", 18),
      executedQty: ethers.parseUnits("1", 18),
      executedPrice: ethers.parseUnits("63000", 18),
      executedVenue: ethers.keccak256(ethers.toUtf8Bytes("Binance.com")),
      alternativesRoot: ethers.keccak256(ethers.toUtf8Bytes("merkle-root-alts")),
      algorithmRef: ethers.keccak256(ethers.toUtf8Bytes("SOR-v3.1||abc123")),
      decisionTsMs: 1717880000000n,
      executionTsMs: 1717880000420n,
      evidenceUri: "ipfs://bafy...alts.json",
      ...overrides,
    };
  }

  it("registers a CASP only by the Foundation", async () => {
    await expect(
      reg.connect(outsider).registerCasp(casp1.address, LICENCE_REF)
    ).to.be.revertedWithCustomError(reg, "NotFoundation");
    await reg.connect(foundation).registerCasp(casp1.address, LICENCE_REF);
    expect((await reg.caspOf(casp1.address)).exists).to.equal(true);
  });

  it("rejects double-registration + zero licence", async () => {
    await reg.connect(foundation).registerCasp(casp1.address, LICENCE_REF);
    await expect(
      reg.connect(foundation).registerCasp(casp1.address, LICENCE_REF)
    ).to.be.revertedWithCustomError(reg, "CaspExists");
    await expect(
      reg.connect(foundation).registerCasp(casp2.address, ethers.ZeroHash)
    ).to.be.revertedWithCustomError(reg, "ZeroLicence");
  });

  it("issues a receipt, increments nonce, emits the full event", async () => {
    await reg.connect(foundation).registerCasp(casp1.address, LICENCE_REF);
    const r = blankReceipt(casp1.address);
    const expectedId = await reg.nextReceiptId(casp1.address);

    await expect(reg.connect(casp1).issueReceipt(r))
      .to.emit(reg, "ReceiptIssued")
      .withArgs(
        casp1.address,
        expectedId,
        r.clientHash,
        r.instrumentSymbol,
        r.executedVenue,
        r.alternativesRoot,
        r.executedQty,
        r.executedPrice,
        r.decisionTsMs,
        r.executionTsMs,
        r.algorithmRef,
        r.evidenceUri
      );

    expect(await reg.issuedCount(casp1.address)).to.equal(1n);
    expect(await reg.receiptNonce(casp1.address)).to.equal(1n);
    const stored = await reg.getReceipt(expectedId);
    expect(stored.executedPrice).to.equal(r.executedPrice);
    expect(await reg.executionLatencyMs(expectedId)).to.equal(420n);
    expect(await reg.caspIsActive(casp1.address)).to.equal(true);
  });

  it("rejects unregistered, paused, and mismatched-casp issuance", async () => {
    const r = blankReceipt(casp1.address);
    // unregistered
    await expect(reg.connect(casp1).issueReceipt(r)).to.be.revertedWithCustomError(reg, "CaspUnknown");

    await reg.connect(foundation).registerCasp(casp1.address, LICENCE_REF);

    // r.casp != msg.sender
    await expect(
      reg.connect(casp1).issueReceipt(blankReceipt(casp2.address))
    ).to.be.revertedWithCustomError(reg, "CaspUnknown");

    // pause then attempt
    await reg.connect(foundation).setCaspPaused(casp1.address, true);
    await expect(reg.connect(casp1).issueReceipt(r)).to.be.revertedWithCustomError(reg, "CaspIsPaused");

    // unpause + succeed
    await reg.connect(foundation).setCaspPaused(casp1.address, false);
    await reg.connect(casp1).issueReceipt(r);
  });

  it("rejects malformed receipts (timestamps, qty, alts root)", async () => {
    await reg.connect(foundation).registerCasp(casp1.address, LICENCE_REF);
    // execution before decision
    await expect(
      reg.connect(casp1).issueReceipt(
        blankReceipt(casp1.address, { decisionTsMs: 1000n, executionTsMs: 500n })
      )
    ).to.be.revertedWithCustomError(reg, "BadTimestamps");

    // zero qty
    await expect(
      reg.connect(casp1).issueReceipt(blankReceipt(casp1.address, { executedQty: 0 }))
    ).to.be.revertedWithCustomError(reg, "BadQuantities");

    // over-execution
    await expect(
      reg.connect(casp1).issueReceipt(
        blankReceipt(casp1.address, {
          requestedQty: ethers.parseUnits("1", 18),
          executedQty: ethers.parseUnits("2", 18),
        })
      )
    ).to.be.revertedWithCustomError(reg, "BadQuantities");

    // zero alts root
    await expect(
      reg.connect(casp1).issueReceipt(
        blankReceipt(casp1.address, { alternativesRoot: ethers.ZeroHash })
      )
    ).to.be.revertedWithCustomError(reg, "BadAlternatives");

    // zero venue
    await expect(
      reg.connect(casp1).issueReceipt(
        blankReceipt(casp1.address, { executedVenue: ethers.ZeroHash })
      )
    ).to.be.revertedWithCustomError(reg, "BadAlternatives");
  });

  it("caspIsActive false until first receipt issued", async () => {
    await reg.connect(foundation).registerCasp(casp1.address, LICENCE_REF);
    expect(await reg.caspIsActive(casp1.address)).to.equal(false);
    await reg.connect(casp1).issueReceipt(blankReceipt(casp1.address));
    expect(await reg.caspIsActive(casp1.address)).to.equal(true);
  });

  it("receiptIds for distinct CASPs do not collide", async () => {
    await reg.connect(foundation).registerCasp(casp1.address, LICENCE_REF);
    await reg.connect(foundation).registerCasp(casp2.address, LICENCE_REF);
    const id1 = await reg.nextReceiptId(casp1.address);
    const id2 = await reg.nextReceiptId(casp2.address);
    expect(id1).to.not.equal(id2);
  });
});
