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

// Full demand requirement shape expected by loadAllocationEntry → buildSplitAllocationReadModel
const makeDemandRequirement = (overrides: Record<string, unknown> = {}) => ({
  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,
  metadata: {},
  createdAt: new Date("2026-03-13"),
  updatedAt: new Date("2026-03-13"),
  project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
  roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
  ...overrides,
});

const makeAssignmentRecord = (overrides: Record<string, unknown> = {}) => ({
  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,
});

/**
 * Builds a minimal DB mock for fillOpenDemand.
 * The allocationId is resolved through findAllocationEntry which calls
 * demandRequirement.findUnique and assignment.findUnique in parallel.
 * If the allocationId matches a demand, that path is taken.
 * If the allocationId matches an assignment (non-null), "already filled" is thrown.
 */
function makeDb({
  demandRecord = makeDemandRequirement(),
  assignmentRecord = null,
}: {
  demandRecord?: Record<string, unknown> | null;
  assignmentRecord?: Record<string, unknown> | null;
} = {}) {
  const assignmentCreate = vi.fn().mockResolvedValue(makeAssignmentRecord());
  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: {
      // loadAllocationEntry uses findUnique with include (DEMAND_REQUIREMENT_RELATIONS_INCLUDE)
      findUnique: vi.fn().mockResolvedValue(demandRecord),
    },
    assignment: {
      // loadAllocationEntry also probes assignment.findUnique
      findUnique: vi.fn().mockResolvedValue(assignmentRecord),
      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 },
      }),
    ),
    _assignmentCreate: assignmentCreate,
    _demandRequirementUpdate: demandRequirementUpdate,
  };
}

describe("fillOpenDemand", () => {
  it("happy path: fills an open demand entry and returns demand_requirement strategy", async () => {
    const db = makeDb();

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

    expect(result.strategy).toBe("demand_requirement");
    expect(result.createdAllocation.id).toBe("assignment_1");
    expect(result.createdAllocation.projectId).toBe("project_1");
    expect(result.createdAllocation.resourceId).toBe("resource_1");
    expect(result.updatedAllocation).not.toBeNull();
    expect(result.updatedAllocation?.id).toBe("demand_1");
    expect(result.updatedAllocation?.resourceId).toBeNull();
  });

  it("throws NOT_FOUND when the allocationId does not resolve to any record", async () => {
    const db = makeDb({ demandRecord: null, assignmentRecord: null });

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

  it("throws BAD_REQUEST when the allocation is already filled (is an assignment)", async () => {
    // When the allocationId resolves to an assignment (not a demand), it is already filled
    const db = makeDb({
      demandRecord: null,
      assignmentRecord: makeAssignmentRecord() as unknown as Record<string, unknown>,
    });

    await expect(
      fillOpenDemand(db as never, {
        allocationId: "assignment_1",
        resourceId: "resource_2",
      }),
    ).rejects.toMatchObject({ code: "BAD_REQUEST" });

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

  it("propagates CANCELLED demand rejection from fillDemandRequirement", async () => {
    const db = makeDb({
      demandRecord: makeDemandRequirement({ status: AllocationStatus.CANCELLED }) as Record<
        string,
        unknown
      >,
    });

    // fillDemandRequirement will re-fetch the demand by ID and throw BAD_REQUEST
    // For this we also need the outer findUnique to return cancelled demand
    // (loadAllocationEntry uses findUnique with include, fillDemandRequirement uses findUnique with select)
    // We need to handle both calls. The mock returns the same shape for both.
    await expect(
      fillOpenDemand(db as never, {
        allocationId: "demand_1",
        resourceId: "resource_1",
      }),
    ).rejects.toMatchObject({ code: "BAD_REQUEST" });
  });

  it("passes optional hoursPerDay override to the underlying fill operation", async () => {
    const db = makeDb();

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

    expect(db._assignmentCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ hoursPerDay: 4 }),
      }),
    );
  });

  it("passes optional status override to the underlying fill operation", async () => {
    const db = makeDb();

    await fillOpenDemand(db as never, {
      allocationId: "demand_1",
      resourceId: "resource_1",
      status: AllocationStatus.CONFIRMED,
    });

    expect(db._assignmentCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({ status: AllocationStatus.CONFIRMED }),
      }),
    );
  });
});
