import { time } from "@nomicfoundation/hardhat-network-helpers";
import { expect } from "chai";
import { ethers } from "hardhat";
import {
  approveEventPlace,
  cancelEvent,
  closeEvent,
  createAndCancelEvent,
  createAndCloseEvent,
  createAndUpdateEventPlace,
  createEvent,
  createEventForCategories,
  createEventPlace,
  declineEventPlace,
  deployContract,
  itExpectsOnlyLocalProvider,
  itExpectsOnlyMaster,
  itExpectsOnlyVerifiedShaman,
  loadFixture,
  submitEventPlaceRequest,
  submitEventRequest,
  timestamp,
} from "./helpers";

import {
  approveEventPlaceArgsToArray,
  baseEventArgsToArray,
  declineEventPlaceArgsToArray,
  submitEventPlaceRequestArgsToArray,
  updateEventPlaceArgsToArray,
} from "./types";

import {
  defaultCreateEventPlaceRequest,
  defaultEventDepositSize,
  defaultSubmitEventRequest,
  defaultUpdateEventPlaceRequest,
} from "./data";

const limitedCategory = {
  name: "Seed",
  discountPercentage: 0,
  quota: defaultCreateEventPlaceRequest.minTickets,
  hasQuota: true,
};

import {
  createEventPlaceCornerCases,
  getSubmitEventCases,
  getSubmitEventDateRangeOverlapCornerCases,
} from "./corner-cases";

