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

describe("SessionKeyManager", function () {
  let identity: MockIdentityRegistry;
  let sessionMgr: SessionKeyManager;
  let owner: SignerWithAddress;
  let sessionAddr: SignerWithAddress;
  let target1: SignerWithAddress;
  let target2: SignerWithAddress;
  let random: SignerWithAddress;

  const MAX_VALUE = ethers.parseEther("1");
  const TOTAL_LIMIT = ethers.parseEther("5");
  const DURATION = 3600; // 1 hour

  beforeEach(async function () {
    [owner, sessionAddr, target1, target2, random] = await ethers.getSigners();

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

    const SessionFactory = await ethers.getContractFactory("SessionKeyManager");
    sessionMgr = (await SessionFactory.deploy(
      await identity.getAddress()
    )) as SessionKeyManager;
  });

  // ═══════════════════════════════════════════════════
  // SESSION CREATION
  // ═══════════════════════════════════════════════════

  describe("Session Creation", function () {
    it("Should create a session with correct parameters", async function () {
      const tx = await sessionMgr.createSession(
        sessionAddr.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        []
      );
      await expect(tx).to.emit(sessionMgr, "SessionCreated");

      const session = await sessionMgr.getSession(0);
      expect(session.owner).to.equal(owner.address);
      expect(session.sessionAddress).to.equal(sessionAddr.address);
      expect(session.maxValue).to.equal(MAX_VALUE);
      expect(session.totalLimit).to.equal(TOTAL_LIMIT);
      expect(session.totalSpent).to.equal(0);
      expect(session.revoked).to.equal(false);
      expect(session.allowAnyContract).to.equal(true);
    });

    it("Should create session with contract allowlist", async function () {
      await sessionMgr.createSession(
        sessionAddr.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        [target1.address]
      );

      const session = await sessionMgr.getSession(0);
      expect(session.allowAnyContract).to.equal(false);
      expect(await sessionMgr.isContractAllowed(0, target1.address)).to.equal(true);
      expect(await sessionMgr.isContractAllowed(0, target2.address)).to.equal(false);
    });

    it("Should reject unregistered caller", async function () {
      await expect(
        sessionMgr.connect(random).createSession(
          sessionAddr.address,
          MAX_VALUE,
          TOTAL_LIMIT,
          DURATION,
          []
        )
      ).to.be.revertedWithCustomError(sessionMgr, "NotRegistered");
    });

    it("Should reject zero session address", async function () {
      await expect(
        sessionMgr.createSession(
          ethers.ZeroAddress,
          MAX_VALUE,
          TOTAL_LIMIT,
          DURATION,
          []
        )
      ).to.be.revertedWithCustomError(sessionMgr, "InvalidSessionAddress");
    });

    it("Should reject zero duration", async function () {
      await expect(
        sessionMgr.createSession(
          sessionAddr.address,
          MAX_VALUE,
          TOTAL_LIMIT,
          0,
          []
        )
      ).to.be.revertedWithCustomError(sessionMgr, "InvalidDuration");
    });

    it("Should track session IDs per owner", async function () {
      await sessionMgr.createSession(sessionAddr.address, MAX_VALUE, TOTAL_LIMIT, DURATION, []);
      await sessionMgr.createSession(random.address, MAX_VALUE, TOTAL_LIMIT, DURATION, []);

      const ids = await sessionMgr.getOwnerSessions(owner.address);
      expect(ids.length).to.equal(2);
      expect(ids[0]).to.equal(0);
      expect(ids[1]).to.equal(1);
    });

    it("Should support reverse lookup from session address", async function () {
      await sessionMgr.createSession(sessionAddr.address, MAX_VALUE, TOTAL_LIMIT, DURATION, []);
      expect(await sessionMgr.sessionToId(sessionAddr.address)).to.equal(0);
    });
  });

  // ═══════════════════════════════════════════════════
  // SESSION VALIDATION
  // ═══════════════════════════════════════════════════

  describe("Session Validation", function () {
    beforeEach(async function () {
      await sessionMgr.createSession(
        sessionAddr.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        [target1.address]
      );
    });

    it("Should validate a valid session usage", async function () {
      await expect(
        sessionMgr.validateSession(0, target1.address, ethers.parseEther("0.5"))
      ).to.emit(sessionMgr, "SessionUsed");
    });

    it("Should reject value exceeding per-tx limit", async function () {
      await expect(
        sessionMgr.validateSession(0, target1.address, ethers.parseEther("1.5"))
      ).to.be.revertedWithCustomError(sessionMgr, "ExceedsSessionValueLimit");
    });

    it("Should reject value exceeding total limit", async function () {
      // Spend 1 ETH x 5 = 5 ETH (total limit)
      for (let i = 0; i < 5; i++) {
        await sessionMgr.validateSession(0, target1.address, MAX_VALUE);
      }
      // 6th should fail
      await expect(
        sessionMgr.validateSession(0, target1.address, ethers.parseEther("0.01"))
      ).to.be.revertedWithCustomError(sessionMgr, "ExceedsSessionTotalLimit");
    });

    it("Should track remaining allowance correctly", async function () {
      expect(await sessionMgr.sessionRemaining(0)).to.equal(TOTAL_LIMIT);
      await sessionMgr.validateSession(0, target1.address, ethers.parseEther("1"));
      expect(await sessionMgr.sessionRemaining(0)).to.equal(ethers.parseEther("4"));
    });

    it("Should reject non-allowed contract", async function () {
      await expect(
        sessionMgr.validateSession(0, target2.address, ethers.parseEther("0.5"))
      ).to.be.revertedWithCustomError(sessionMgr, "ContractNotAllowedInSession");
    });

    it("Should allow any contract when allowAnyContract is true", async function () {
      // Create a session with no contract restrictions
      await sessionMgr.createSession(
        random.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        [] // empty = allow any
      );

      await expect(
        sessionMgr.validateSession(1, target2.address, ethers.parseEther("0.5"))
      ).to.emit(sessionMgr, "SessionUsed");
    });
  });

  // ═══════════════════════════════════════════════════
  // SESSION EXPIRY
  // ═══════════════════════════════════════════════════

  describe("Session Expiry", function () {
    beforeEach(async function () {
      await sessionMgr.createSession(
        sessionAddr.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        []
      );
    });

    it("Should be valid before expiry", async function () {
      expect(await sessionMgr.isSessionValid(0)).to.equal(true);
    });

    it("Should be invalid after expiry", async function () {
      await time.increase(DURATION + 1);
      expect(await sessionMgr.isSessionValid(0)).to.equal(false);
    });

    it("Should reject validation after expiry", async function () {
      await time.increase(DURATION + 1);
      await expect(
        sessionMgr.validateSession(0, target1.address, ethers.parseEther("0.1"))
      ).to.be.revertedWithCustomError(sessionMgr, "SessionExpired");
    });

    it("Should return 0 remaining after expiry", async function () {
      await time.increase(DURATION + 1);
      expect(await sessionMgr.sessionRemaining(0)).to.equal(0);
    });
  });

  // ═══════════════════════════════════════════════════
  // SESSION REVOCATION
  // ═══════════════════════════════════════════════════

  describe("Session Revocation", function () {
    beforeEach(async function () {
      await sessionMgr.createSession(
        sessionAddr.address,
        MAX_VALUE,
        TOTAL_LIMIT,
        DURATION,
        []
      );
    });

    it("Should allow owner to revoke", async function () {
      await expect(sessionMgr.revokeSession(0))
        .to.emit(sessionMgr, "SessionRevoked");
      expect(await sessionMgr.isSessionValid(0)).to.equal(false);
    });

    it("Should reject revocation from non-owner", async function () {
      await expect(
        sessionMgr.connect(random).revokeSession(0)
      ).to.be.revertedWithCustomError(sessionMgr, "NotSessionOwner");
    });

    it("Should reject validation after revocation", async function () {
      await sessionMgr.revokeSession(0);
      await expect(
        sessionMgr.validateSession(0, target1.address, ethers.parseEther("0.1"))
      ).to.be.revertedWithCustomError(sessionMgr, "SessionRevoked_");
    });

    it("Should return 0 remaining after revocation", async function () {
      await sessionMgr.revokeSession(0);
      expect(await sessionMgr.sessionRemaining(0)).to.equal(0);
    });

    it("Should revoke all sessions at once", async function () {
      await sessionMgr.createSession(random.address, MAX_VALUE, TOTAL_LIMIT, DURATION, []);

      await expect(sessionMgr.revokeAllSessions())
        .to.emit(sessionMgr, "AllSessionsRevoked");

      expect(await sessionMgr.isSessionValid(0)).to.equal(false);
      expect(await sessionMgr.isSessionValid(1)).to.equal(false);
    });
  });
});
