import { TRPCError } from "@trpc/server";
import { beforeEach, describe, expect, it, vi } from "vitest";

const {
  loadAllocationEntryMock,
  updateAssignmentMock,
  createAssignmentMock,
  deleteAllocationEntryMock,
} = vi.hoisted(() => ({
  loadAllocationEntryMock: vi.fn(),
  updateAssignmentMock: vi.fn(),
  createAssignmentMock: vi.fn(),
  deleteAllocationEntryMock: vi.fn(),
}));

vi.mock("@capakraken/application", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@capakraken/application")>();
  return {
    ...actual,
    loadAllocationEntry: loadAllocationEntryMock,
    updateAssignment: updateAssignmentMock,
    createAssignment: createAssignmentMock,
    deleteAllocationEntry: deleteAllocationEntryMock,
  };
});

import {
  carveTimelineAllocationRange,
  extractTimelineAllocationFragment,
} from "../router/timeline-allocation-fragment-support.js";

function createResolvedAssignment() {
  return {
    kind: "assignment" as const,
    entry: {
      id: "assignment_1",
      startDate: new Date("2026-04-06T00:00:00.000Z"),
      endDate: new Date("2026-04-17T00:00:00.000Z"),
      hoursPerDay: 8,
      metadata: {},
    },
    assignment: {
      id: "assignment_1",
      demandRequirementId: null,
      resourceId: "resource_1",
      projectId: "project_1",
      startDate: new Date("2026-04-06T00:00:00.000Z"),
      endDate: new Date("2026-04-17T00:00:00.000Z"),
      hoursPerDay: 8,
      percentage: 100,
      role: "Artist",
      roleId: "role_1",
      dailyCostCents: 80000,
      status: "ACTIVE",
      metadata: {},
    },
    projectId: "project_1",
    resourceId: "resource_1",
  };
}

