import { ProjectStatus, SystemRole } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { createProjectLifecycleProcedures } from "../router/project-lifecycle.js";
import { createCallerFactory, createTRPCRouter } from "../trpc.js";

// ─── Dependency mocks ─────────────────────────────────────────────────────────

const deps = {
  invalidateDashboardCacheInBackground: vi.fn(),
  dispatchProjectWebhookInBackground: vi.fn(),
};

const procedures = createProjectLifecycleProcedures(deps);
const router = createTRPCRouter(procedures);
const createCaller = createCallerFactory(router);

// ─── Caller factories ─────────────────────────────────────────────────────────

function createManagerCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "mgr@example.com", name: "Manager", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: { id: "user_mgr", systemRole: SystemRole.MANAGER, permissionOverrides: null },
  });
}

function createAdminCaller(db: Record<string, unknown>) {
  return createCaller({
    session: {
      user: { email: "admin@example.com", name: "Admin", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    db: db as never,
    dbUser: { id: "user_admin", systemRole: SystemRole.ADMIN, permissionOverrides: null },
  });
}

// ─── Shared project fixture ───────────────────────────────────────────────────

const baseProject = {
  id: "proj_1",
  name: "Alpha Project",
  shortCode: "ALF-001",
  status: ProjectStatus.DRAFT,
};

// ─── Transaction mock helpers ─────────────────────────────────────────────────

function makeTxMock() {
  return {
    assignment: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
    demandRequirement: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
    calculationRule: { updateMany: vi.fn().mockResolvedValue({ count: 0 }) },
    project: {
      update: vi.fn(),
      delete: vi.fn().mockResolvedValue(undefined),
      deleteMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    auditLog: { create: vi.fn().mockResolvedValue(undefined) },
  };
}

// ─── Tests ────────────────────────────────────────────────────────────────────

describe("project-lifecycle router", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  // ── updateStatus ─────────────────────────────────────────────────────────────

  describe("updateStatus", () => {
    it("successfully transitions DRAFT → ACTIVE", async () => {
      const updated = { ...baseProject, status: ProjectStatus.ACTIVE };
      const db = {
        project: {
          findUnique: vi
            .fn()
            .mockResolvedValue({ id: baseProject.id, status: ProjectStatus.DRAFT }),
          update: vi.fn().mockResolvedValue(updated),
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.updateStatus({
        id: baseProject.id,
        status: ProjectStatus.ACTIVE,
      });

      expect(db.project.update).toHaveBeenCalledWith({
        where: { id: baseProject.id },
        data: { status: ProjectStatus.ACTIVE },
      });
      expect(result.status).toBe(ProjectStatus.ACTIVE);
    });

    it("calls webhook and cache invalidation after successful status update", async () => {
      const updated = { ...baseProject, status: ProjectStatus.ACTIVE };
      const db = {
        project: {
          findUnique: vi
            .fn()
            .mockResolvedValue({ id: baseProject.id, status: ProjectStatus.DRAFT }),
          update: vi.fn().mockResolvedValue(updated),
        },
      };

      const caller = createManagerCaller(db);
      await caller.updateStatus({ id: baseProject.id, status: ProjectStatus.ACTIVE });

      expect(deps.invalidateDashboardCacheInBackground).toHaveBeenCalledOnce();
      expect(deps.dispatchProjectWebhookInBackground).toHaveBeenCalledOnce();
      expect(deps.dispatchProjectWebhookInBackground).toHaveBeenCalledWith(
        db as never,
        "project.status_changed",
        expect.objectContaining({ id: updated.id, status: ProjectStatus.ACTIVE }),
      );
    });

    it("is a no-op when the target status equals the current status", async () => {
      const unchanged = { ...baseProject, status: ProjectStatus.DRAFT };
      const db = {
        project: {
          findUnique: vi
            .fn()
            .mockResolvedValue({ id: baseProject.id, status: ProjectStatus.DRAFT }),
          update: vi.fn().mockResolvedValue(unchanged),
        },
      };

      const caller = createManagerCaller(db);
      const result = await caller.updateStatus({ id: baseProject.id, status: ProjectStatus.DRAFT });

      // update is still called (same status written back), but no transition validation fires
      expect(db.project.update).toHaveBeenCalledOnce();
      expect(result.status).toBe(ProjectStatus.DRAFT);
    });

    it("throws NOT_FOUND when the project does not exist", async () => {
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue(null),
          update: vi.fn(),
        },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.updateStatus({ id: "proj_missing", status: ProjectStatus.ACTIVE }),
      ).rejects.toMatchObject({ code: "NOT_FOUND", message: "Project not found" });

      expect(db.project.update).not.toHaveBeenCalled();
    });

    it("throws BAD_REQUEST for an invalid transition (DRAFT → COMPLETED)", async () => {
      const db = {
        project: {
          findUnique: vi
            .fn()
            .mockResolvedValue({ id: baseProject.id, status: ProjectStatus.DRAFT }),
          update: vi.fn(),
        },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.updateStatus({ id: baseProject.id, status: ProjectStatus.COMPLETED }),
      ).rejects.toMatchObject({ code: "BAD_REQUEST" });

      expect(db.project.update).not.toHaveBeenCalled();
    });

    it("throws BAD_REQUEST for COMPLETED → ON_HOLD (not an allowed transition)", async () => {
      const db = {
        project: {
          findUnique: vi
            .fn()
            .mockResolvedValue({ id: baseProject.id, status: ProjectStatus.COMPLETED }),
          update: vi.fn(),
        },
      };

      const caller = createManagerCaller(db);
      await expect(
        caller.updateStatus({ id: baseProject.id, status: ProjectStatus.ON_HOLD }),
      ).rejects.toMatchObject({ code: "BAD_REQUEST" });

      expect(db.project.update).not.toHaveBeenCalled();
    });
  });

  // ── batchUpdateStatus ─────────────────────────────────────────────────────────

  describe("batchUpdateStatus", () => {
    it("updates multiple projects inside a transaction", async () => {
      const txMock = makeTxMock();
      txMock.project.update
        .mockResolvedValueOnce({ id: "proj_1", status: ProjectStatus.ON_HOLD })
        .mockResolvedValueOnce({ id: "proj_2", status: ProjectStatus.ON_HOLD });

      const db = {
        $transaction: vi
          .fn()
          .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      };

      const caller = createManagerCaller(db);
      const result = await caller.batchUpdateStatus({
        ids: ["proj_1", "proj_2"],
        status: ProjectStatus.ON_HOLD,
      });

      expect(txMock.project.update).toHaveBeenCalledTimes(2);
      expect(result).toEqual({ count: 2 });
    });

    it("calls invalidateDashboardCacheInBackground after a successful batch update", async () => {
      const txMock = makeTxMock();
      txMock.project.update.mockResolvedValue({ id: "proj_1", status: ProjectStatus.ACTIVE });

      const db = {
        $transaction: vi
          .fn()
          .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      };

      const caller = createManagerCaller(db);
      await caller.batchUpdateStatus({ ids: ["proj_1"], status: ProjectStatus.ACTIVE });

      expect(deps.invalidateDashboardCacheInBackground).toHaveBeenCalledOnce();
    });

    it("creates an audit log entry inside the transaction", async () => {
      const txMock = makeTxMock();
      txMock.project.update.mockResolvedValue({ id: "proj_1", status: ProjectStatus.CANCELLED });

      const db = {
        $transaction: vi
          .fn()
          .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      };

      const caller = createManagerCaller(db);
      await caller.batchUpdateStatus({ ids: ["proj_1"], status: ProjectStatus.CANCELLED });

      expect(txMock.auditLog.create).toHaveBeenCalledOnce();
      expect(txMock.auditLog.create).toHaveBeenCalledWith(
        expect.objectContaining({
          data: expect.objectContaining({
            entityType: "Project",
            action: "UPDATE",
          }),
        }),
      );
    });
  });

  // ── delete ────────────────────────────────────────────────────────────────────

  describe("delete", () => {
    it("deletes a project with cascade (assignments, demandRequirements, calculationRules)", async () => {
      const txMock = makeTxMock();
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue({
            id: "proj_1",
            name: "Alpha Project",
            shortCode: "ALF-001",
          }),
        },
        $transaction: vi
          .fn()
          .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      };

      const caller = createAdminCaller(db);
      await caller.delete({ id: "proj_1" });

      expect(txMock.assignment.deleteMany).toHaveBeenCalledWith({ where: { projectId: "proj_1" } });
      expect(txMock.demandRequirement.deleteMany).toHaveBeenCalledWith({
        where: { projectId: "proj_1" },
      });
      expect(txMock.calculationRule.updateMany).toHaveBeenCalledWith({
        where: { projectId: "proj_1" },
        data: { projectId: null },
      });
      expect(txMock.project.delete).toHaveBeenCalledWith({ where: { id: "proj_1" } });
    });

    it("throws NOT_FOUND when the project does not exist", async () => {
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue(null),
        },
        $transaction: vi.fn(),
      };

      const caller = createAdminCaller(db);
      await expect(caller.delete({ id: "proj_missing" })).rejects.toMatchObject({
        code: "NOT_FOUND",
        message: "Project not found",
      });

      expect(db.$transaction).not.toHaveBeenCalled();
    });

    it("calls invalidateDashboardCacheInBackground after a successful delete", async () => {
      const txMock = makeTxMock();
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue({
            id: "proj_1",
            name: "Alpha Project",
            shortCode: "ALF-001",
          }),
        },
        $transaction: vi
          .fn()
          .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      };

      const caller = createAdminCaller(db);
      await caller.delete({ id: "proj_1" });

      expect(deps.invalidateDashboardCacheInBackground).toHaveBeenCalledOnce();
    });

    it("returns the id and name of the deleted project", async () => {
      const txMock = makeTxMock();
      const db = {
        project: {
          findUnique: vi.fn().mockResolvedValue({
            id: "proj_1",
            name: "Alpha Project",
            shortCode: "ALF-001",
          }),
        },
        $transaction: vi
          .fn()
          .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      };

      const caller = createAdminCaller(db);
      const result = await caller.delete({ id: "proj_1" });

      expect(result).toEqual({ id: "proj_1", name: "Alpha Project" });
    });
  });

  // ── batchDelete ───────────────────────────────────────────────────────────────

  describe("batchDelete", () => {
    it("deletes multiple projects with cascade inside a transaction", async () => {
      const txMock = makeTxMock();
      const projects = [
        { id: "proj_1", name: "Alpha", shortCode: "ALF-001" },
        { id: "proj_2", name: "Beta", shortCode: "BET-001" },
      ];
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue(projects),
        },
        $transaction: vi
          .fn()
          .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      };

      const caller = createAdminCaller(db);
      const result = await caller.batchDelete({ ids: ["proj_1", "proj_2"] });

      expect(txMock.assignment.deleteMany).toHaveBeenCalledWith({
        where: { projectId: { in: ["proj_1", "proj_2"] } },
      });
      expect(txMock.demandRequirement.deleteMany).toHaveBeenCalledWith({
        where: { projectId: { in: ["proj_1", "proj_2"] } },
      });
      expect(txMock.calculationRule.updateMany).toHaveBeenCalledWith({
        where: { projectId: { in: ["proj_1", "proj_2"] } },
        data: { projectId: null },
      });
      expect(txMock.project.deleteMany).toHaveBeenCalledWith({
        where: { id: { in: ["proj_1", "proj_2"] } },
      });
      expect(result).toEqual({ count: 2 });
    });

    it("throws NOT_FOUND when none of the requested projects exist", async () => {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([]),
        },
        $transaction: vi.fn(),
      };

      const caller = createAdminCaller(db);
      await expect(caller.batchDelete({ ids: ["proj_missing"] })).rejects.toMatchObject({
        code: "NOT_FOUND",
        message: "No projects found",
      });

      expect(db.$transaction).not.toHaveBeenCalled();
    });

    it("returns the count of deleted projects", async () => {
      const txMock = makeTxMock();
      const projects = [
        { id: "proj_1", name: "Alpha", shortCode: "ALF-001" },
        { id: "proj_2", name: "Beta", shortCode: "BET-001" },
        { id: "proj_3", name: "Gamma", shortCode: "GAM-001" },
      ];
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue(projects),
        },
        $transaction: vi
          .fn()
          .mockImplementation((cb: (tx: typeof txMock) => Promise<unknown>) => cb(txMock)),
      };

      const caller = createAdminCaller(db);
      const result = await caller.batchDelete({ ids: ["proj_1", "proj_2", "proj_3"] });

      expect(result).toEqual({ count: 3 });
    });
  });
});
