import { expect } from "chai";
import { ethers } from "hardhat";
import { time } from "@nomicfoundation/hardhat-network-helpers";
import {
  ScopedDelegation,
  MockIdentityRegistry,
  MockBondsRegistry,
  MockReputationRegistry,
} from "../typechain-types";
import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers";

describe("ScopedDelegation", function () {
  let identity: MockIdentityRegistry;
  let bonds: MockBondsRegistry;
  let reputation: MockReputationRegistry;
  let scoped: ScopedDelegation;
  let owner: SignerWithAddress;
  let agentA: SignerWithAddress;
  let agentB: SignerWithAddress;
  let agentC: SignerWithAddress;
  let random: SignerWithAddress;

  const MAX_PER_TX = ethers.parseEther("1");
  const MAX_TOTAL = ethers.parseEther("10");
  const MAX_TX_COUNT = 100;
  const RATE_LIMIT = 60; // 60 seconds
  const DURATION = 30 * 24 * 3600; // 30 days

  beforeEach(async function () {
    [owner, agentA, agentB, agentC, random] = await ethers.getSigners();

    const IdentityFactory = await ethers.getContractFactory("MockIdentityRegistry");
    identity = (await IdentityFactory.deploy()) as MockIdentityRegistry;
    await identity.register(agentA.address);
    await identity.register(agentB.address);
    await identity.register(agentC.address);

    const BondsFactory = await ethers.getContractFactory("MockBondsRegistry");
    bonds = (await BondsFactory.deploy()) as MockBondsRegistry;

    const ReputationFactory = await ethers.getContractFactory("MockReputationRegistry");
    reputation = (await ReputationFactory.deploy()) as MockReputationRegistry;

    const ScopedFactory = await ethers.getContractFactory("ScopedDelegation");
    scoped = (await ScopedFactory.deploy(
      await identity.getAddress(),
      await bonds.getAddress(),
      await reputation.getAddress()
    )) as ScopedDelegation;
  });

  describe("Deployment", function () {
    it("Should deploy with correct addresses", async function () {
      expect(await scoped.identityRegistry()).to.equal(await identity.getAddress());
      expect(await scoped.owner()).to.equal(owner.address);
    });
  });

  describe("Scope Creation", function () {
    it("Should create a scope with correct parameters", async function () {
      const tx = await scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      );
      await expect(tx).to.emit(scoped, "ScopeCreated");

      const scope = await scoped.scopes(0);
      expect(scope.grantor).to.equal(agentA.address);
      expect(scope.grantee).to.equal(agentB.address);
      expect(scope.maxValuePerTx).to.equal(MAX_PER_TX);
      expect(scope.active).to.be.true;
    });

    it("Should reject self-delegation", async function () {
      await expect(scoped.connect(agentA).createScope(
        agentA.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      )).to.be.reverted;
    });

    it("Should reject unregistered agents", async function () {
      await expect(scoped.connect(random).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      )).to.be.reverted;
    });

    it("Should reject invalid duration", async function () {
      await expect(scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, 0
      )).to.be.reverted;
    });
  });

  describe("Scope Usage", function () {
    beforeEach(async function () {
      await scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      );
    });

    it("Should validate and record usage", async function () {
      const tx = await scoped.connect(agentB).useScope(0, ethers.parseEther("0.5"), ethers.ZeroAddress, "0x00000000");
      await expect(tx).to.emit(scoped, "ScopeUsed");

      const scope = await scoped.scopes(0);
      expect(scope.totalSpent).to.equal(ethers.parseEther("0.5"));
      expect(scope.txCount).to.equal(1);
    });

    it("Should reject value exceeding per-tx limit", async function () {
      await expect(scoped.connect(agentB).useScope(
        0, ethers.parseEther("2"), ethers.ZeroAddress, "0x00000000"
      )).to.be.reverted;
    });

    it("Should reject when total limit exceeded", async function () {
      // Use up most of the limit
      for (let i = 0; i < 10; i++) {
        await scoped.connect(agentB).useScope(0, ethers.parseEther("1"), ethers.ZeroAddress, "0x00000000");
        await time.increase(RATE_LIMIT + 1);
      }
      // Next one should fail
      await time.increase(RATE_LIMIT + 1);
      await expect(scoped.connect(agentB).useScope(
        0, ethers.parseEther("1"), ethers.ZeroAddress, "0x00000000"
      )).to.be.reverted;
    });

    it("Should enforce rate limiting", async function () {
      await scoped.connect(agentB).useScope(0, ethers.parseEther("0.1"), ethers.ZeroAddress, "0x00000000");
      // Try immediately again — should fail
      await expect(scoped.connect(agentB).useScope(
        0, ethers.parseEther("0.1"), ethers.ZeroAddress, "0x00000000"
      )).to.be.reverted;

      // Wait and try again — should succeed
      await time.increase(RATE_LIMIT + 1);
      await scoped.connect(agentB).useScope(0, ethers.parseEther("0.1"), ethers.ZeroAddress, "0x00000000");
    });

    it("Should reject usage by non-grantee", async function () {
      await expect(scoped.connect(agentA).useScope(
        0, ethers.parseEther("0.5"), ethers.ZeroAddress, "0x00000000"
      )).to.be.reverted;
    });

    it("Should reject usage of expired scope", async function () {
      await time.increase(DURATION + 1);
      await expect(scoped.connect(agentB).useScope(
        0, ethers.parseEther("0.5"), ethers.ZeroAddress, "0x00000000"
      )).to.be.reverted;
    });
  });

  describe("Sub-delegation (Chain)", function () {
    beforeEach(async function () {
      await scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      );
    });

    it("Should create sub-delegation with narrower scope", async function () {
      const tx = await scoped.connect(agentB).subDelegate(
        0, agentC.address,
        ethers.parseEther("0.5"), // narrower per-tx
        ethers.parseEther("5"),   // narrower total
        50,                        // narrower tx count
        120,                       // more restrictive rate limit
        15 * 24 * 3600             // shorter duration
      );
      await expect(tx).to.emit(scoped, "ScopeCreated");

      const scope = await scoped.scopes(1);
      expect(scope.grantor).to.equal(agentB.address);
      expect(scope.grantee).to.equal(agentC.address);
      expect(scope.maxValuePerTx).to.equal(ethers.parseEther("0.5"));
    });

    it("Should reject wider sub-delegation (maxValuePerTx)", async function () {
      await expect(scoped.connect(agentB).subDelegate(
        0, agentC.address,
        ethers.parseEther("2"), // WIDER than parent
        ethers.parseEther("5"), 50, 120, 15 * 24 * 3600
      )).to.be.reverted;
    });

    it("Should reject less restrictive rate limit", async function () {
      await expect(scoped.connect(agentB).subDelegate(
        0, agentC.address,
        ethers.parseEther("0.5"), ethers.parseEther("5"), 50,
        30, // LESS restrictive than parent's 60
        15 * 24 * 3600
      )).to.be.reverted;
    });

    it("Should reject longer duration than parent remaining", async function () {
      await expect(scoped.connect(agentB).subDelegate(
        0, agentC.address,
        ethers.parseEther("0.5"), ethers.parseEther("5"), 50, 120,
        DURATION + 1 // LONGER than parent
      )).to.be.reverted;
    });
  });

  describe("Contract Allowlists", function () {
    beforeEach(async function () {
      await scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      );
    });

    it("Should enforce contract allowlist", async function () {
      const allowedContract = agentC.address;
      await scoped.connect(agentA).setContractAllowlist(0, allowedContract, true);

      // Should succeed with allowed contract
      await scoped.connect(agentB).useScope(0, ethers.parseEther("0.1"), allowedContract, "0x00000000");

      // Should fail with non-allowed contract
      await time.increase(RATE_LIMIT + 1);
      await expect(scoped.connect(agentB).useScope(
        0, ethers.parseEther("0.1"), random.address, "0x00000000"
      )).to.be.reverted;
    });
  });

  describe("Scope Revocation", function () {
    it("Should revoke scope", async function () {
      await scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      );
      await scoped.connect(agentA).revokeScope(0);
      expect(await scoped.isScopeValid(0)).to.be.false;
    });

    it("Should reject revocation by non-grantor", async function () {
      await scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      );
      await expect(scoped.connect(random).revokeScope(0)).to.be.reverted;
    });
  });

  describe("View Functions", function () {
    it("Should return remaining allowance", async function () {
      await scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      );
      const [value, txs, duration_] = await scoped.getScopeRemaining(0);
      expect(value).to.equal(MAX_TOTAL);
      expect(txs).to.equal(MAX_TX_COUNT);
    });

    it("Should return granted and received scopes", async function () {
      await scoped.connect(agentA).createScope(
        agentB.address, MAX_PER_TX, MAX_TOTAL, MAX_TX_COUNT, RATE_LIMIT, DURATION
      );
      const granted = await scoped.getGrantedScopes(agentA.address);
      const received = await scoped.getReceivedScopes(agentB.address);
      expect(granted.length).to.equal(1);
      expect(received.length).to.equal(1);
    });
  });
});
