import { AllocationStatus } from "@nexus/shared";
import { TRPCError } from "@trpc/server";
import { describe, expect, it, vi } from "vitest";
import { fillDemandRequirement } from "../index.js";

// Minimal assignment shape returned from createAssignment inside the transaction
function makeAssignment(overrides: Record<string, unknown> = {}) {
  return {
    id: "assignment_1",
    demandRequirementId: "demand_1",
    resourceId: "resource_1",
    projectId: "project_1",
    startDate: new Date("2026-03-16"),
    endDate: new Date("2026-03-27"),
    hoursPerDay: 8,
    percentage: 100,
    role: "Compositor",
    roleId: "role_comp",
    dailyCostCents: 40000,
    status: AllocationStatus.PROPOSED,
    metadata: {},
    createdAt: new Date("2026-03-13"),
    updatedAt: new Date("2026-03-13"),
    resource: { id: "resource_1", displayName: "Alice", eid: "E-001", lcrCents: 5000 },
    project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
    roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
    demandRequirement: {
      id: "demand_1",
      projectId: "project_1",
      startDate: new Date("2026-03-16"),
      endDate: new Date("2026-03-27"),
      hoursPerDay: 8,
      percentage: 100,
      role: "Compositor",
      roleId: "role_comp",
      headcount: 1,
      status: AllocationStatus.PROPOSED,
    },
    ...overrides,
  };
}

function makeDb(
  demandOverride: Record<string, unknown> = {},
  txOverrides: Record<string, unknown> = {},
) {
  const demand = {
    id: "demand_1",
    projectId: "project_1",
    startDate: new Date("2026-03-16"),
    endDate: new Date("2026-03-27"),
    hoursPerDay: 8,
    role: "Compositor",
    roleId: "role_comp",
    headcount: 1,
    status: AllocationStatus.PROPOSED,
    metadata: {},
    ...demandOverride,
  };

  const assignmentCreate = vi.fn().mockResolvedValue(makeAssignment());
  const demandRequirementUpdate = vi.fn().mockResolvedValue({
    id: "demand_1",
    projectId: "project_1",
    headcount: 0,
    status: AllocationStatus.COMPLETED,
  });
  const auditLogCreate = vi.fn().mockResolvedValue({});
  const vacationFindMany = vi.fn().mockResolvedValue([]);
  const assignmentFindMany = vi.fn().mockResolvedValue([]);
  const resourceFindUnique = vi.fn().mockResolvedValue({
    id: "resource_1",
    lcrCents: 5000,
    availability: {
      monday: 8,
      tuesday: 8,
      wednesday: 8,
      thursday: 8,
      friday: 8,
      saturday: 0,
      sunday: 0,
    },
  });
  const projectFindUnique = vi.fn().mockResolvedValue({ id: "project_1" });

  return {
    demandRequirement: {
      findUnique: vi.fn().mockResolvedValue(demand),
    },
    assignment: { findMany: assignmentFindMany },
    $transaction: vi.fn(async (callback: (tx: unknown) => Promise<unknown>) =>
      callback({
        project: { findUnique: projectFindUnique },
        resource: { findUnique: resourceFindUnique },
        demandRequirement: {
          findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1" }),
          update: demandRequirementUpdate,
        },
        assignment: {
          findMany: assignmentFindMany,
          create: assignmentCreate,
        },
        vacation: { findMany: vacationFindMany },
        auditLog: { create: auditLogCreate },
        ...txOverrides,
      }),
    ),
    _assignmentCreate: assignmentCreate,
    _demandRequirementUpdate: demandRequirementUpdate,
  };
}

