import { AllocationStatus, SystemRole } from "@capakraken/shared";
import { describe, expect, it, vi } from "vitest";
import { timelineRouter } from "../router/timeline.js";
import { createCallerFactory } from "../trpc.js";

vi.mock("../sse/event-bus.js", () => ({
  emitAllocationCreated: vi.fn(),
  emitAllocationDeleted: vi.fn(),
  emitAllocationUpdated: vi.fn(),
  emitProjectShifted: vi.fn(),
}));

vi.mock("../lib/budget-alerts.js", () => ({
  checkBudgetThresholds: vi.fn(),
}));

vi.mock("../lib/cache.js", () => ({
  invalidateDashboardCache: vi.fn(),
}));

const createCaller = createCallerFactory(timelineRouter);

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

describe("timeline allocation entry resolution", () => {
  it("creates a quick assignment without dual-writing a legacy allocation row", async () => {
    const createdAssignment = {
      id: "assignment_quick_1",
      demandRequirementId: null,
      resourceId: "resource_1",
      projectId: "project_1",
      startDate: new Date("2026-03-16"),
      endDate: new Date("2026-03-20"),
      hoursPerDay: 8,
      percentage: 100,
      role: "Team Member",
      roleId: null,
      dailyCostCents: 40000,
      status: AllocationStatus.PROPOSED,
      metadata: { source: "quickAssign" },
      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: null,
      demandRequirement: null,
    };

    const db = {
      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,
          },
        }),
      },
      allocation: {
        findMany: vi.fn().mockResolvedValue([]),
        create: vi.fn(),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([]),
        create: vi.fn().mockResolvedValue(createdAssignment),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      auditLog: {
        create: vi.fn().mockResolvedValue({}),
      },
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const caller = createManagerCaller(db);
    const result = await caller.quickAssign({
      resourceId: "resource_1",
      projectId: "project_1",
      startDate: new Date("2026-03-16"),
      endDate: new Date("2026-03-20"),
      hoursPerDay: 8,
      role: "Team Member",
      status: AllocationStatus.PROPOSED,
    });

    expect(result.id).toBe("assignment_quick_1");
    expect(result.isPlaceholder).toBe(false);
    expect(db.allocation.create).not.toHaveBeenCalled();
    expect(db.assignment.create).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          resourceId: "resource_1",
          metadata: { source: "quickAssign" },
        }),
      }),
    );
  });

  it("updates an explicit assignment through updateAllocationInline", async () => {
    const existingAssignment = {
      id: "assignment_1",
      demandRequirementId: null,
      resourceId: "resource_1",
      projectId: "project_1",
      startDate: new Date("2026-03-16"),
      endDate: new Date("2026-03-20"),
      hoursPerDay: 4,
      percentage: 50,
      role: "Compositor",
      roleId: "role_comp",
      dailyCostCents: 20000,
      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,
        availability: {
          monday: 8,
          tuesday: 8,
          wednesday: 8,
          thursday: 8,
          friday: 8,
          saturday: 0,
          sunday: 0,
        },
      },
      project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
      roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
      demandRequirement: null,
    };
    const updatedAssignment = {
      ...existingAssignment,
      hoursPerDay: 6,
      endDate: new Date("2026-03-21"),
      percentage: 75,
      dailyCostCents: 30000,
      metadata: { includeSaturday: true },
      updatedAt: new Date("2026-03-14"),
    };

    const db = {
      allocation: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
      demandRequirement: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
      assignment: {
        findUnique: vi.fn().mockResolvedValue(existingAssignment),
        update: vi.fn().mockResolvedValue(updatedAssignment),
      },
      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,
          },
        }),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      auditLog: {
        create: vi.fn().mockResolvedValue({}),
      },
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const caller = createManagerCaller(db);
    const result = await caller.updateAllocationInline({
      allocationId: "assignment_1",
      hoursPerDay: 6,
      endDate: new Date("2026-03-21"),
      includeSaturday: true,
    });

    expect(result.id).toBe("assignment_1");
    expect(result.hoursPerDay).toBe(6);
    expect(db.assignment.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "assignment_1" },
      }),
    );
  });

  it("extracts a visible subrange into its own assignment fragment", async () => {
    const existingAssignment = {
      id: "assignment_1",
      demandRequirementId: null,
      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.CONFIRMED,
      metadata: {},
      createdAt: new Date("2026-03-13"),
      updatedAt: new Date("2026-03-13"),
      resource: {
        id: "resource_1",
        displayName: "Alice",
        eid: "E-001",
        lcrCents: 5000,
        availability: {
          monday: 8,
          tuesday: 8,
          wednesday: 8,
          thursday: 8,
          friday: 8,
          saturday: 0,
          sunday: 0,
        },
      },
      project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
      roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
      demandRequirement: null,
    };

    const db = {
      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,
          },
        }),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      demandRequirement: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
      assignment: {
        findUnique: vi.fn().mockResolvedValue(existingAssignment),
        findMany: vi.fn().mockResolvedValue([]),
        update: vi.fn().mockImplementation(async ({ data }: { data: Record<string, unknown> }) => ({
          ...existingAssignment,
          ...data,
          metadata: data.metadata ?? existingAssignment.metadata,
        })),
        create: vi.fn()
          .mockResolvedValueOnce({
            ...existingAssignment,
            id: "assignment_left",
            startDate: new Date("2026-03-16"),
            endDate: new Date("2026-03-20"),
          })
          .mockResolvedValueOnce({
            ...existingAssignment,
            id: "assignment_right",
            startDate: new Date("2026-03-26"),
            endDate: new Date("2026-03-27"),
          }),
      },
      auditLog: {
        create: vi.fn().mockResolvedValue({}),
      },
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const caller = createManagerCaller(db);
    const result = await caller.extractAllocationFragment({
      allocationId: "assignment_1",
      startDate: new Date("2026-03-23"),
      endDate: new Date("2026-03-25"),
    });

    expect(result).toEqual({
      action: "extracted",
      allocationGroupId: expect.any(String),
      extractedAllocationId: "assignment_1",
      updatedAllocationIds: ["assignment_1"],
      createdAllocationIds: ["assignment_left", "assignment_right"],
      projectId: "project_1",
      resourceId: "resource_1",
    });
    expect(db.assignment.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "assignment_1" },
        data: expect.objectContaining({
          startDate: new Date("2026-03-23"),
          endDate: new Date("2026-03-25"),
        }),
      }),
    );
    expect(db.assignment.create).toHaveBeenCalledTimes(2);
  });

  it("falls back to default rules when calculationRule and vacation tables are missing", async () => {
    const existingAssignment = {
      id: "assignment_legacy_1",
      demandRequirementId: null,
      resourceId: "resource_1",
      projectId: "project_1",
      startDate: new Date("2026-03-16"),
      endDate: new Date("2026-03-20"),
      hoursPerDay: 4,
      percentage: 50,
      role: "Compositor",
      roleId: "role_comp",
      dailyCostCents: 20000,
      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,
        availability: {
          monday: 8,
          tuesday: 8,
          wednesday: 8,
          thursday: 8,
          friday: 8,
          saturday: 0,
          sunday: 0,
        },
      },
      project: { id: "project_1", name: "Project One", shortCode: "PRJ" },
      roleEntity: { id: "role_comp", name: "Compositor", color: "#111111" },
      demandRequirement: null,
    };
    const missingVacationTableError = {
      code: "P2021",
      message: "The table `public.vacation` does not exist in the current database.",
      meta: { table: "public.vacation" },
    };
    const missingCalculationRuleTableError = {
      code: "P2021",
      message: "The table `public.calculation_rule` does not exist in the current database.",
      meta: { table: "public.calculation_rule" },
    };

    const db = {
      allocation: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
      demandRequirement: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
      assignment: {
        findUnique: vi.fn().mockResolvedValue(existingAssignment),
        update: vi.fn().mockImplementation(async ({ data }: { data: Record<string, unknown> }) => ({
          ...existingAssignment,
          ...data,
          metadata: data.metadata ?? existingAssignment.metadata,
          updatedAt: new Date("2026-03-14"),
        })),
      },
      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,
          },
        }),
      },
      vacation: {
        findMany: vi.fn().mockRejectedValue(missingVacationTableError),
      },
      calculationRule: {
        findMany: vi.fn().mockRejectedValue(missingCalculationRuleTableError),
      },
      auditLog: {
        create: vi.fn().mockResolvedValue({}),
      },
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const caller = createManagerCaller(db);
    const result = await caller.updateAllocationInline({
      allocationId: "assignment_legacy_1",
      hoursPerDay: 6,
      endDate: new Date("2026-03-21"),
      includeSaturday: true,
    });

    expect(result.id).toBe("assignment_legacy_1");
    expect(db.vacation.findMany).toHaveBeenCalledTimes(1);
    expect(db.calculationRule.findMany).toHaveBeenCalledTimes(1);
    expect(db.assignment.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          dailyCostCents: expect.any(Number),
          metadata: expect.objectContaining({ includeSaturday: true }),
        }),
      }),
    );
  });

  it("updates an explicit demand row through updateAllocationInline", async () => {
    const existingDemand = {
      id: "demand_1",
      projectId: "project_1",
      startDate: new Date("2026-03-16"),
      endDate: new Date("2026-03-20"),
      hoursPerDay: 4,
      percentage: 50,
      role: "FX Artist",
      roleId: "role_fx",
      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_fx", name: "FX Artist", color: "#222222" },
    };
    const updatedDemand = {
      ...existingDemand,
      hoursPerDay: 6,
      endDate: new Date("2026-03-21"),
      percentage: 50,
      metadata: { includeSaturday: true },
      updatedAt: new Date("2026-03-14"),
    };

    const db = {
      allocation: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
      demandRequirement: {
        findUnique: vi.fn().mockResolvedValue(existingDemand),
        update: vi.fn().mockResolvedValue(updatedDemand),
      },
      assignment: {
        findUnique: vi.fn().mockResolvedValue(null),
        update: vi.fn(),
      },
      resource: {
        findUnique: vi.fn(),
      },
      vacation: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      auditLog: {
        create: vi.fn().mockResolvedValue({}),
      },
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const caller = createManagerCaller(db);
    const result = await caller.updateAllocationInline({
      allocationId: "demand_1",
      hoursPerDay: 6,
      endDate: new Date("2026-03-21"),
      includeSaturday: true,
    });

    expect(result.id).toBe("demand_1");
    expect(result.hoursPerDay).toBe(6);
    expect(db.resource.findUnique).not.toHaveBeenCalled();
    expect(db.demandRequirement.update).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { id: "demand_1" },
      }),
    );
  });

  it("returns resolved holiday overlays for assigned resources", async () => {
    const db = {
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "assignment_1",
            kind: "assignment",
            resourceId: "resource_by",
            projectId: "project_1",
            startDate: new Date("2026-01-01"),
            endDate: new Date("2026-01-31"),
            hoursPerDay: 8,
            status: AllocationStatus.CONFIRMED,
            metadata: {},
            project: {
              id: "project_1",
              name: "Project One",
              shortCode: "PRJ",
              status: "ACTIVE",
              startDate: new Date("2026-01-01"),
              endDate: new Date("2026-03-31"),
              orderType: "CHARGEABLE",
              clientId: null,
            },
            resource: {
              id: "resource_by",
              displayName: "Alice",
              eid: "E-001",
              chapter: null,
            },
          },
        ]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "resource_by",
            countryId: "country_de",
            federalState: "BY",
            metroCityId: null,
            country: { code: "DE" },
            metroCity: null,
          },
        ]),
      },
      project: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      holidayCalendar: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      country: {
        findUnique: vi.fn(),
      },
      metroCity: {
        findUnique: vi.fn(),
      },
    };

    const caller = createManagerCaller(db);
    const overlays = await caller.getHolidayOverlays({
      startDate: new Date("2026-01-01"),
      endDate: new Date("2026-01-31"),
    });

    expect(overlays).toEqual(
      expect.arrayContaining([
        expect.objectContaining({
          resourceId: "resource_by",
          type: "PUBLIC_HOLIDAY",
          note: "Heilige Drei Könige",
        }),
      ]),
    );
  });
});