describe("timeline allocation fragment support", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("splits an assignment into left and right fragments around the carved range", async () => {
    loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment());
    updateAssignmentMock.mockResolvedValue({ id: "assignment_1" });
    createAssignmentMock.mockResolvedValue({ id: "assignment_2" });

    const db = {
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const result = await carveTimelineAllocationRange({
      db: db as never,
      allocationId: "assignment_1",
      startDate: new Date("2026-04-09T00:00:00.000Z"),
      endDate: new Date("2026-04-10T00:00:00.000Z"),
    });

    expect(result).toEqual({
      action: "split",
      allocationGroupId: expect.any(String),
      updatedAllocationIds: ["assignment_1"],
      createdAllocationIds: ["assignment_2"],
      deletedAllocationIds: [],
      projectId: "project_1",
      resourceId: "resource_1",
    });
    expect(updateAssignmentMock).toHaveBeenCalledWith(
      db,
      "assignment_1",
      expect.objectContaining({
        startDate: new Date("2026-04-06T00:00:00.000Z"),
        endDate: new Date("2026-04-08T00:00:00.000Z"),
      }),
    );
    expect(createAssignmentMock).toHaveBeenCalledWith(
      db,
      expect.objectContaining({
        resourceId: "resource_1",
        projectId: "project_1",
        startDate: new Date("2026-04-11T00:00:00.000Z"),
        endDate: new Date("2026-04-17T00:00:00.000Z"),
      }),
    );
  });

  it("shrinks the existing assignment when carving from the start edge", async () => {
    loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment());
    updateAssignmentMock.mockResolvedValue({ id: "assignment_1" });

    const db = {
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const result = await carveTimelineAllocationRange({
      db: db as never,
      allocationId: "assignment_1",
      startDate: new Date("2026-04-06T00:00:00.000Z"),
      endDate: new Date("2026-04-08T00:00:00.000Z"),
    });

    expect(result.action).toBe("updated");
    expect(createAssignmentMock).not.toHaveBeenCalled();
    expect(updateAssignmentMock).toHaveBeenCalledWith(
      db,
      "assignment_1",
      expect.objectContaining({
        startDate: new Date("2026-04-09T00:00:00.000Z"),
        endDate: new Date("2026-04-17T00:00:00.000Z"),
      }),
    );
  });

  it("deletes the assignment when the carved range covers the full interval", async () => {
    loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment());

    const db = {
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const result = await carveTimelineAllocationRange({
      db: db as never,
      allocationId: "assignment_1",
      startDate: new Date("2026-04-06T00:00:00.000Z"),
      endDate: new Date("2026-04-17T00:00:00.000Z"),
    });

    expect(result.action).toBe("deleted");
    expect(deleteAllocationEntryMock).toHaveBeenCalledWith(
      db,
      expect.objectContaining({
        assignment: expect.objectContaining({ id: "assignment_1" }),
      }),
    );
  });

  it("rejects carve ranges outside the allocation interval", async () => {
    loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment());

    await expect(
      carveTimelineAllocationRange({
        db: { $transaction: vi.fn() } as never,
        allocationId: "assignment_1",
        startDate: new Date("2026-04-05T00:00:00.000Z"),
        endDate: new Date("2026-04-06T00:00:00.000Z"),
      }),
    ).rejects.toThrowError(
      new TRPCError({
        code: "BAD_REQUEST",
        message: "The requested carve range must be fully inside the existing allocation.",
      }),
    );
  });

  it("extracts a middle segment into the original assignment and creates siblings", async () => {
    loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment());
    updateAssignmentMock.mockResolvedValue({ id: "assignment_1" });
    createAssignmentMock
      .mockResolvedValueOnce({ id: "assignment_left" })
      .mockResolvedValueOnce({ id: "assignment_right" });

    const db = {
      $transaction: vi.fn(async (callback: (tx: unknown) => unknown) => callback(db)),
    };

    const result = await extractTimelineAllocationFragment({
      db: db as never,
      allocationId: "assignment_1",
      startDate: new Date("2026-04-09T00:00:00.000Z"),
      endDate: new Date("2026-04-10T00:00:00.000Z"),
    });

    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(updateAssignmentMock.mock.invocationCallOrder[0]).toBeLessThan(
      createAssignmentMock.mock.invocationCallOrder[0] ?? Number.POSITIVE_INFINITY,
    );
    expect(updateAssignmentMock).toHaveBeenCalledWith(
      db,
      "assignment_1",
      expect.objectContaining({
        startDate: new Date("2026-04-09T00:00:00.000Z"),
        endDate: new Date("2026-04-10T00:00:00.000Z"),
      }),
    );
    expect(createAssignmentMock).toHaveBeenNthCalledWith(
      1,
      db,
      expect.objectContaining({
        startDate: new Date("2026-04-06T00:00:00.000Z"),
        endDate: new Date("2026-04-08T00:00:00.000Z"),
      }),
    );
    expect(createAssignmentMock).toHaveBeenNthCalledWith(
      2,
      db,
      expect.objectContaining({
        startDate: new Date("2026-04-11T00:00:00.000Z"),
        endDate: new Date("2026-04-17T00:00:00.000Z"),
      }),
    );
  });

  it("returns unchanged when extracting the full assignment range", async () => {
    loadAllocationEntryMock.mockResolvedValue(createResolvedAssignment());

    const result = await extractTimelineAllocationFragment({
      db: { $transaction: vi.fn() } as never,
      allocationId: "assignment_1",
      startDate: new Date("2026-04-06T00:00:00.000Z"),
      endDate: new Date("2026-04-17T00:00:00.000Z"),
    });

    expect(result).toEqual({
      action: "unchanged",
      allocationGroupId: expect.any(String),
      extractedAllocationId: "assignment_1",
      updatedAllocationIds: [],
      createdAllocationIds: [],
      projectId: "project_1",
      resourceId: "resource_1",
    });
    expect(updateAssignmentMock).not.toHaveBeenCalled();
    expect(createAssignmentMock).not.toHaveBeenCalled();
  });
});
