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

describe("DelegationRegistry", function () {
  let identity: MockIdentityRegistry;
  let bonds: MockBondsRegistry;
  let reputation: MockReputationRegistry;
  let registry: DelegationRegistry;
  let delegator: SignerWithAddress;
  let delegate: SignerWithAddress;
  let random: SignerWithAddress;

  const MAX_VALUE = ethers.parseEther("1");
  const TOTAL_LIMIT = ethers.parseEther("10");
  const DURATION = 86400; // 24 hours
  const MIN_BOND = ethers.parseEther("0.5");
  const MIN_REPUTATION = 50;
  const KEY_HASH = ethers.keccak256(ethers.toUtf8Bytes("encrypted-session-key-ref"));

  beforeEach(async function () {
    [delegator, delegate, random] = await ethers.getSigners();

    const IdentityFactory = await ethers.getContractFactory("MockIdentityRegistry");
    identity = (await IdentityFactory.deploy()) as MockIdentityRegistry;
    await identity.register(delegator.address);
    await identity.register(delegate.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 RegistryFactory = await ethers.getContractFactory("DelegationRegistry");
    registry = (await RegistryFactory.deploy(
      await identity.getAddress(),
      await bonds.getAddress(),
      await reputation.getAddress()
    )) as DelegationRegistry;
  });

  // ═══════════════════════════════════════════════════
  // DELEGATION CREATION
  // ═══════════════════════════════════════════════════

  describe("Delegation Creation", function () {
    it("Should create a delegation between two registered agents", async function () {
      const tx = await registry.createDelegation(
        delegate.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        MIN_BOND,
        MIN_REPUTATION,
        [], // no contract restrictions
        [], // no function restrictions
        KEY_HASH
      );
      await expect(tx).to.emit(registry, "DelegationCreated");

      const d = await registry.getDelegation(0);
      expect(d.delegator).to.equal(delegator.address);
      expect(d.delegate).to.equal(delegate.address);
      expect(d.maxValue).to.equal(MAX_VALUE);
      expect(d.totalLimit).to.equal(TOTAL_LIMIT);
      expect(d.totalSpent).to.equal(0);
      expect(d.minBondRequired).to.equal(MIN_BOND);
      expect(d.minReputation).to.equal(MIN_REPUTATION);
      expect(d.revoked).to.equal(false);
      expect(d.accepted).to.equal(false);
      expect(d.hasContractAllowlist).to.equal(false);
      expect(d.hasFunctionAllowlist).to.equal(false);
      expect(d.encryptedKeyHash).to.equal(KEY_HASH);
    });

    it("Should reject unregistered delegator", async function () {
      await expect(
        registry.connect(random).createDelegation(
          delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
        )
      ).to.be.revertedWithCustomError(registry, "NotRegistered");
    });

    it("Should reject unregistered delegate", async function () {
      await expect(
        registry.createDelegation(
          random.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
        )
      ).to.be.revertedWithCustomError(registry, "NotRegistered");
    });

    it("Should reject self-delegation", async function () {
      await expect(
        registry.createDelegation(
          delegator.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
        )
      ).to.be.revertedWithCustomError(registry, "CannotDelegateToSelf");
    });

    it("Should reject zero duration", async function () {
      await expect(
        registry.createDelegation(
          delegate.address, MAX_VALUE, TOTAL_LIMIT, 0, 0, 0, [], [], KEY_HASH
        )
      ).to.be.revertedWithCustomError(registry, "InvalidDuration");
    });

    it("Should set contract allowlist flag when contracts provided", async function () {
      await registry.createDelegation(
        delegate.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        0,
        0,
        [random.address],
        [],
        KEY_HASH
      );
      const d = await registry.getDelegation(0);
      expect(d.hasContractAllowlist).to.equal(true);
      expect(d.hasFunctionAllowlist).to.equal(false);
    });

    it("Should set function allowlist flag when selectors provided", async function () {
      const transferSelector = "0xa9059cbb"; // transfer(address,uint256)
      await registry.createDelegation(
        delegate.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        0,
        0,
        [],
        [transferSelector],
        KEY_HASH
      );
      const d = await registry.getDelegation(0);
      expect(d.hasContractAllowlist).to.equal(false);
      expect(d.hasFunctionAllowlist).to.equal(true);
    });

    it("Should track delegation IDs for both parties", async function () {
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );
      const delegatorIds = await registry.getDelegatorDelegations(delegator.address);
      const delegateIds = await registry.getDelegateDelegations(delegate.address);
      expect(delegatorIds.length).to.equal(1);
      expect(delegateIds.length).to.equal(1);
    });
  });

  // ═══════════════════════════════════════════════════
  // DELEGATION ACCEPTANCE WITH BOND + REPUTATION GATING
  // ═══════════════════════════════════════════════════

  describe("Acceptance with Bond + Reputation Gating", function () {
    let bondId: bigint;

    beforeEach(async function () {
      // Set up delegate's bond and reputation
      const tx = await bonds.createBond(delegate.address, ethers.parseEther("1"));
      const receipt = await tx.wait();
      bondId = 0n; // first bond created

      await reputation.setReputation(delegate.address, 80, 100);

      // Create delegation requiring bond and reputation
      await registry.createDelegation(
        delegate.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        MIN_BOND,
        MIN_REPUTATION,
        [],
        [],
        KEY_HASH
      );
    });

    it("Should accept delegation when bond + reputation meet requirements", async function () {
      await expect(
        registry.connect(delegate).acceptDelegation(0, bondId)
      ).to.emit(registry, "DelegationAccepted");

      const d = await registry.getDelegation(0);
      expect(d.accepted).to.equal(true);
    });

    it("Should reject acceptance with insufficient bond stake", async function () {
      // Lower the bond below requirement
      await bonds.setBondStake(bondId, ethers.parseEther("0.1"));

      await expect(
        registry.connect(delegate).acceptDelegation(0, bondId)
      ).to.be.revertedWithCustomError(registry, "InsufficientBond");
    });

    it("Should reject acceptance with inactive bond", async function () {
      await bonds.deactivateBond(bondId);

      await expect(
        registry.connect(delegate).acceptDelegation(0, bondId)
      ).to.be.revertedWithCustomError(registry, "InsufficientBond");
    });

    it("Should reject acceptance with wrong bond owner", async function () {
      // Create a bond owned by someone else
      await bonds.createBond(random.address, ethers.parseEther("10"));
      const wrongBondId = 1n;

      await expect(
        registry.connect(delegate).acceptDelegation(0, wrongBondId)
      ).to.be.revertedWithCustomError(registry, "InsufficientBond");
    });

    it("Should reject acceptance with insufficient reputation", async function () {
      await reputation.setReputation(delegate.address, 30, 100); // below 50

      await expect(
        registry.connect(delegate).acceptDelegation(0, bondId)
      ).to.be.revertedWithCustomError(registry, "InsufficientReputation");
    });

    it("Should reject acceptance by wrong delegate", async function () {
      await expect(
        registry.connect(random).acceptDelegation(0, bondId)
      ).to.be.revertedWithCustomError(registry, "NotDelegate");
    });

    it("Should reject double acceptance", async function () {
      await registry.connect(delegate).acceptDelegation(0, bondId);
      await expect(
        registry.connect(delegate).acceptDelegation(0, bondId)
      ).to.be.revertedWithCustomError(registry, "AlreadyAccepted");
    });

    it("Should reject acceptance after revocation", async function () {
      await registry.revokeDelegation(0);
      await expect(
        registry.connect(delegate).acceptDelegation(0, bondId)
      ).to.be.revertedWithCustomError(registry, "DelegationRevoked_");
    });

    it("Should reject acceptance after expiry", async function () {
      await time.increase(DURATION + 1);
      await expect(
        registry.connect(delegate).acceptDelegation(0, bondId)
      ).to.be.revertedWithCustomError(registry, "DelegationExpired");
    });

    it("Should store bondId for re-verification", async function () {
      await registry.connect(delegate).acceptDelegation(0, bondId);
      expect(await registry.delegationBondIds(0)).to.equal(bondId);
    });

    it("Should accept delegation with zero bond requirement", async function () {
      // Create delegation with no bond/rep requirements
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );
      // Should accept with any bondId (including 0)
      await expect(
        registry.connect(delegate).acceptDelegation(1, 0)
      ).to.emit(registry, "DelegationAccepted");
    });
  });

  // ═══════════════════════════════════════════════════
  // DELEGATION VALIDATION
  // ═══════════════════════════════════════════════════

  describe("Delegation Validation", function () {
    let bondId: bigint;
    const targetContract = "0x1111111111111111111111111111111111111111";
    const transferSelector = "0xa9059cbb";

    beforeEach(async function () {
      await bonds.createBond(delegate.address, ethers.parseEther("1"));
      bondId = 0n;
      await reputation.setReputation(delegate.address, 80, 100);

      // Create delegation with allowlists
      await registry.createDelegation(
        delegate.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        MIN_BOND,
        MIN_REPUTATION,
        [targetContract],
        [transferSelector],
        KEY_HASH
      );

      // Accept it
      await registry.connect(delegate).acceptDelegation(0, bondId);
    });

    it("Should validate valid delegation usage", async function () {
      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.5"), transferSelector)
      ).to.emit(registry, "DelegationUsed");
    });

    it("Should track spending correctly", async function () {
      await registry.validateDelegation(0, targetContract, ethers.parseEther("1"), transferSelector);
      expect(await registry.delegationRemaining(0)).to.equal(ethers.parseEther("9"));
    });

    it("Should reject value exceeding per-tx limit", async function () {
      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("1.5"), transferSelector)
      ).to.be.revertedWithCustomError(registry, "ExceedsDelegationValueLimit");
    });

    it("Should reject exceeding total limit", async function () {
      // Spend the full 10 ETH
      for (let i = 0; i < 10; i++) {
        await registry.validateDelegation(0, targetContract, MAX_VALUE, transferSelector);
      }
      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.01"), transferSelector)
      ).to.be.revertedWithCustomError(registry, "ExceedsDelegationTotalLimit");
    });

    it("Should enforce contract allowlist", async function () {
      const wrongContract = "0x2222222222222222222222222222222222222222";
      await expect(
        registry.validateDelegation(0, wrongContract, ethers.parseEther("0.1"), transferSelector)
      ).to.be.revertedWithCustomError(registry, "ContractNotAllowedInDelegation");
    });

    it("Should enforce function selector allowlist", async function () {
      const wrongSelector = "0x12345678";
      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.1"), wrongSelector)
      ).to.be.revertedWithCustomError(registry, "FunctionNotAllowedInDelegation");
    });

    it("Should allow any contract when no allowlist set", async function () {
      // Create delegation with no allowlists
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );
      await registry.connect(delegate).acceptDelegation(1, 0);

      const anyContract = "0x3333333333333333333333333333333333333333";
      const anySelector = "0xdeadbeef";
      await expect(
        registry.validateDelegation(1, anyContract, ethers.parseEther("0.1"), anySelector)
      ).to.emit(registry, "DelegationUsed");
    });

    it("Should reject usage of unaccepted delegation", async function () {
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );
      // Don't accept it
      await expect(
        registry.validateDelegation(1, targetContract, ethers.parseEther("0.1"), transferSelector)
      ).to.be.revertedWithCustomError(registry, "DelegationNotAccepted");
    });

    it("Should reject usage after revocation", async function () {
      await registry.revokeDelegation(0);
      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.1"), transferSelector)
      ).to.be.revertedWithCustomError(registry, "DelegationRevoked_");
    });

    it("Should reject usage after expiry", async function () {
      await time.increase(DURATION + 1);
      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.1"), transferSelector)
      ).to.be.revertedWithCustomError(registry, "DelegationExpired");
    });
  });

  // ═══════════════════════════════════════════════════
  // TRUST RE-VERIFICATION (Auto-revoke on bond/rep drop)
  // ═══════════════════════════════════════════════════

  describe("Trust Re-Verification", function () {
    let bondId: bigint;
    const targetContract = "0x1111111111111111111111111111111111111111";
    const selector = "0xa9059cbb";

    beforeEach(async function () {
      await bonds.createBond(delegate.address, ethers.parseEther("1"));
      bondId = 0n;
      await reputation.setReputation(delegate.address, 80, 100);

      await registry.createDelegation(
        delegate.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        MIN_BOND,
        MIN_REPUTATION,
        [],
        [],
        KEY_HASH
      );
      await registry.connect(delegate).acceptDelegation(0, bondId);
    });

    it("Should auto-revoke when bond stake drops below requirement", async function () {
      // First validation works fine
      await registry.validateDelegation(0, targetContract, ethers.parseEther("0.1"), selector);

      // Drop bond below threshold
      await bonds.setBondStake(bondId, ethers.parseEther("0.1"));

      // Call verifyTrust — this commits the revocation without reverting
      const result = await registry.verifyTrust.staticCall(0);
      expect(result).to.equal(false);
      await registry.verifyTrust(0); // persist the state change

      // Delegation should be revoked permanently
      expect(await registry.isDelegationValid(0)).to.equal(false);
      const d = await registry.getDelegation(0);
      expect(d.revoked).to.equal(true);

      // Subsequent validateDelegation should revert immediately
      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.1"), selector)
      ).to.be.revertedWithCustomError(registry, "DelegationRevoked_");
    });

    it("Should auto-revoke when bond is deactivated", async function () {
      await bonds.deactivateBond(bondId);

      await registry.verifyTrust(0); // persist revocation
      expect(await registry.isDelegationValid(0)).to.equal(false);

      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.1"), selector)
      ).to.be.revertedWithCustomError(registry, "DelegationRevoked_");
    });

    it("Should auto-revoke when reputation drops below threshold", async function () {
      await reputation.setReputation(delegate.address, 20, 100);

      await registry.verifyTrust(0); // persist revocation
      expect(await registry.isDelegationValid(0)).to.equal(false);

      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.1"), selector)
      ).to.be.revertedWithCustomError(registry, "DelegationRevoked_");
    });

    it("Should emit TrustViolation event on auto-revoke", async function () {
      await bonds.setBondStake(bondId, ethers.parseEther("0.1"));

      await expect(
        registry.verifyTrust(0)
      ).to.emit(registry, "TrustViolation");
    });

    it("Should still work if bond and reputation remain above thresholds", async function () {
      // Slightly above thresholds
      await bonds.setBondStake(bondId, MIN_BOND);
      await reputation.setReputation(delegate.address, 50, 100);

      // Should still work
      await expect(
        registry.validateDelegation(0, targetContract, ethers.parseEther("0.1"), selector)
      ).to.emit(registry, "DelegationUsed");
    });
  });

  // ═══════════════════════════════════════════════════
  // REVOCATION
  // ═══════════════════════════════════════════════════

  describe("Revocation", function () {
    beforeEach(async function () {
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );
    });

    it("Should allow delegator to revoke", async function () {
      await expect(registry.revokeDelegation(0))
        .to.emit(registry, "DelegationRevoked");
      expect(await registry.isDelegationValid(0)).to.equal(false);
    });

    it("Should reject revocation from non-delegator", async function () {
      await expect(
        registry.connect(delegate).revokeDelegation(0)
      ).to.be.revertedWithCustomError(registry, "NotDelegator");
    });

    it("Should revoke all delegations at once", async function () {
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );

      await expect(registry.revokeAllDelegations())
        .to.emit(registry, "AllDelegationsRevoked");

      expect(await registry.isDelegationValid(0)).to.equal(false);
      expect(await registry.isDelegationValid(1)).to.equal(false);
    });

    it("Should return 0 remaining after revocation", async function () {
      await registry.connect(delegate).acceptDelegation(0, 0);
      await registry.revokeDelegation(0);
      expect(await registry.delegationRemaining(0)).to.equal(0);
    });
  });

  // ═══════════════════════════════════════════════════
  // VIEW FUNCTIONS
  // ═══════════════════════════════════════════════════

  describe("View Functions", function () {
    it("Should return correct validity for active delegation", async function () {
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );
      await registry.connect(delegate).acceptDelegation(0, 0);
      expect(await registry.isDelegationValid(0)).to.equal(true);
    });

    it("Should return false for expired delegation", async function () {
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );
      await registry.connect(delegate).acceptDelegation(0, 0);
      await time.increase(DURATION + 1);
      expect(await registry.isDelegationValid(0)).to.equal(false);
    });

    it("Should return delegation count", async function () {
      expect(await registry.delegationCount()).to.equal(0);
      await registry.createDelegation(
        delegate.address, MAX_VALUE, TOTAL_LIMIT, DURATION, 0, 0, [], [], KEY_HASH
      );
      expect(await registry.delegationCount()).to.equal(1);
    });
  });
});