describe("fillDemandRequirement", () => {
  it("happy path: fills an open demand with a valid resource assignment", async () => {
    const db = makeDb();

    const result = await fillDemandRequirement(db as never, {
      demandRequirementId: "demand_1",
      resourceId: "resource_1",
    });

    expect(result.assignment.id).toBe("assignment_1");
    expect(result.assignment.resourceId).toBe("resource_1");
    expect(result.updatedDemandRequirement.id).toBe("demand_1");
    expect(db._assignmentCreate).toHaveBeenCalledOnce();
    expect(db._demandRequirementUpdate).toHaveBeenCalledOnce();
  });

  it("throws NOT_FOUND when the demand requirement does not exist", async () => {
    const db = {
      demandRequirement: { findUnique: vi.fn().mockResolvedValue(null) },
      assignment: { findMany: vi.fn().mockResolvedValue([]) },
      $transaction: vi.fn(),
    };

    await expect(
      fillDemandRequirement(db as never, {
        demandRequirementId: "nonexistent",
        resourceId: "resource_1",
      }),
    ).rejects.toThrow(TRPCError);

    await expect(
      fillDemandRequirement(db as never, {
        demandRequirementId: "nonexistent",
        resourceId: "resource_1",
      }),
    ).rejects.toMatchObject({ code: "NOT_FOUND" });
  });

  it("throws BAD_REQUEST when demand requirement is CANCELLED", async () => {
    const db = makeDb({ status: AllocationStatus.CANCELLED });

    await expect(
      fillDemandRequirement(db as never, {
        demandRequirementId: "demand_1",
        resourceId: "resource_1",
      }),
    ).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("cancelled") });

    // No transaction should have been initiated
    expect(db.$transaction).not.toHaveBeenCalled();
  });

  it("throws BAD_REQUEST when demand requirement is already COMPLETED", async () => {
    const db = makeDb({ status: AllocationStatus.COMPLETED });

    await expect(
      fillDemandRequirement(db as never, {
        demandRequirementId: "demand_1",
        resourceId: "resource_1",
      }),
    ).rejects.toMatchObject({ code: "BAD_REQUEST", message: expect.stringContaining("completed") });

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

  it("throws CONFLICT when the same resource is already assigned to the same project with overlapping dates", async () => {
    // assignment.findMany returns an existing booking for the same resource+project+dates
    const existingBooking = {
      id: "assignment_existing",
      projectId: "project_1",
      resourceId: "resource_1",
      startDate: new Date("2026-03-10"),
      endDate: new Date("2026-03-20"),
      hoursPerDay: 8,
      dailyCostCents: 40000,
      status: AllocationStatus.CONFIRMED,
      project: {
        id: "project_1",
        name: "Project One",
        shortCode: "PRJ",
        status: "ACTIVE",
        orderType: "EXTERNAL",
        clientId: null,
        dynamicFields: null,
      },
      resource: { id: "resource_1", displayName: "Alice", chapter: null },
    };

    const db = {
      demandRequirement: {
        findUnique: vi.fn().mockResolvedValue({
          id: "demand_1",
          projectId: "project_1",
          startDate: new Date("2026-03-16"),
          endDate: new Date("2026-03-27"),
          hoursPerDay: 8,
          role: "Compositor",
          roleId: "role_comp",
          headcount: 1,
          status: AllocationStatus.PROPOSED,
          metadata: {},
        }),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([existingBooking]),
      },
      $transaction: vi.fn(),
    };

    await expect(
      fillDemandRequirement(db as never, {
        demandRequirementId: "demand_1",
        resourceId: "resource_1",
      }),
    ).rejects.toMatchObject({ code: "CONFLICT" });

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

  it("allows filling when existing assignment is for a different project (no duplicate)", async () => {
    const differentProjectBooking = {
      id: "assignment_other",
      projectId: "project_2", // different project — should not trigger duplicate check
      resourceId: "resource_1",
      startDate: new Date("2026-03-16"),
      endDate: new Date("2026-03-27"),
      hoursPerDay: 8,
      dailyCostCents: 40000,
      status: AllocationStatus.CONFIRMED,
      project: {
        id: "project_2",
        name: "Project Two",
        shortCode: "PT2",
        status: "ACTIVE",
        orderType: "EXTERNAL",
        clientId: null,
        dynamicFields: null,
      },
      resource: { id: "resource_1", displayName: "Alice", chapter: null },
    };

    // Outer findMany (duplicate check in fillDemandRequirement) sees the different-project booking.
    // Inside the transaction, createAssignment calls its own listAssignmentBookings — that must
    // return empty so the availability validator doesn't see an overallocation.
    const outerFindMany = vi.fn().mockResolvedValue([differentProjectBooking]);
    const txFindMany = vi.fn().mockResolvedValue([]);

    const assignmentCreate = vi.fn().mockResolvedValue(makeAssignment());
    const demandRequirementUpdate = vi.fn().mockResolvedValue({
      id: "demand_1",
      projectId: "project_1",
      headcount: 0,
      status: AllocationStatus.COMPLETED,
    });

    const db = {
      demandRequirement: {
        findUnique: vi.fn().mockResolvedValue({
          id: "demand_1",
          projectId: "project_1",
          startDate: new Date("2026-03-16"),
          endDate: new Date("2026-03-27"),
          hoursPerDay: 8,
          role: "Compositor",
          roleId: "role_comp",
          headcount: 1,
          status: AllocationStatus.PROPOSED,
          metadata: {},
        }),
      },
      assignment: { findMany: outerFindMany },
      $transaction: vi.fn(async (callback: (tx: unknown) => Promise<unknown>) =>
        callback({
          project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1" }) },
          resource: {
            findUnique: vi.fn().mockResolvedValue({
              id: "resource_1",
              lcrCents: 5000,
              availability: {
                monday: 8,
                tuesday: 8,
                wednesday: 8,
                thursday: 8,
                friday: 8,
                saturday: 0,
                sunday: 0,
              },
            }),
          },
          demandRequirement: {
            findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1" }),
            update: demandRequirementUpdate,
          },
          assignment: { findMany: txFindMany, create: assignmentCreate },
          vacation: { findMany: vi.fn().mockResolvedValue([]) },
          auditLog: { create: vi.fn().mockResolvedValue({}) },
        }),
      ),
    };

    const result = await fillDemandRequirement(db as never, {
      demandRequirementId: "demand_1",
      resourceId: "resource_1",
    });

    expect(result.assignment.id).toBe("assignment_1");
  });

  it("uses input hoursPerDay when provided, overriding demand default", async () => {
    const db = makeDb();

    await fillDemandRequirement(db as never, {
      demandRequirementId: "demand_1",
      resourceId: "resource_1",
      hoursPerDay: 4,
    });

    // createAssignment is called inside the transaction — verify it received hoursPerDay: 4
    expect(db._assignmentCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ hoursPerDay: 4 }),
      }),
    );
  });

  it("decrement headcount when demand has multiple headcount (headcount > 1)", async () => {
    const demandRequirementUpdate = vi.fn().mockResolvedValue({
      id: "demand_1",
      projectId: "project_1",
      headcount: 1,
      status: AllocationStatus.PROPOSED,
    });

    const db = {
      demandRequirement: {
        findUnique: vi.fn().mockResolvedValue({
          id: "demand_1",
          projectId: "project_1",
          startDate: new Date("2026-03-16"),
          endDate: new Date("2026-03-27"),
          hoursPerDay: 8,
          role: "Compositor",
          roleId: "role_comp",
          headcount: 2,
          status: AllocationStatus.PROPOSED,
          metadata: {},
        }),
      },
      assignment: { findMany: vi.fn().mockResolvedValue([]) },
      $transaction: vi.fn(async (callback: (tx: unknown) => Promise<unknown>) =>
        callback({
          project: { findUnique: vi.fn().mockResolvedValue({ id: "project_1" }) },
          resource: {
            findUnique: vi.fn().mockResolvedValue({
              id: "resource_1",
              lcrCents: 5000,
              availability: {
                monday: 8,
                tuesday: 8,
                wednesday: 8,
                thursday: 8,
                friday: 8,
                saturday: 0,
                sunday: 0,
              },
            }),
          },
          demandRequirement: {
            findUnique: vi.fn().mockResolvedValue({ id: "demand_1", projectId: "project_1" }),
            update: demandRequirementUpdate,
          },
          assignment: {
            findMany: vi.fn().mockResolvedValue([]),
            create: vi.fn().mockResolvedValue(makeAssignment()),
          },
          vacation: { findMany: vi.fn().mockResolvedValue([]) },
          auditLog: { create: vi.fn().mockResolvedValue({}) },
        }),
      ),
    };

    const result = await fillDemandRequirement(db as never, {
      demandRequirementId: "demand_1",
      resourceId: "resource_1",
    });

    expect(result.updatedDemandRequirement.headcount).toBe(1);
    expect(result.updatedDemandRequirement.status).toBe(AllocationStatus.PROPOSED);
    expect(demandRequirementUpdate).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { headcount: 1 },
      }),
    );
  });

  it("marks demand COMPLETED when it is the last headcount seat", async () => {
    const db = makeDb({ headcount: 1 });

    const result = await fillDemandRequirement(db as never, {
      demandRequirementId: "demand_1",
      resourceId: "resource_1",
    });

    expect(result.updatedDemandRequirement.status).toBe(AllocationStatus.COMPLETED);
    expect(db._demandRequirementUpdate).toHaveBeenCalledWith(
      expect.objectContaining({
        data: { status: AllocationStatus.COMPLETED },
      }),
    );
  });
});
