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

describe("AereAttestationGateway", () => {
  let foundation, bafin, finma, subject1, outsider;
  let gw;
  const SCHEMA_MICA = ethers.keccak256(ethers.toUtf8Bytes("mica-casp-v1"));
  const SCHEMA_AIACT = ethers.keccak256(ethers.toUtf8Bytes("ai-act-annex3-v1"));
  const SPEC_HASH_MICA = ethers.keccak256(ethers.toUtf8Bytes("mica-spec-bytes"));
  const SPEC_HASH_AI = ethers.keccak256(ethers.toUtf8Bytes("ai-act-spec-bytes"));
  const SUBJECT_A = ethers.keccak256(ethers.toUtf8Bytes("subject:exchange-A"));
  const PAYLOAD_HASH = ethers.keccak256(ethers.toUtf8Bytes("payload-bytes"));

  beforeEach(async () => {
    [foundation, bafin, finma, subject1, outsider] = await ethers.getSigners();
    const F = await ethers.getContractFactory("AereAttestationGateway");
    gw = await F.deploy(foundation.address);
    await gw.waitForDeployment();
  });

  async function nowTs() {
    return (await ethers.provider.getBlock("latest")).timestamp;
  }

  it("only Foundation publishes schemas; cannot republish", async () => {
    await expect(
      gw.connect(outsider).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "mica-casp-v1", "1.0.0", "ipfs://")
    ).to.be.revertedWithCustomError(gw, "NotFoundation");
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "mica-casp-v1", "1.0.0", "ipfs://");
    expect((await gw.schemas(SCHEMA_MICA)).exists).to.equal(true);
    await expect(
      gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "x", "y", "z")
    ).to.be.revertedWithCustomError(gw, "SchemaExists");
    await expect(
      gw.connect(foundation).publishSchema(SCHEMA_AIACT, ethers.ZeroHash, "n", "v", "u")
    ).to.be.revertedWithCustomError(gw, "EmptySpec");
  });

  it("rejects attest from non-authorised attestor + unknown schema", async () => {
    await expect(
      gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, 0, 0, "")
    ).to.be.revertedWithCustomError(gw, "SchemaUnknown");
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await expect(
      gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, 0, 0, "")
    ).to.be.revertedWithCustomError(gw, "NotAuthorisedAttestor");
  });

  it("authorise + attest emits + records + latestFor + subjectCurrentlyAttested", async () => {
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, true);
    const t = await nowTs();
    await expect(
      gw.connect(bafin).attest(
        SCHEMA_MICA, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, t, t + 3600, "ipfs://x"
      )
    ).to.emit(gw, "Attested");
    const id = await gw.latestFor(SUBJECT_A, SCHEMA_MICA);
    const a = await gw.attestationOf(id);
    expect(a.attestor).to.equal(bafin.address);
    expect(a.payloadHash).to.equal(PAYLOAD_HASH);
    expect(await gw.isValidNow(id)).to.equal(true);
    expect(await gw.subjectCurrentlyAttested(SUBJECT_A, SCHEMA_MICA)).to.equal(true);
  });

  it("validity window — past validUntil is invalid", async () => {
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, true);
    const t = await nowTs();
    await gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, t, t + 10, "");
    await ethers.provider.send("evm_increaseTime", [20]);
    await ethers.provider.send("evm_mine", []);
    const id = await gw.latestFor(SUBJECT_A, SCHEMA_MICA);
    expect(await gw.isValidNow(id)).to.equal(false);
  });

  it("bad validity window (validUntil < validFrom) rejected", async () => {
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, true);
    const t = await nowTs();
    await expect(
      gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, t + 100, t, "")
    ).to.be.revertedWithCustomError(gw, "BadValidityWindow");
  });

  it("revocation: only issuer; immediate invalidity; cannot revoke twice", async () => {
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, true);
    const t = await nowTs();
    await gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, t, 0, "");
    const id = await gw.latestFor(SUBJECT_A, SCHEMA_MICA);

    await expect(gw.connect(outsider).revoke(id)).to.be.revertedWithCustomError(gw, "NotIssuer");
    await gw.connect(bafin).revoke(id);
    expect(await gw.isValidNow(id)).to.equal(false);
    await expect(gw.connect(bafin).revoke(id)).to.be.revertedWithCustomError(gw, "AlreadyRevoked");
  });

  it("inheritance: child invalid if parent revoked OR expired", async () => {
    // KYB schema by Foundation; child = best-ex schema by BAFIN.
    const SCHEMA_KYB = ethers.keccak256(ethers.toUtf8Bytes("foundation-kyb-v1"));
    await gw.connect(foundation).publishSchema(SCHEMA_KYB, SPEC_HASH_MICA, "kyb", "1", "u");
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await gw.connect(foundation).setAttestor(SCHEMA_KYB, foundation.address, true);
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, true);

    const t = await nowTs();
    await gw.connect(foundation).attest(SCHEMA_KYB, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, t, 0, "");
    const parent = await gw.latestFor(SUBJECT_A, SCHEMA_KYB);

    await gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, parent, PAYLOAD_HASH, t, 0, "");
    const child = await gw.latestFor(SUBJECT_A, SCHEMA_MICA);

    expect(await gw.isValidNow(child)).to.equal(true);

    // Revoke parent → child fails.
    await gw.connect(foundation).revoke(parent);
    expect(await gw.isValidNow(child)).to.equal(false);
  });

  it("inheritance: bad parent rejected at attest time", async () => {
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, true);
    const fakeParent = ethers.keccak256(ethers.toUtf8Bytes("nonexistent"));
    await expect(
      gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, fakeParent, PAYLOAD_HASH, 0, 0, "")
    ).to.be.revertedWithCustomError(gw, "ParentInvalid");
  });

  it("inheritance: parent must match subject (audit fix HIGH #23)", async () => {
    const SCHEMA_KYB = ethers.keccak256(ethers.toUtf8Bytes("kyb"));
    await gw.connect(foundation).publishSchema(SCHEMA_KYB, SPEC_HASH_MICA, "kyb", "1", "u");
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await gw.connect(foundation).setAttestor(SCHEMA_KYB, foundation.address, true);
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, true);
    const SUBJECT_B = ethers.keccak256(ethers.toUtf8Bytes("subject:exchange-B"));
    // KYB issued for SUBJECT_B (not SUBJECT_A).
    await gw.connect(foundation).attest(SCHEMA_KYB, SUBJECT_B, ethers.ZeroHash, PAYLOAD_HASH, 0, 0, "");
    const parent = await gw.latestFor(SUBJECT_B, SCHEMA_KYB);
    // Trying to chain that parent for SUBJECT_A → rejected.
    await expect(
      gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, parent, PAYLOAD_HASH, 0, 0, "")
    ).to.be.revertedWithCustomError(gw, "ParentSubjectMismatch");
  });

  it("toggling attestor off does not retro-invalidate past attestations", async () => {
    await gw.connect(foundation).publishSchema(SCHEMA_MICA, SPEC_HASH_MICA, "n", "v", "u");
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, true);
    const t = await nowTs();
    await gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, t, 0, "");
    const id = await gw.latestFor(SUBJECT_A, SCHEMA_MICA);

    // De-authorise BAFIN going forward.
    await gw.connect(foundation).setAttestor(SCHEMA_MICA, bafin.address, false);
    // Past attestation is still valid (only revocation by issuer kills it).
    expect(await gw.isValidNow(id)).to.equal(true);

    // But BAFIN can no longer issue new attestations under this schema.
    await expect(
      gw.connect(bafin).attest(SCHEMA_MICA, SUBJECT_A, ethers.ZeroHash, PAYLOAD_HASH, t, 0, "")
    ).to.be.revertedWithCustomError(gw, "NotAuthorisedAttestor");
  });
});