describe("CyberValleyEventManager", () => {
  let testStartBlock: number;
  let provider: typeof ethers.provider;

  beforeEach(async () => {
    provider = ethers.provider;
    testStartBlock = await provider.getBlockNumber();
  });

  afterEach(async function () {
    const currentBlock = await provider.getBlockNumber();
    const txHashes: string[] = [];

    console.log(`\n${"=".repeat(80)}`);
    console.log(`TEST: ${this.currentTest?.title || "unknown"}`);
    console.log(`Blocks: ${testStartBlock + 1} to ${currentBlock}`);
    console.log(`${"=".repeat(80)}`);

    for (let i = testStartBlock + 1; i <= currentBlock; i++) {
      const block = await provider.getBlock(i, true);
      if (block && block.transactions.length > 0) {
        console.log(
          `\nBlock ${i} (${block.transactions.length} transactions):`,
        );
        for (const txHash of block.transactions) {
          const tx = await provider.getTransaction(txHash as string);
          if (tx) {
            const receipt = await provider.getTransactionReceipt(tx.hash);
            const eventCount = receipt?.logs.length || 0;
            console.log(`  โ€ข TX: ${tx.hash} (${eventCount} events)`);
            txHashes.push(tx.hash);
          }
        }
      }
    }

    console.log(`\nTotal transactions in test: ${txHashes.length}`);
    console.log(`${"=".repeat(80)}\n`);
  });

  async function createSubmittedEvent() {
    const {
      eventManager,
      ERC20,
      master,
      verifiedShaman,
      localProvider,
      creator,
      splitter,
    } = await loadFixture(deployContract);
    const VERIFIED_SHAMAN_ROLE = await eventManager.VERIFIED_SHAMAN_ROLE();
    await eventManager
      .connect(master)
      .grantRole(VERIFIED_SHAMAN_ROLE, await creator.getAddress());
    await ERC20.connect(creator).mint(defaultEventDepositSize);
    await ERC20.connect(creator).approve(
      await eventManager.getAddress(),
      defaultEventDepositSize,
    );
    const { eventPlaceId } = await createEventPlace(
      eventManager,
      verifiedShaman,
      localProvider,
      {},
    );
    const { getEventId } = await submitEventRequest(eventManager, creator, {
      eventPlaceId,
      startDate: await timestamp(10),
    });
    const eventId = await getEventId();
    return {
      eventManager,
      ERC20,
      verifiedShaman,
      localProvider,
      creator,
      splitter,
      eventId,
    };
  }
  describe("setRevenueSplitter", () => {
    itExpectsOnlyMaster("setRevenueSplitter", [ethers.ZeroAddress]);

    it("sets revenue splitter address", async () => {
      const { eventManager, master, splitter } =
        await loadFixture(deployContract);
      const newSplitter = await splitter.getAddress();
      await eventManager.connect(master).setRevenueSplitter(newSplitter);
      expect(await eventManager.revenueSplitter()).to.equal(newSplitter);
    });

    it("reverts when splitter is zero address", async () => {
      const { eventManager, master } = await loadFixture(deployContract);
      await expect(
        eventManager.connect(master).setRevenueSplitter(ethers.ZeroAddress),
      ).to.be.revertedWith("Splitter address cannot be zero");
    });
  });

  describe("submitEventPlaceRequest", () => {
    itExpectsOnlyVerifiedShaman(
      "submitEventPlaceRequest",
      submitEventPlaceRequestArgsToArray(defaultCreateEventPlaceRequest),
    );

    it("should emit NewEventPlaceRequest", async () => {
      const { eventManager, verifiedShaman } =
        await loadFixture(deployContract);
      const { tx } = await submitEventPlaceRequest(
        eventManager,
        verifiedShaman,
      );
      const args = submitEventPlaceRequestArgsToArray(
        defaultCreateEventPlaceRequest,
      );
      await expect(tx)
        .to.emit(eventManager, "NewEventPlaceRequest")
        .withArgs(0, await verifiedShaman.getAddress(), ...args.slice(0, 9));
    });

    it("auto-approves when requester is a local provider", async () => {
      const { eventManager, master, localProvider } =
        await loadFixture(deployContract);

      const VERIFIED_SHAMAN_ROLE = await eventManager.VERIFIED_SHAMAN_ROLE();
      await eventManager
        .connect(master)
        .grantRole(VERIFIED_SHAMAN_ROLE, await localProvider.getAddress());

      const tx = await eventManager
        .connect(localProvider)
        .submitEventPlaceRequest(
          ...submitEventPlaceRequestArgsToArray(defaultCreateEventPlaceRequest),
        );

      const args = submitEventPlaceRequestArgsToArray(
        defaultCreateEventPlaceRequest,
      );
      await expect(tx)
        .to.emit(eventManager, "EventPlaceUpdated")
        .withArgs(
          await localProvider.getAddress(),
          0,
          args[0],
          args[1],
          args[2],
          args[3],
          args[4],
          args[5],
          1,
          args[6],
          args[7],
          args[8],
          args[9],
        );

      const eventPlace = await eventManager.eventPlaces(0);
      expect(eventPlace.status).to.equal(1);
      expect(eventPlace.provider).to.equal(await localProvider.getAddress());
    });

    createEventPlaceCornerCases.forEach(({ patch, revertedWith }, idx) =>
      it(`should validate invariants. Case ${idx + 1}: ${JSON.stringify(patch)}`, async () => {
        const { eventManager, verifiedShaman } =
          await loadFixture(deployContract);
        await expect(
          submitEventPlaceRequest(eventManager, verifiedShaman, {
            ...defaultCreateEventPlaceRequest,
            ...patch,
          }),
        ).to.be.revertedWith(revertedWith);
      }),
    );
  });

  describe("approveEventPlace", () => {
    itExpectsOnlyLocalProvider(
      "approveEventPlace",
      approveEventPlaceArgsToArray({ eventPlaceId: 0, eventDepositSize: 100 }),
    );

    it("emits EventPlaceUpdated", async () => {
      const { eventManager, verifiedShaman, localProvider } =
        await loadFixture(deployContract);
      const { eventPlaceId } = await submitEventPlaceRequest(
        eventManager,
        verifiedShaman,
      );
      await expect(
        approveEventPlace(eventManager, localProvider, {
          eventPlaceId,
          eventDepositSize: 100,
        }),
      ).to.emit(eventManager, "EventPlaceUpdated");
    });

    it("reverts on non-existing event place", async () => {
      const { eventManager, localProvider } = await loadFixture(deployContract);
      await expect(
        approveEventPlace(eventManager, localProvider, {
          eventPlaceId: 999,
          eventDepositSize: 100,
        }),
      ).to.be.revertedWith("EventPlace does not exist");
    });

    it("reverts on already approved event place", async () => {
      const { eventManager, verifiedShaman, localProvider } =
        await loadFixture(deployContract);
      const { eventPlaceId } = await submitEventPlaceRequest(
        eventManager,
        verifiedShaman,
      );
      await approveEventPlace(eventManager, localProvider, {
        eventPlaceId,
        eventDepositSize: 100,
      });
      await expect(
        approveEventPlace(eventManager, localProvider, {
          eventPlaceId,
          eventDepositSize: 100,
        }),
      ).to.be.revertedWith("EventPlace status differs from submitted");
    });

    it("reverts when deposit is zero", async () => {
      const { eventManager, verifiedShaman, localProvider } =
        await loadFixture(deployContract);
      const { eventPlaceId } = await submitEventPlaceRequest(
        eventManager,
        verifiedShaman,
      );
      await expect(
        approveEventPlace(eventManager, localProvider, {
          eventPlaceId,
          eventDepositSize: 0,
        }),
      ).to.be.revertedWith("Deposit must be greater than zero");
    });
  });

  describe("declineEventPlace", () => {
    itExpectsOnlyLocalProvider(
      "declineEventPlace",
      declineEventPlaceArgsToArray({ eventPlaceId: 0 }),
    );

    it("reverts on non-existing event place", async () => {
      const { eventManager, localProvider } = await loadFixture(deployContract);
      await expect(
        declineEventPlace(eventManager, localProvider, { eventPlaceId: 999 }),
      ).to.be.revertedWith("EventPlace does not exist");
    });
  });

  describe("updateEventPlace", () => {
    itExpectsOnlyLocalProvider(
      "updateEventPlace",
      updateEventPlaceArgsToArray(defaultUpdateEventPlaceRequest),
    );

    it("should emit EventPlaceUpdated", async () => {
      const { eventManager, verifiedShaman, localProvider } =
        await loadFixture(deployContract);
      const tx = await createAndUpdateEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
        {},
      );
      const args = updateEventPlaceArgsToArray(defaultUpdateEventPlaceRequest);
      await expect(tx)
        .to.emit(eventManager, "EventPlaceUpdated")
        .withArgs(
          await localProvider.getAddress(),
          args[0], // eventPlaceId
          args[1], // maxTickets
          args[2], // minTickets
          args[3], // minPrice
          args[4], // daysBeforeCancel
          args[5], // minDays
          args[6], // available
          1, // status (Approved)
          args[7], // digest
          args[8], // hashFunction
          args[9], // size
          100, // eventDepositSize
        );
    });

    createEventPlaceCornerCases.forEach(({ patch, revertedWith }, idx) =>
      it(`should validate invariants. Case ${idx + 1}: ${JSON.stringify(patch)}`, async () => {
        const { eventManager, verifiedShaman, localProvider } =
          await loadFixture(deployContract);
        await expect(
          createAndUpdateEventPlace(
            eventManager,
            verifiedShaman,
            localProvider,
            {
              ...defaultUpdateEventPlaceRequest,
              ...patch,
            },
          ),
        ).to.be.revertedWith(revertedWith);
      }),
    );
  });

  describe("submitEventRequest", () => {
    it("emits NewEventRequest", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
      );
      const { request, tx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        {
          eventPlaceId,
        },
      );
      await expect(tx)
        .to.emit(eventManager, "NewEventRequest")
        .withArgs(
          await getEventId(),
          await creator.getAddress(),
          ...baseEventArgsToArray(request),
        );
    });

    it("transfers ERC20 token", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await expect(
        await ERC20.balanceOf(await eventManager.getAddress()),
      ).to.equal(0);
      const { eventPlaceId, tx: createEventPlaceTx } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
      );
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { tx } = await submitEventRequest(eventManager, creator, {
        eventPlaceId,
      });
      await expect(tx).to.changeTokenBalances(
        ERC20,
        [await eventManager.getAddress(), await creator.getAddress()],
        [defaultEventDepositSize, -defaultEventDepositSize],
      );
    });

    it("reverts on insufficient funds", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
        {
          ...defaultCreateEventPlaceRequest,
        },
      );
      const { tx } = await submitEventRequest(eventManager, creator, {
        eventPlaceId,
      });
      await expect(tx).to.be.revertedWith("Not enough tokens");
    });

    for (const idx of [0, 1, 2, 3, 4]) {
      it(`validation case ${idx + 1}`, async () => {
        const cases = await getSubmitEventCases(timestamp);
        const testCase = cases[idx];
        if (!testCase) return;
        const { eventPlacePatch, eventRequestPatch, revertsWith } = testCase;
        const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
          await loadFixture(deployContract);
        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );

        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
          eventPlacePatch,
        );
        const { tx } = await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          ...eventRequestPatch,
        });
        if (revertsWith == null) {
          await expect(tx).to.not.be.reverted;
        } else {
          await expect(tx).to.be.revertedWith(revertsWith);
        }
      });
    }

    for (const idx of [0, 1, 2]) {
      it(`reverts on overlap: Case ${idx}`, async () => {
        const cases =
          await getSubmitEventDateRangeOverlapCornerCases(timestamp);
        const { approvedEventPatch, submittedEventPatch } = cases[idx];
        const {
          eventManager,
          ERC20,
          verifiedShaman,
          localProvider,
          creator,
          splitter,
        } = await loadFixture(deployContract);
        const { tx: createEventTx } = await createEvent(
          eventManager,
          ERC20,
          verifiedShaman,
          localProvider,
          creator,
          {},
          approvedEventPatch,
          {},
          splitter,
        );
        await createEventTx;
        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );
        const { tx } = await submitEventRequest(
          eventManager,
          creator,
          submittedEventPatch,
        );
        await expect(tx).to.be.revertedWith(
          "Requested event overlaps with existing",
        );
      });
    }

    it("allows submit in a one-day gap between approved events", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);

      const { request: firstRequest } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        { startDate: await timestamp(11), daysAmount: 3 },
        {},
        splitter,
      );

      const eventPlaceId = firstRequest.eventPlaceId;

      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { tx: secondSubmitTx, getEventId: getSecondEventId } =
        await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(15),
          daysAmount: 3,
        });
      await secondSubmitTx;
      const secondEventId = await getSecondEventId();
      await expect(
        eventManager.connect(localProvider).approveEvent(secondEventId, 1n),
      ).to.not.be.reverted;

      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { tx: gapEventTx } = await submitEventRequest(eventManager, creator, {
        eventPlaceId,
        startDate: await timestamp(14),
        daysAmount: 1,
      });
      await expect(gapEventTx).to.not.be.reverted;
    });
  });

  describe("approveEvent", () => {
    itExpectsOnlyLocalProvider("approveEvent", [BigInt(0), BigInt(1)]);

    it("emits EventStatusChanged", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { request, tx, eventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        {},
        splitter,
      );
      await expect(tx)
        .to.emit(eventManager, "EventStatusChanged")
        .withArgs(eventId, 1);
    });

    it("reverts on unexisting event request", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { tx } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        { eventId: BigInt(1000 + Math.floor(Math.random() * 1000)) },
        splitter,
      );
      await expect(tx).to.be.revertedWith("Event with given id does not exist");
    });

    it("reverts when event has no categories", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
        {},
      );
      const tx = eventManager
        .connect(creator)
        .submitEventRequest(
          eventPlaceId,
          defaultSubmitEventRequest.ticketPrice,
          await timestamp(10),
          defaultSubmitEventRequest.daysAmount,
          defaultSubmitEventRequest.digest,
          defaultSubmitEventRequest.hashFunction,
          defaultSubmitEventRequest.size,
          [],
        );
      await expect(tx).to.be.revertedWith("At least one category is required");
    });
  });

  describe("declineEvent", () => {
    itExpectsOnlyLocalProvider("declineEvent", [BigInt(0)]);

    it("emits EventStatusChanged", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      await createEventPlace(eventManager, verifiedShaman, localProvider);
      const { request, tx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        {},
      );
      const eventId = await getEventId();
      await expect(
        await eventManager.connect(localProvider).declineEvent(eventId),
      )
        .to.emit(eventManager, "EventStatusChanged")
        .withArgs(eventId, 2);
    });

    it("reverts on unexisting event request", async () => {
      const { eventManager, localProvider } = await loadFixture(deployContract);
      await expect(
        eventManager
          .connect(localProvider)
          .declineEvent(Math.floor(Math.random() * 1000)),
      ).to.be.revertedWith("Event with given id does not exist");
    });

    it("refunds tokens to creator", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      await createEventPlace(eventManager, verifiedShaman, localProvider);
      const { request, tx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        {},
      );
      await tx;
      await expect(
        await eventManager
          .connect(localProvider)
          .declineEvent(await getEventId()),
      ).to.changeTokenBalances(
        ERC20,
        [await eventManager.getAddress(), await creator.getAddress()],
        [-defaultEventDepositSize, defaultEventDepositSize],
      );
    });
  });

  describe("updateEvent", () => {
    itExpectsOnlyLocalProvider("updateEvent", [
      BigInt(0),
      ...baseEventArgsToArray(defaultSubmitEventRequest),
    ]);

    it("emits EventUpdated", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { eventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        { startDate: await timestamp(5), daysAmount: 3 },
        {},
        splitter,
      );
      const updatedRequest = {
        ...defaultSubmitEventRequest,
        startDate: await timestamp(10),
        daysAmount: 2,
        ticketPrice: 50,
      };
      const tx = await eventManager
        .connect(localProvider)
        .updateEvent(eventId, ...baseEventArgsToArray(updatedRequest));
      await expect(tx)
        .to.emit(eventManager, "EventUpdated")
        .withArgs(eventId, ...baseEventArgsToArray(updatedRequest));
    });

    it("reverts on unexisting event", async () => {
      const { eventManager, localProvider } = await loadFixture(deployContract);
      const nonExistentEventId = BigInt(9999);
      await expect(
        eventManager
          .connect(localProvider)
          .updateEvent(
            nonExistentEventId,
            ...baseEventArgsToArray(defaultSubmitEventRequest),
          ),
      ).to.be.revertedWith("Event with given id does not exist");
    });

    it("checks date ranges overlap", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { eventId: firstEventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        { startDate: await timestamp(5), daysAmount: 4 },
        {},
        splitter,
      );
      const { eventId: secondEventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        { startDate: await timestamp(15), daysAmount: 4 },
        {},
        splitter,
      );
      const updatedRequest = {
        ...defaultSubmitEventRequest,
        startDate: await timestamp(5),
        daysAmount: 4,
      };
      await expect(
        eventManager
          .connect(localProvider)
          .updateEvent(secondEventId, ...baseEventArgsToArray(updatedRequest)),
      ).to.be.revertedWith("Requested event overlaps with existing");
    });

    it("allows moving event into gap without self-overlap false positive", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);

      const { request: firstRequest } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        { startDate: await timestamp(11), daysAmount: 3 },
        {},
        splitter,
      );
      const eventPlaceId = firstRequest.eventPlaceId;

      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { tx: secondSubmitTx, getEventId: getSecondEventId } =
        await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(15),
          daysAmount: 3,
        });
      await secondSubmitTx;
      const secondEventId = await getSecondEventId();
      await expect(
        eventManager.connect(localProvider).approveEvent(secondEventId, 1n),
      ).to.not.be.reverted;

      const updatedRequest = {
        ...defaultSubmitEventRequest,
        eventPlaceId,
        startDate: await timestamp(14),
        daysAmount: 3,
      };
      await expect(
        eventManager
          .connect(localProvider)
          .updateEvent(secondEventId, ...baseEventArgsToArray(updatedRequest)),
      ).to.not.be.reverted;
    });
  });

  describe("mintTicket", () => {
    it("emits TicketMinted", async () => {
      const {
        eventManager,
        eventTicket,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
        splitter,
      } = await loadFixture(deployContract);
      const { eventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        {},
        splitter,
      );
      const ticketPrice = 20;
      const multihash = {
        digest:
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        hashFunction: 18,
        size: 32,
      };
      await ERC20.connect(owner).mint(ticketPrice);
      await ERC20.connect(owner).approve(
        await eventManager.getAddress(),
        ticketPrice,
      );
      // Use category 0 (the default category created by createEvent)
      const tx = await eventManager.connect(owner).mintTickets(
        eventId,
        0, // categoryId
        1, // amount
        multihash.digest,
        multihash.hashFunction,
        multihash.size,
        "0x0000000000000000000000000000000000000000",
      );
      await expect(tx).to.emit(eventTicket, "TicketMinted");
    });

    it("emits referralData as 0x-hex string when referrer is provided", async () => {
      const {
        eventManager,
        eventTicket,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
        staff,
        splitter,
      } = await loadFixture(deployContract);

      const { eventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        {},
        splitter,
      );

      const ticketPrice = 20;
      const multihash = {
        digest:
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        hashFunction: 18,
        size: 32,
      };

      await ERC20.connect(owner).mint(ticketPrice);
      await ERC20.connect(owner).approve(
        await eventManager.getAddress(),
        ticketPrice,
      );

      const referrer = await staff.getAddress();
      const tx = eventManager.connect(owner).mintTickets(
        eventId,
        0, // categoryId
        1, // amount
        multihash.digest,
        multihash.hashFunction,
        multihash.size,
        referrer,
      );

      await expect(tx)
        .to.emit(eventTicket, "TicketMinted")
        .withArgs(
          eventId,
          1,
          0,
          await owner.getAddress(),
          multihash.digest,
          multihash.hashFunction,
          multihash.size,
          referrer,
          ticketPrice,
        );
    });

    it("reverts on sold out", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
      } = await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        { maxTickets: 2, minTickets: 1 },
        {},
      );
      // Create a category with quota of 1
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Limited", 0, 1, true);
      // Approve the event
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      const ticketPrice = 20;
      const multihash = {
        digest:
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        hashFunction: 18,
        size: 32,
      };
      await ERC20.connect(owner).mint(ticketPrice * 2);
      await ERC20.connect(owner).approve(
        await eventManager.getAddress(),
        ticketPrice * 2,
      );
      // Mint first ticket with category 1 (the limited category)
      await eventManager.connect(owner).mintTickets(
        eventId,
        1, // categoryId for "Limited"
        1, // amount
        multihash.digest,
        multihash.hashFunction,
        multihash.size,
        "0x0000000000000000000000000000000000000000",
      );
      // Second mint should revert due to category quota
      await expect(
        eventManager.connect(owner).mintTickets(
          eventId,
          1, // categoryId for "Limited"
          1, // amount
          multihash.digest,
          multihash.hashFunction,
          multihash.size,
          "0x0000000000000000000000000000000000000000",
        ),
      ).to.be.revertedWith("Category quota exceeded");
    });

    it("transfers required amount of tokens", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
        splitter,
      } = await loadFixture(deployContract);
      const { eventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        {},
        splitter,
      );
      const ticketPrice = 20;
      const multihash = {
        digest:
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        hashFunction: 18,
        size: 32,
      };
      await ERC20.connect(owner).mint(ticketPrice);
      await ERC20.connect(owner).approve(
        await eventManager.getAddress(),
        ticketPrice,
      );
      // Use category 0 (the default category created by createEvent)
      const tx = await eventManager.connect(owner).mintTickets(
        eventId,
        0, // categoryId
        1, // amount
        multihash.digest,
        multihash.hashFunction,
        multihash.size,
        "0x0000000000000000000000000000000000000000",
      );
      await expect(tx).to.changeTokenBalances(
        ERC20,
        [await eventManager.getAddress(), await owner.getAddress()],
        [ticketPrice, -ticketPrice],
      );
    });

    it("mints NFT with proper metadata", async () => {
      const {
        eventManager,
        eventTicket,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
        splitter,
      } = await loadFixture(deployContract);
      const { eventId, tx: createEventTx } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        {},
        splitter,
      );
      await createEventTx;
      const ticketPrice = 20;
      const multihash = {
        digest:
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        hashFunction: 18,
        size: 32,
      };
      await ERC20.connect(owner).mint(ticketPrice);
      await ERC20.connect(owner).approve(
        await eventManager.getAddress(),
        ticketPrice,
      );
      // Use category 0 (the default category created by createEvent)
      const tx = eventManager.connect(owner).mintTickets(
        eventId,
        0, // categoryId
        1, // amount
        multihash.digest,
        multihash.hashFunction,
        multihash.size,
        "0x0000000000000000000000000000000000000000",
      );
      await expect(tx)
        .to.emit(eventTicket, "TicketMinted")
        .withArgs(
          eventId,
          1,
          0, // categoryId (default category)
          await owner.getAddress(),
          multihash.digest,
          multihash.hashFunction,
          multihash.size,
          "0x0000000000000000000000000000000000000000",
          ticketPrice,
        );
    });
  });

  describe("ticket categories", () => {
    itExpectsOnlyVerifiedShaman("createCategory", [
      BigInt(0),
      "Families",
      1000,
      10,
      true,
    ]);

    it("reverts creating category after event is approved", async () => {
      const { eventManager, creator, localProvider, eventId } =
        await createSubmittedEvent();
      // Event is already created with default category, approve it
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);
      // Try to create another category after approval
      await expect(
        eventManager
          .connect(creator)
          .createCategory(eventId, "Families", 1000, 10, true),
      ).to.be.revertedWith("Event must be in submitted state");
    });

    it("allows one unlimited category per event", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize * 2n);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize * 2n,
      );
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
        {},
      );
      const { getEventId: getFirstEventId } = await submitEventRequest(
        eventManager,
        creator,
        {
          eventPlaceId,
          startDate: await timestamp(10),
          categories: [limitedCategory],
        },
      );
      const { getEventId: getSecondEventId } = await submitEventRequest(
        eventManager,
        creator,
        {
          eventPlaceId,
          startDate: await timestamp(20),
          categories: [limitedCategory],
        },
      );
      const firstEventId = await getFirstEventId();
      const secondEventId = await getSecondEventId();

      await eventManager
        .connect(creator)
        .createCategory(firstEventId, "Regular", 0, 0, false);
      await expect(
        eventManager
          .connect(creator)
          .createCategory(secondEventId, "Regular", 0, 0, false),
      ).to.not.be.reverted;
    });

    it("applies discount for unlimited categories", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
      } = await loadFixture(deployContract);
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
        {},
      );
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { getEventId } = await submitEventRequest(eventManager, creator, {
        eventPlaceId,
        startDate: await timestamp(10),
        ticketPrice: 100,
        categories: [limitedCategory],
      });
      const eventId = await getEventId();
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Regular", 1000, 0, false);
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      await ERC20.connect(owner).mint(90);
      await ERC20.connect(owner).approve(await eventManager.getAddress(), 90);
      await expect(
        eventManager.connect(owner).mintTickets(
          eventId,
          1, // Regular category (with 10% discount)
          1, // amount
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
          18,
          32,
          "0x0000000000000000000000000000000000000000",
        ),
      ).to.changeTokenBalances(
        ERC20,
        [await eventManager.getAddress(), await owner.getAddress()],
        [90, -90],
      );
    });

    it("enforces quota for limited categories", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
      } = await loadFixture(deployContract);
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
        {},
      );
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { getEventId } = await submitEventRequest(eventManager, creator, {
        eventPlaceId,
        startDate: await timestamp(10),
        ticketPrice: 100,
        categories: defaultSubmitEventRequest.categories,
      });
      const eventId = await getEventId();
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Families", 0, 1, true);
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      await ERC20.connect(owner).mint(200);
      await ERC20.connect(owner).approve(await eventManager.getAddress(), 200);
      await eventManager.connect(owner).mintTickets(
        eventId,
        1, // Families category (created after default)
        1, // amount
        "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        18,
        32,
        "0x0000000000000000000000000000000000000000",
      );
      await expect(
        eventManager.connect(owner).mintTickets(
          eventId,
          1, // Families category (created after default)
          1, // amount
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
          18,
          32,
          "0x0000000000000000000000000000000000000000",
        ),
      ).to.be.revertedWith("Category quota exceeded");
    });

    it("should revert when creating category with quota exceeding maxTickets", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        { maxTickets: 100 },
        {},
      );
      await expect(
        eventManager
          .connect(creator)
          .createCategory(eventId, "BigCategory", 1000, 150, true),
      ).to.be.revertedWith("Quota exceeds event capacity");
    });

    it("should revert when total category quotas exceed maxTickets", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        { maxTickets: 100 },
        {},
      );
      // Create first category with quota = 60
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category1", 1000, 60, true);
      // Try to create second category with quota = 50 (total would be 110 > 100)
      await expect(
        eventManager
          .connect(creator)
          .createCategory(eventId, "Category2", 1000, 50, true),
      ).to.be.revertedWith("Total category quotas exceed event capacity");
    });

    it("should revert when creating category with quota = 0", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
      );
      await expect(
        eventManager
          .connect(creator)
          .createCategory(eventId, "ZeroQuota", 1000, 0, true),
      ).to.be.revertedWith("Quota must be greater than 0");
    });

    it("should allow creating categories until quotas sum to maxTickets", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        { maxTickets: 100 },
        {},
      );
      // Create category1 with quota = 50
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category1", 1000, 50, true);
      // Create category2 with quota = 50 (total = 100)
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category2", 1000, 50, true);
      // Try to create category3 with quota = 1 (should fail, total would be 101)
      await expect(
        eventManager
          .connect(creator)
          .createCategory(eventId, "Category3", 1000, 1, true),
      ).to.be.revertedWith("Total category quotas exceed event capacity");
    });

    it("should allow creating unlimited category when quotas fill maxTickets", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        { maxTickets: 100 },
        { categories: [limitedCategory] },
      );
      // Create category1 with quota = 50 (fills all remaining capacity)
      await eventManager
        .connect(creator)
        .createCategory(eventId, "FullCapacity", 1000, 50, true);
      // Create unlimited category (should succeed)
      await expect(
        eventManager
          .connect(creator)
          .createCategory(eventId, "Unlimited", 500, 0, false),
      ).to.not.be.reverted;
    });

    it("should revert when updating category quota below sold amount", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
      } = await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
      );
      // Create category with quota = 50
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category", 0, 50, true);
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      // Buy 10 tickets from category
      await ERC20.connect(owner).mint(1000);
      await ERC20.connect(owner).approve(await eventManager.getAddress(), 1000);
      await eventManager.connect(owner).mintTickets(
        eventId,
        0,
        10, // amount
        "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        18,
        32,
        "0x0000000000000000000000000000000000000000",
      );

      // Try to update quota to 5 (less than 10 sold) - should fail because event is approved
      await expect(
        eventManager
          .connect(localProvider)
          .updateCategory(0, "Category", 0, 5, true),
      ).to.be.revertedWith("Event must be in submitted state");
    });

    it("should revert when updating increases total quotas beyond maxTickets", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        { maxTickets: 100 },
        { categories: defaultSubmitEventRequest.categories },
      );
      // Create category1 with quota = 60
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category1", 1000, 60, true);
      // Create category2 with quota = 30
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category2", 1000, 30, true);
      // Try to update category2 (categoryId 2) quota to 50 (total would be 110 > 100)
      await expect(
        eventManager
          .connect(localProvider)
          .updateCategory(2, "Category2", 1000, 50, true),
      ).to.be.revertedWith("Total category quotas exceed event capacity");
    });

    it("should allow updating category within quota constraints", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        { maxTickets: 100 },
        {},
      );
      // Create category1 with quota = 50
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category1", 1000, 50, true);
      // Create category2 with quota = 30
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category2", 1000, 30, true);
      // Update category2 quota to 40 (total = 90, should succeed)
      await expect(
        eventManager
          .connect(localProvider)
          .updateCategory(2, "Category2", 1000, 40, true),
      ).to.not.be.reverted;
    });

    it("should track totalCategoryQuota correctly after multiple operations", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      // Create event with empty categories for full control
      const { eventId } = await createEventForCategories(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        { maxTickets: 100 },
        { categories: defaultSubmitEventRequest.categories },
      );
      // Create category1 with quota = 30 (totalCategoryQuota = 30)
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category1", 1000, 30, true);
      // Create category2 with quota = 40 (totalCategoryQuota = 70)
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category2", 1000, 40, true);

      // Verify we can only add 30 more (100 - 70 = 30)
      await expect(
        eventManager
          .connect(creator)
          .createCategory(eventId, "Category3", 1000, 31, true),
      ).to.be.revertedWith("Total category quotas exceed event capacity");

      // Update category1 (categoryId 1) quota to 50 (totalCategoryQuota = 90)
      await eventManager
        .connect(localProvider)
        .updateCategory(1, "Category1", 1000, 50, true);

      // Now we can only add 10 more (100 - 90 = 10)
      await expect(
        eventManager
          .connect(creator)
          .createCategory(eventId, "Category3", 1000, 11, true),
      ).to.be.revertedWith("Total category quotas exceed event capacity");

      // But 10 should work
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Category3", 1000, 10, true);
    });
  });

  describe("closeEvent", () => {
    itExpectsOnlyLocalProvider("closeEvent", [BigInt(0)]);

    it("allows master to close any provider event", async () => {
      const {
        eventManager,
        ERC20,
        master,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);

      const { tx: createEventTx, eventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        {},
        splitter,
      );
      await createEventTx;
      await time.increase(100_000_000);

      const tx = await eventManager.connect(master).closeEvent(eventId);
      await expect(tx).to.emit(eventManager, "EventStatusChanged").withArgs(eventId, 4);
    });

    it("emits EventStatusChanged", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { tx, request } = await createAndCloseEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        splitter,
      );
      await expect(tx)
        .to.emit(eventManager, "EventStatusChanged")
        .withArgs(request.eventId, 4);
    });

    it("reverts on unexisting event", async () => {
      const { eventManager, localProvider } = await loadFixture(deployContract);
      const { tx } = await closeEvent(eventManager, localProvider, {
        eventId: Math.floor(Math.random() * 1000),
      });
      await expect(tx).to.be.revertedWith("Event with given id does not exist");
    });

    it("reverts to close cancelled event", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { tx, request } = await createAndCancelEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        splitter,
      );
      await tx;
      const { tx: closeEventTx } = await closeEvent(
        eventManager,
        localProvider,
        request,
      );
      await expect(closeEventTx).to.be.revertedWith(
        "Only event in approved state can be closed",
      );
    });

    it("reverts to close closed event", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { tx, request } = await createAndCloseEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        splitter,
      );
      await tx;
      const { tx: closeEventTx } = await closeEvent(
        eventManager,
        localProvider,
        request,
      );
      await expect(closeEventTx).to.be.revertedWith(
        "Only event in approved state can be closed",
      );
    });

    it("reverts to close submitted event", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
      );
      const { request, tx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        {
          eventPlaceId,
        },
      );
      await tx;
      const { tx: closeEventTx } = await closeEvent(
        eventManager,
        localProvider,
        {
          eventId: await getEventId(),
        },
      );
      await expect(closeEventTx).to.be.revertedWith(
        "Only event in approved state can be closed",
      );
    });

    it("reverts to close declined event", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      await createEventPlace(eventManager, verifiedShaman, localProvider);
      const { request, tx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        {},
      );
      const eventId = await getEventId();
      const { tx: closeEventTx } = await closeEvent(
        eventManager,
        localProvider,
        {
          eventId,
        },
      );
      await expect(closeEventTx).to.be.revertedWith(
        "Only event in approved state can be closed",
      );
    });

    it("reverts if event was not finished", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { tx: createEventTx, eventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        {},
        splitter,
      );
      await createEventTx;
      const { tx } = await closeEvent(eventManager, localProvider, { eventId });
      await expect(tx).to.be.revertedWith("Event has not been finished yet");
    });

    it("proportionally spreads funds", async () => {
      const {
        eventManager,
        ERC20,
        master,
        verifiedShaman,
        localProvider,
        creator,
        staff,
        owner,
      } = await loadFixture(deployContract);

      // Create event with ticket sales
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
      );
      const { tx: submitTx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        { eventPlaceId, ticketPrice: 20 },
      );
      await submitTx;
      const eventId = await getEventId();

      // Categories are created automatically during event submission
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      // Buy 5 tickets @ 20 USDT = 100 USDT revenue
      await ERC20.connect(owner).mint(100);
      await ERC20.connect(owner).approve(await eventManager.getAddress(), 100);
      await eventManager.connect(owner).mintTickets(
        eventId,
        0,
        5, // amount
        "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        18,
        32,
        "0x0000000000000000000000000000000000000000",
      );

      // Move time to allow closing
      await time.increase(100_000_000);

      const { tx } = await closeEvent(eventManager, localProvider, { eventId });

      // Deposit (100) returned to creator
      // Ticket revenue (100) distributed:
      //   10% (10) to master (cyberiaDAO), 5% (5) to staff (cvePtPma)
      //   5% (5) to localProvider (profile manager bps)
      //   80% (80) to profile recipient (creator)
      await expect(tx).to.changeTokenBalances(
        ERC20,
        [
          await creator.getAddress(),
          await master.getAddress(),
          await staff.getAddress(),
          await localProvider.getAddress(),
          await eventManager.getAddress(),
        ],
        [180, 10, 5, 5, -200],
      );
    });
  });

  describe("cancelEvent", () => {
    itExpectsOnlyLocalProvider("cancelEvent", [BigInt(0)]);

    it("allows master to cancel any provider event", async () => {
      const {
        eventManager,
        ERC20,
        master,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);

      const { tx: createEventTx, eventId } = await createEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        {},
        {},
        splitter,
      );
      await createEventTx;
      await time.increase(100_000_000);

      const tx = await eventManager.connect(master).cancelEvent(eventId);
      await expect(tx).to.emit(eventManager, "EventStatusChanged").withArgs(eventId, 3);
    });

    it("emits EventStatusChanged", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { tx, request } = await createAndCancelEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        splitter,
      );
      await expect(tx)
        .to.emit(eventManager, "EventStatusChanged")
        .withArgs(request.eventId, 3);
    });

    it("refunds discounted ticket price", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        owner,
      } = await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
      );
      const { getEventId } = await submitEventRequest(eventManager, creator, {
        eventPlaceId,
        ticketPrice: 100,
        startDate: await timestamp(10),
      });
      const eventId = await getEventId();
      await eventManager
        .connect(creator)
        .createCategory(eventId, "Families", 1000, 10, true);
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      const discountedPrice = 90n;
      await ERC20.connect(owner).mint(discountedPrice);
      await ERC20.connect(owner).approve(
        await eventManager.getAddress(),
        discountedPrice,
      );
      // Category 0 = "General" (created by default), Category 1 = "Families" (with discount)
      await eventManager.connect(owner).mintTickets(
        eventId,
        1, // Use Families category (with discount)
        1, // amount
        "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        18,
        32,
        "0x0000000000000000000000000000000000000000",
      );

      const tx = await eventManager.connect(localProvider).cancelEvent(eventId);
      // On cancel:
      // - Deposit goes to creator (shaman)
      // - Ticket revenue goes to localProvider
      await expect(tx).to.changeTokenBalances(
        ERC20,
        [
          await eventManager.getAddress(),
          await owner.getAddress(),
          await localProvider.getAddress(),
          await creator.getAddress(),
        ],
        [
          -(defaultEventDepositSize + discountedPrice),
          0,
          discountedPrice,
          defaultEventDepositSize,
        ],
      );
    });

    it("reverts to cancel cancelled event", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { tx, request } = await createAndCancelEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        splitter,
      );
      await tx;
      const { tx: cancelEventTx } = await cancelEvent(
        eventManager,
        localProvider,
        request,
      );
      await expect(cancelEventTx).to.be.revertedWith(
        "Only event in approved state can be cancelled",
      );
    });

    it("reverts to cancel closed event", async () => {
      const {
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        splitter,
      } = await loadFixture(deployContract);
      const { tx, request } = await createAndCancelEvent(
        eventManager,
        ERC20,
        verifiedShaman,
        localProvider,
        creator,
        {},
        splitter,
      );
      await tx;
      const { tx: cancelEventTx } = await cancelEvent(
        eventManager,
        localProvider,
        request,
      );
      await expect(cancelEventTx).to.be.revertedWith(
        "Only event in approved state can be cancelled",
      );
    });

    it("reverts to cancel submitted event", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
      );
      const { request, tx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        {
          eventPlaceId,
        },
      );
      await tx;
      const { tx: cancelEventTx } = await cancelEvent(
        eventManager,
        localProvider,
        {
          eventId: await getEventId(),
        },
      );
      await expect(cancelEventTx).to.be.revertedWith(
        "Only event in approved state can be cancelled",
      );
    });

    it("reverts to cancel declined event", async () => {
      const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
        await loadFixture(deployContract);
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      await createEventPlace(eventManager, verifiedShaman, localProvider);
      const { request, tx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        {},
      );
      const eventId = await getEventId();
      const { tx: cancelEventTx } = await cancelEvent(
        eventManager,
        localProvider,
        {
          eventId,
        },
      );
      await expect(cancelEventTx).to.be.revertedWith(
        "Only event in approved state can be cancelled",
      );
    });

    it("refunds tokens to owners and local provider", async () => {
      const {
        eventManager,
        ERC20,
        master,
        verifiedShaman,
        localProvider,
        creator,
        staff,
      } = await loadFixture(deployContract);

      const allSigners = await ethers.getSigners();
      const customer1 = allSigners[5];
      const customer2 = allSigners[6];
      const customer3 = allSigners[7];

      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );

      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
        {
          maxTickets: 100,
          minTickets: 1,
          minPrice: 10,
        },
      );

      const { tx: submitEventRequestTx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        {
          eventPlaceId,
          ticketPrice: 10,
        },
      );
      await submitEventRequestTx;
      const eventId = await getEventId();

      // Categories are created automatically during event submission
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      const customers = [customer1, customer2, customer3];
      const ticketsPerCustomer = [2, 3, 1];
      const totalTickets = ticketsPerCustomer.reduce((a, b) => a + b, 0);

      const multihash = {
        digest:
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        hashFunction: 18,
        size: 32,
      };

      for (let i = 0; i < customers.length; i++) {
        const customer = customers[i];
        const ticketCount = ticketsPerCustomer[i];
        const totalCost = ticketCount * 10;

        await ERC20.connect(customer).mint(totalCost);
        await ERC20.connect(customer).approve(
          await eventManager.getAddress(),
          totalCost,
        );

        await eventManager.connect(customer).mintTickets(
          eventId,
          0, // categoryId
          ticketCount, // amount
          multihash.digest,
          multihash.hashFunction,
          multihash.size,
          "0x0000000000000000000000000000000000000000",
        );
      }

      const { tx: cancelTx } = await cancelEvent(eventManager, localProvider, {
        eventId,
      });

      const eventDepositSize = defaultEventDepositSize;
      const totalRevenue = BigInt(totalTickets) * 10n;

      // After cancelEvent:
      // - Deposit goes to creator (shaman)
      // - Ticket revenue goes to localProvider
      await expect(cancelTx).to.changeTokenBalances(
        ERC20,
        [
          await master.getAddress(),
          await localProvider.getAddress(),
          await creator.getAddress(),
          await customer1.getAddress(),
          await customer2.getAddress(),
          await customer3.getAddress(),
          await eventManager.getAddress(),
        ],
        [
          0,
          totalRevenue,
          eventDepositSize,
          0,
          0,
          0,
          -(eventDepositSize + totalRevenue),
        ],
      );
    });
  });

  describe("Fund Distribution", () => {
    it("distributes funds through DynamicRevenueSplitter on closeEvent", async () => {
      const {
        ERC20,
        eventManager,
        splitter,
        master,
        staff,
        verifiedShaman,
        localProvider,
        creator,
        owner,
      } = await loadFixture(deployContract);

      // Setup event and tickets
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );

      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
        { maxTickets: 100, minTickets: 1, minPrice: 10 },
      );

      const { tx: submitTx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        { eventPlaceId, ticketPrice: 10 },
      );
      await submitTx;
      const eventId = await getEventId();

      // Categories are created automatically during event submission
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      // Buy 10 tickets @ 10 USDT = 100 USDT
      const ticketPrice = 10;
      const ticketCount = 10;
      const totalTicketCost = ticketPrice * ticketCount;
      const multihash = {
        digest:
          "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
        hashFunction: 18,
        size: 32,
      };

      await ERC20.connect(owner).mint(totalTicketCost);
      await ERC20.connect(owner).approve(
        await eventManager.getAddress(),
        totalTicketCost,
      );
      await eventManager.connect(owner).mintTickets(
        eventId,
        0, // categoryId
        ticketCount, // amount
        multihash.digest,
        multihash.hashFunction,
        multihash.size,
        "0x0000000000000000000000000000000000000000",
      );

      // Total ticket revenue = 100 (distributed via splitter)
      // Deposit = 100 (returned to creator)
      const totalAmount = 100;

      // Move time to allow closing
      await time.increase(100_000_000);

      // Close event and check distribution
      // Deposit: 100 returned to creator
      // Ticket revenue: 10% (10) to master, 5% (5) to staff,
      //   5% (5) to localProvider (profile manager bps), 80% (80) to profile recipient (creator)

      const tx = await eventManager.connect(localProvider).closeEvent(eventId);

      await expect(tx).to.changeTokenBalances(
        ERC20,
        [
          await creator.getAddress(),
          await master.getAddress(),
          await staff.getAddress(),
          await localProvider.getAddress(),
          await eventManager.getAddress(),
        ],
        [180, 10, 5, 5, -200],
      );

      await expect(tx)
        .to.emit(splitter, "RevenueDistributed")
        .withArgs(totalAmount, eventId);
    });

    it("uses event-specific profile if set", async () => {
      const {
        ERC20,
        eventManager,
        splitter,
        master,
        staff,
        verifiedShaman,
        localProvider,
        creator,
        owner,
      } = await loadFixture(deployContract);

      const customer = owner;
      await ERC20.connect(creator).mint(defaultEventDepositSize);
      await ERC20.connect(creator).approve(
        await eventManager.getAddress(),
        defaultEventDepositSize,
      );
      const { eventPlaceId } = await createEventPlace(
        eventManager,
        verifiedShaman,
        localProvider,
      );
      const { tx: submitTx, getEventId } = await submitEventRequest(
        eventManager,
        creator,
        { eventPlaceId, ticketPrice: 20 },
      );
      await submitTx;
      const eventId = await getEventId();

      // Categories are created automatically during event submission
      await eventManager.connect(localProvider).approveEvent(eventId, 1n);

      // Create and set event-specific profile (admin creates for themselves)
      await splitter
        .connect(master)
        .createDistributionProfile([await customer.getAddress()], [8500]);
      await splitter.connect(master).setEventProfile(eventId, 2);

      await time.increase(100_000_000);
      // No tickets sold, so no revenue to distribute
      // Deposit (100) is returned to creator

      const tx = await eventManager.connect(localProvider).closeEvent(eventId);

      // Creator gets deposit back (100), no ticket revenue to distribute
      await expect(tx).to.changeTokenBalance(ERC20, creator, 100);
    });
  });

  describe("Local Provider Management", () => {
    describe("grantLocalProvider", () => {
      itExpectsOnlyMaster("grantLocalProvider", [ethers.ZeroAddress, 100]);

      it("grants LOCAL_PROVIDER_ROLE and sets bps in splitter", async () => {
        const { eventManager, splitter, master, owner } =
          await loadFixture(deployContract);
        const LOCAL_PROVIDER_ROLE = await eventManager.LOCAL_PROVIDER_ROLE();

        await expect(
          eventManager
            .connect(master)
            .grantLocalProvider(await owner.getAddress(), 1000),
        )
          .to.emit(eventManager, "RoleGranted")
          .withArgs(
            LOCAL_PROVIDER_ROLE,
            await owner.getAddress(),
            await master.getAddress(),
          );

        expect(
          await eventManager.hasRole(
            LOCAL_PROVIDER_ROLE,
            await owner.getAddress(),
          ),
        ).to.be.true;
        expect(
          await splitter.profileManagerBps(await owner.getAddress()),
        ).to.equal(1000);
      });

      it("reverts when revenue splitter is not set", async () => {
        const { eventManager, master, owner } =
          await loadFixture(deployContract);
        // Create a new event manager without splitter
        const CyberValleyEventManagerFactory = await ethers.getContractFactory(
          "CyberValleyEventManager",
        );
        const newEventManager = await CyberValleyEventManagerFactory.deploy(
          await eventManager.usdtTokenContract(),
          await eventManager.eventTicketContract(),
          master,
          await timestamp(0),
        );

        await expect(
          newEventManager
            .connect(master)
            .grantLocalProvider(await owner.getAddress(), 1000),
        ).to.be.revertedWith("Revenue splitter not set");
      });

      it("reverts for zero address", async () => {
        const { eventManager, master } = await loadFixture(deployContract);
        await expect(
          eventManager
            .connect(master)
            .grantLocalProvider(ethers.ZeroAddress, 1000),
        ).to.be.revertedWith("Local provider cannot be zero address");
      });
    });

    describe("revokeLocalProvider", () => {
      itExpectsOnlyMaster("revokeLocalProvider", [ethers.ZeroAddress]);

      it("transfers EventPlaces to Master and profiles to Master", async () => {
        const {
          eventManager,
          splitter,
          master,
          localProvider,
          verifiedShaman,
          creator,
        } = await loadFixture(deployContract);

        // Create an EventPlace - shamans submit, localProvider approves
        // This creates the place with localProvider as provider
        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
          {
            ...defaultCreateEventPlaceRequest,
            eventDepositSize: 200,
          },
        );

        // Verify localProvider is the provider
        let eventPlace = await eventManager.eventPlaces(eventPlaceId);
        expect(eventPlace.provider).to.equal(await localProvider.getAddress());

        // Create a profile for localProvider (using creator as recipient)
        await splitter
          .connect(localProvider)
          .createDistributionProfile([await creator.getAddress()], [8000]);

        // Verify localProvider owns the profile (profileId 1)
        expect(
          await splitter.isProfileOwner(1, await localProvider.getAddress()),
        ).to.be.true;

        // Revoke local provider
        await expect(
          eventManager
            .connect(master)
            .revokeLocalProvider(await localProvider.getAddress()),
        )
          .to.emit(eventManager, "EventPlaceUpdated")
          .withArgs(
            await master.getAddress(),
            eventPlaceId,
            eventPlace.maxTickets,
            eventPlace.minTickets,
            eventPlace.minPrice,
            eventPlace.daysBeforeCancel,
            eventPlace.minDays,
            eventPlace.available,
            eventPlace.status,
            eventPlace.meta.digest,
            eventPlace.meta.hashFunction,
            eventPlace.meta.size,
            eventPlace.eventDepositSize,
          );

        // Verify EventPlace provider is now Master
        eventPlace = await eventManager.eventPlaces(eventPlaceId);
        expect(eventPlace.provider).to.equal(await master.getAddress());

        // Verify LOCAL_PROVIDER_ROLE is revoked
        const LOCAL_PROVIDER_ROLE = await eventManager.LOCAL_PROVIDER_ROLE();
        expect(
          await eventManager.hasRole(
            LOCAL_PROVIDER_ROLE,
            await localProvider.getAddress(),
          ),
        ).to.be.false;

        // Verify profiles were transferred to Master
        expect(
          await splitter.isProfileOwner(1, await localProvider.getAddress()),
        ).to.be.false;
        expect(await splitter.isProfileOwner(1, await master.getAddress())).to
          .be.true;
      });

      it("handles provider with no EventPlaces", async () => {
        const { eventManager, splitter, master } =
          await loadFixture(deployContract);

        // Create a completely new provider address
        const newProvider = ethers.Wallet.createRandom().connect(
          ethers.provider,
        );

        // Send some ETH for gas
        await master.sendTransaction({
          to: await newProvider.getAddress(),
          value: ethers.parseEther("1"),
        });

        // Grant local provider role
        await eventManager
          .connect(master)
          .grantLocalProvider(await newProvider.getAddress(), 500);

        // Create a profile for the new provider (so transferAllProfiles has something to transfer)
        await splitter
          .connect(newProvider)
          .createDistributionProfile([await master.getAddress()], [8000]);

        // Revoke should succeed even with no EventPlaces
        await expect(
          eventManager
            .connect(master)
            .revokeLocalProvider(await newProvider.getAddress()),
        ).to.not.be.reverted;

        // Verify role is revoked
        const LOCAL_PROVIDER_ROLE = await eventManager.LOCAL_PROVIDER_ROLE();
        expect(
          await eventManager.hasRole(
            LOCAL_PROVIDER_ROLE,
            await newProvider.getAddress(),
          ),
        ).to.be.false;
      });
    });

    describe("revokeVerifiedShaman", () => {
      itExpectsOnlyLocalProvider("revokeVerifiedShaman", [ethers.ZeroAddress]);

      it("revokes VERIFIED_SHAMAN_ROLE", async () => {
        const { eventManager, master, localProvider, creator } =
          await loadFixture(deployContract);

        const VERIFIED_SHAMAN_ROLE = await eventManager.VERIFIED_SHAMAN_ROLE();

        // Ensure creator has the role
        expect(
          await eventManager.hasRole(
            VERIFIED_SHAMAN_ROLE,
            await creator.getAddress(),
          ),
        ).to.be.true;

        await expect(
          eventManager
            .connect(localProvider)
            .revokeVerifiedShaman(await creator.getAddress()),
        )
          .to.emit(eventManager, "RoleRevoked")
          .withArgs(
            VERIFIED_SHAMAN_ROLE,
            await creator.getAddress(),
            await localProvider.getAddress(),
          );

        expect(
          await eventManager.hasRole(
            VERIFIED_SHAMAN_ROLE,
            await creator.getAddress(),
          ),
        ).to.be.false;
      });
    });
  });

  describe("Category Management", () => {
    describe("updateCategory", () => {
      itExpectsOnlyLocalProvider("updateCategory", [
        BigInt(0),
        "Updated Name",
        500,
        10,
        true,
      ]);

      it("updates category name and discount", async () => {
        const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
          await loadFixture(deployContract);

        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
        );

        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );

        const { getEventId } = await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(10),
          categories: [limitedCategory],
        });
        const eventId = await getEventId();

        // Category 0 is the default category created during event submission
        await expect(
          eventManager
            .connect(localProvider)
            .updateCategory(0, "Updated Name", 500, 15, true),
        )
          .to.emit(eventManager, "TicketCategoryUpdated")
          .withArgs(0, eventId, "Updated Name", 500, 15, true);

        const category = await eventManager.getCategory(0);
        expect(category.name).to.equal("Updated Name");
        expect(category.discountPercentage).to.equal(500);
        expect(category.quota).to.equal(15);
      });

      it("reverts when event is not in submitted state", async () => {
        const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
          await loadFixture(deployContract);

        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
        );

        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );

        const { getEventId } = await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(10),
        });
        const eventId = await getEventId();

        // Approve the event - now it's no longer in submitted state
        await eventManager.connect(localProvider).approveEvent(eventId, 1n);

        // Try to update category after approval - should fail
        await expect(
          eventManager
            .connect(localProvider)
            .updateCategory(0, "Updated Name", 500, 10, true),
        ).to.be.revertedWith("Event must be in submitted state");
      });

      it("reverts when caller is not the event's local provider", async () => {
        const {
          eventManager,
          ERC20,
          verifiedShaman,
          localProvider,
          creator,
          master,
          splitter,
        } = await loadFixture(deployContract);

        // Create a second local provider who has role but doesn't own the event place
        const [, , , , , otherProvider] = await ethers.getSigners();
        await eventManager
          .connect(master)
          .grantLocalProvider(await otherProvider.getAddress(), 300);

        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
          { minTickets: 5 },
        );

        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );

        const { getEventId } = await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(10),
          categories: [
            {
              name: "Limited",
              discountPercentage: 0,
              quota: 5,
              hasQuota: true,
            },
          ],
        });
        const eventId = await getEventId();

        // Try to update category as otherProvider (has LOCAL_PROVIDER_ROLE but doesn't own this event)
        await expect(
          eventManager
            .connect(otherProvider)
            .updateCategory(0, "Limited", 0, 3, true),
        ).to.be.revertedWith(
          "Only the local provider of the event can update categories",
        );
      });

      it("reverts when increasing total quotas beyond maxTickets", async () => {
        const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
          await loadFixture(deployContract);

        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
          { maxTickets: 10, minTickets: 1 },
        );

        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );

        const { getEventId } = await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(10),
          categories: [
            { name: "Cat1", discountPercentage: 0, quota: 5, hasQuota: true },
            { name: "Cat2", discountPercentage: 0, quota: 5, hasQuota: true },
          ],
        });
        const eventId = await getEventId();

        // Try to update Cat1 quota to exceed maxTickets (total would be 6+5=11 > 10)
        await expect(
          eventManager
            .connect(localProvider)
            .updateCategory(0, "Cat1", 0, 6, true),
        ).to.be.revertedWith("Total category quotas exceed event capacity");
      });

      it("reverts when changing from limited to unlimited quota", async () => {
        const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
          await loadFixture(deployContract);

        // Create event place with lower minTickets
        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
          { minTickets: 5 },
        );

        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );

        const { getEventId } = await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(10),
          // Create a limited category first (quota 5 meets minTickets=5)
          categories: [
            {
              name: "Limited",
              discountPercentage: 0,
              quota: 5,
              hasQuota: true,
            },
          ],
        });
        const eventId = await getEventId();

        // Try to change from limited to unlimited
        await expect(
          eventManager
            .connect(localProvider)
            .updateCategory(0, "Unlimited", 0, 0, false),
        ).to.be.revertedWith("Cannot change from limited to unlimited quota");
      });

      it("reverts for non-existent category", async () => {
        const { eventManager, localProvider } =
          await loadFixture(deployContract);

        await expect(
          eventManager
            .connect(localProvider)
            .updateCategory(999, "Name", 0, 10, true),
        ).to.be.revertedWith("Category does not exist");
      });
    });

    describe("getCategory", () => {
      it("returns correct category data", async () => {
        const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
          await loadFixture(deployContract);

        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
        );

        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );

        const { getEventId } = await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(10),
          categories: [
            {
              name: "TestCat",
              discountPercentage: 1000,
              quota: 50,
              hasQuota: true,
            },
          ],
        });
        const eventId = await getEventId();

        const category = await eventManager.getCategory(0);
        expect(category.name).to.equal("TestCat");
        expect(category.eventId).to.equal(eventId);
        expect(category.discountPercentage).to.equal(1000);
        expect(category.quota).to.equal(50);
        expect(category.hasQuota).to.be.true;
        expect(category.sold).to.equal(0);
      });

      it("reverts for non-existent category", async () => {
        const { eventManager } = await loadFixture(deployContract);

        await expect(eventManager.getCategory(999)).to.be.revertedWith(
          "Category does not exist",
        );
      });
    });

    describe("getCategoriesForEvent", () => {
      it("returns all category IDs for an event", async () => {
        const { eventManager, ERC20, verifiedShaman, localProvider, creator } =
          await loadFixture(deployContract);

        const { eventPlaceId } = await createEventPlace(
          eventManager,
          verifiedShaman,
          localProvider,
          { minTickets: 10 }, // Lower minTickets so 25+25=50 >= 10 passes
        );

        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );

        const { getEventId } = await submitEventRequest(eventManager, creator, {
          eventPlaceId,
          startDate: await timestamp(10),
          categories: [
            { name: "Cat1", discountPercentage: 0, quota: 25, hasQuota: true },
            {
              name: "Cat2",
              discountPercentage: 500,
              quota: 25,
              hasQuota: true,
            },
          ],
        });
        const eventId = await getEventId();

        const categoryIds = await eventManager.getCategoriesForEvent(eventId);
        expect(categoryIds.length).to.equal(2);
        expect(categoryIds[0]).to.equal(0);
        expect(categoryIds[1]).to.equal(1);
      });

      it("returns empty array for event with no categories", async () => {
        const { eventManager } = await loadFixture(deployContract);

        const categoryIds = await eventManager.getCategoriesForEvent(999);
        expect(categoryIds).to.deep.equal([]);
      });
    });
  });

  describe("View Functions", () => {
    describe("getEventsCount", () => {
      it("returns correct event count", async () => {
        const {
          eventManager,
          ERC20,
          verifiedShaman,
          localProvider,
          creator,
          splitter,
        } = await loadFixture(deployContract);

        expect(await eventManager.getEventsCount()).to.equal(0);

        // Create first event
        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );
        const { eventId: eventId1 } = await createEvent(
          eventManager,
          ERC20,
          verifiedShaman,
          localProvider,
          creator,
          {},
          {},
          {},
          splitter,
        );

        expect(await eventManager.getEventsCount()).to.equal(1);

        // Create second event
        await ERC20.connect(creator).mint(defaultEventDepositSize);
        await ERC20.connect(creator).approve(
          await eventManager.getAddress(),
          defaultEventDepositSize,
        );
        const { eventId: eventId2 } = await createEvent(
          eventManager,
          ERC20,
          verifiedShaman,
          localProvider,
          creator,
          {},
          { startDate: await timestamp(30) },
          {},
          splitter,
        );

        expect(await eventManager.getEventsCount()).to.equal(2);
      });
    });
  });
});

Graph