import { SystemRole } from "@capakraken/shared";
import { beforeEach, describe, expect, it, vi } from "vitest";

vi.mock("@capakraken/application", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@capakraken/application")>();
  return {
    ...actual,
    listAssignmentBookings: vi.fn(),
  };
});

vi.mock("../lib/anonymization.js", async (importOriginal) => {
  const actual = await importOriginal<typeof import("../lib/anonymization.js")>();
  return {
    ...actual,
    getAnonymizationDirectory: vi.fn().mockResolvedValue(null),
  };
});

import { listAssignmentBookings } from "@capakraken/application";
import { timelineRouter } from "../router/timeline.js";
import { createCallerFactory } from "../trpc.js";

const createCaller = createCallerFactory(timelineRouter);

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

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

describe("timeline router detail views", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("returns a self-service timeline view scoped to the caller's linked resource", async () => {
    const demandFindMany = vi.fn();
    const assignmentFindMany = vi.fn().mockResolvedValue([
      {
        id: "asg_self",
        projectId: "project_1",
        resourceId: "res_self",
        role: "Artist",
        hoursPerDay: 6,
        startDate: new Date("2026-01-05T00:00:00.000Z"),
        endDate: new Date("2026-01-09T00:00:00.000Z"),
        status: "CONFIRMED",
        metadata: null,
        resource: {
          id: "res_self",
          displayName: "Alice",
          eid: "EMP-SELF",
          chapter: "Delivery",
          lcrCents: 10000,
        },
        project: {
          id: "project_1",
          name: "Gelddruckmaschine",
          shortCode: "GDM",
          clientId: "client_1",
          budgetCents: 100000,
          winProbability: 100,
          status: "ACTIVE",
          startDate: new Date("2026-01-05T00:00:00.000Z"),
          endDate: new Date("2026-01-16T00:00:00.000Z"),
          staffingReqs: null,
          responsiblePerson: "Larissa",
          color: "#fff",
          orderType: "CHARGEABLE",
        },
        roleEntity: null,
      },
    ]);

    const caller = createProtectedCaller({
      demandRequirement: {
        findMany: demandFindMany,
      },
      assignment: {
        findMany: assignmentFindMany,
      },
      resource: {
        findFirst: vi.fn().mockResolvedValue({ id: "res_self" }),
      },
      project: {
        findMany: vi.fn(),
      },
    });

    const result = await caller.getMyEntriesView({
      startDate: new Date("2026-01-05T00:00:00.000Z"),
      endDate: new Date("2026-01-09T00:00:00.000Z"),
      resourceIds: ["res_other"],
      chapters: ["Finance"],
      eids: ["EMP-OTHER"],
      countryCodes: ["US"],
    });

    expect(result.assignments).toHaveLength(1);
    expect(result.assignments[0]?.resourceId).toBe("res_self");
    expect(demandFindMany).not.toHaveBeenCalled();
    expect(assignmentFindMany).toHaveBeenCalledWith(expect.objectContaining({
      where: expect.objectContaining({
        resourceId: { in: ["res_self"] },
      }),
    }));
  });

  it("returns self-service holiday overlays for the caller's linked resource", async () => {
    const demandFindMany = vi.fn();
    const assignmentFindMany = vi.fn().mockResolvedValue([]);
    const resourceFindMany = vi.fn().mockResolvedValue([
      {
        id: "res_self",
        countryId: "country_de",
        federalState: "BY",
        metroCityId: "city_munich",
        country: { code: "DE", name: "Germany" },
        metroCity: { name: "Muenchen" },
      },
    ]);

    const caller = createProtectedCaller({
      demandRequirement: {
        findMany: demandFindMany,
      },
      assignment: {
        findMany: assignmentFindMany,
      },
      resource: {
        findFirst: vi.fn().mockResolvedValue({ id: "res_self" }),
        findMany: resourceFindMany,
      },
      project: {
        findMany: vi.fn(),
      },
    });

    const result = await caller.getMyHolidayOverlays({
      startDate: new Date("2026-01-05T00:00:00.000Z"),
      endDate: new Date("2026-01-09T00:00:00.000Z"),
      resourceIds: ["res_other"],
      chapters: ["Finance"],
      eids: ["EMP-OTHER"],
      countryCodes: ["US"],
    });

    expect(result).toEqual([
      expect.objectContaining({
        resourceId: "res_self",
        note: "Heilige Drei Könige",
        scope: "STATE",
        countryCode: "DE",
        countryName: "Germany",
        federalState: "BY",
        metroCityName: "Muenchen",
      }),
    ]);
    expect(demandFindMany).not.toHaveBeenCalled();
    expect(assignmentFindMany).toHaveBeenCalledWith(expect.objectContaining({
      where: expect.objectContaining({
        resourceId: { in: ["res_self"] },
      }),
    }));
    expect(resourceFindMany).toHaveBeenCalledWith(expect.objectContaining({
      where: { id: { in: ["res_self"] } },
    }));
  });

  it("returns empty self-service timeline data when the caller has no linked resource", async () => {
    const demandFindMany = vi.fn();
    const assignmentFindMany = vi.fn();

    const caller = createProtectedCaller({
      demandRequirement: {
        findMany: demandFindMany,
      },
      assignment: {
        findMany: assignmentFindMany,
      },
      resource: {
        findFirst: vi.fn().mockResolvedValue(null),
      },
      project: {
        findMany: vi.fn(),
      },
    });

    const result = await caller.getMyEntriesView({
      startDate: new Date("2026-01-05T00:00:00.000Z"),
      endDate: new Date("2026-01-09T00:00:00.000Z"),
    });

    expect(result.allocations).toEqual([]);
    expect(result.demands).toEqual([]);
    expect(result.assignments).toEqual([]);
    expect(demandFindMany).not.toHaveBeenCalled();
    expect(assignmentFindMany).not.toHaveBeenCalled();
  });

  it("returns empty self-service holiday overlays when the caller has no linked resource", async () => {
    const demandFindMany = vi.fn();
    const assignmentFindMany = vi.fn();
    const resourceFindMany = vi.fn();

    const caller = createProtectedCaller({
      demandRequirement: {
        findMany: demandFindMany,
      },
      assignment: {
        findMany: assignmentFindMany,
      },
      resource: {
        findFirst: vi.fn().mockResolvedValue(null),
        findMany: resourceFindMany,
      },
      project: {
        findMany: vi.fn(),
      },
    });

    const result = await caller.getMyHolidayOverlays({
      startDate: new Date("2026-01-05T00:00:00.000Z"),
      endDate: new Date("2026-01-09T00:00:00.000Z"),
    });

    expect(result).toEqual([]);
    expect(demandFindMany).not.toHaveBeenCalled();
    expect(assignmentFindMany).not.toHaveBeenCalled();
    expect(resourceFindMany).not.toHaveBeenCalled();
  });

  it("returns a detailed timeline entries view with holiday overlays and summary", async () => {
    const caller = createAdminCaller({
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "dem_1",
            projectId: "project_1",
            resourceId: null,
            role: "Artist",
            hoursPerDay: 6,
            startDate: new Date("2026-01-05T00:00:00.000Z"),
            endDate: new Date("2026-01-09T00:00:00.000Z"),
            status: "OPEN",
            metadata: null,
            project: {
              id: "project_1",
              name: "Gelddruckmaschine",
              shortCode: "GDM",
              clientId: "client_1",
              budgetCents: 100000,
              winProbability: 100,
              status: "ACTIVE",
              startDate: new Date("2026-01-05T00:00:00.000Z"),
              endDate: new Date("2026-01-16T00:00:00.000Z"),
              staffingReqs: null,
              responsiblePerson: "Larissa",
              color: "#fff",
              orderType: "CHARGEABLE",
            },
            roleEntity: null,
          },
        ]),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "asg_by",
            projectId: "project_1",
            resourceId: "res_by",
            role: "Artist",
            hoursPerDay: 6,
            startDate: new Date("2026-01-05T00:00:00.000Z"),
            endDate: new Date("2026-01-09T00:00:00.000Z"),
            status: "CONFIRMED",
            metadata: null,
            resource: {
              id: "res_by",
              displayName: "Alice",
              eid: "EMP-BY",
              chapter: "Delivery",
              lcrCents: 10000,
            },
            project: {
              id: "project_1",
              name: "Gelddruckmaschine",
              shortCode: "GDM",
              clientId: "client_1",
              budgetCents: 100000,
              winProbability: 100,
              status: "ACTIVE",
              startDate: new Date("2026-01-05T00:00:00.000Z"),
              endDate: new Date("2026-01-16T00:00:00.000Z"),
              staffingReqs: null,
              responsiblePerson: "Larissa",
              color: "#fff",
              orderType: "CHARGEABLE",
            },
            roleEntity: null,
          },
          {
            id: "asg_hh",
            projectId: "project_1",
            resourceId: "res_hh",
            role: "Artist",
            hoursPerDay: 6,
            startDate: new Date("2026-01-05T00:00:00.000Z"),
            endDate: new Date("2026-01-09T00:00:00.000Z"),
            status: "CONFIRMED",
            metadata: null,
            resource: {
              id: "res_hh",
              displayName: "Bob",
              eid: "EMP-HH",
              chapter: "Delivery",
              lcrCents: 10000,
            },
            project: {
              id: "project_1",
              name: "Gelddruckmaschine",
              shortCode: "GDM",
              clientId: "client_1",
              budgetCents: 100000,
              winProbability: 100,
              status: "ACTIVE",
              startDate: new Date("2026-01-05T00:00:00.000Z"),
              endDate: new Date("2026-01-16T00:00:00.000Z"),
              staffingReqs: null,
              responsiblePerson: "Larissa",
              color: "#fff",
              orderType: "CHARGEABLE",
            },
            roleEntity: null,
          },
        ]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "res_by",
            countryId: "country_de",
            federalState: "BY",
            metroCityId: "city_munich",
            country: { code: "DE", name: "Germany" },
            metroCity: { name: "Muenchen" },
          },
          {
            id: "res_hh",
            countryId: "country_de",
            federalState: "HH",
            metroCityId: "city_hamburg",
            country: { code: "DE", name: "Germany" },
            metroCity: { name: "Hamburg" },
          },
        ]),
      },
      project: {
        findMany: vi.fn(),
      },
    });

    const result = await caller.getEntriesDetail({
      startDate: "2026-01-05",
      endDate: "2026-01-09",
      projectIds: ["project_1"],
    });

    expect(result.period).toEqual({
      startDate: "2026-01-05",
      endDate: "2026-01-09",
    });
    expect(result.summary).toEqual(
      expect.objectContaining({
        demandCount: 1,
        assignmentCount: 2,
        overlayCount: 1,
        resourceCount: 2,
      }),
    );
    expect(result.demands).toHaveLength(1);
    expect(result.assignments).toHaveLength(2);
    expect(result.holidayOverlays).toEqual([
      expect.objectContaining({
        resourceId: "res_by",
        startDate: "2026-01-06",
        note: "Heilige Drei Könige",
        scope: "STATE",
        countryCode: "DE",
        countryName: "Germany",
        federalState: "BY",
        metroCityName: "Muenchen",
      }),
    ]);
  });

  it("returns detailed project timeline context with overlap summaries", async () => {
    vi.mocked(listAssignmentBookings).mockResolvedValue([
      {
        id: "asg_project",
        projectId: "project_ctx",
        resourceId: "res_1",
        startDate: new Date("2026-01-05T00:00:00.000Z"),
        endDate: new Date("2026-01-16T00:00:00.000Z"),
        hoursPerDay: 6,
        dailyCostCents: 0,
        status: "CONFIRMED",
        project: { id: "project_ctx", name: "Gelddruckmaschine", shortCode: "GDM", status: "ACTIVE", orderType: "CHARGEABLE", dynamicFields: null },
        resource: { id: "res_1", displayName: "Alice", chapter: "Delivery" },
      },
      {
        id: "asg_other",
        projectId: "project_other",
        resourceId: "res_1",
        startDate: new Date("2026-01-08T00:00:00.000Z"),
        endDate: new Date("2026-01-10T00:00:00.000Z"),
        hoursPerDay: 4,
        dailyCostCents: 0,
        status: "CONFIRMED",
        project: { id: "project_other", name: "Other Project", shortCode: "OTH", status: "ACTIVE", orderType: "CHARGEABLE", dynamicFields: null },
        resource: { id: "res_1", displayName: "Alice", chapter: "Delivery" },
      },
    ]);

    const project = {
      id: "project_ctx",
      name: "Gelddruckmaschine",
      shortCode: "GDM",
      orderType: "CHARGEABLE",
      budgetCents: 100000,
      winProbability: 100,
      status: "ACTIVE",
      startDate: new Date("2026-01-05T00:00:00.000Z"),
      endDate: new Date("2026-01-16T00:00:00.000Z"),
      staffingReqs: null,
    };

    const caller = createAdminCaller({
      project: {
        findUnique: vi.fn().mockResolvedValue(project),
      },
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "dem_ctx",
            projectId: "project_ctx",
            resourceId: null,
            role: "Artist",
            hoursPerDay: 6,
            startDate: new Date("2026-01-05T00:00:00.000Z"),
            endDate: new Date("2026-01-16T00:00:00.000Z"),
            status: "OPEN",
            metadata: null,
            project,
            roleEntity: null,
          },
        ]),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "asg_project",
            projectId: "project_ctx",
            resourceId: "res_1",
            role: "Artist",
            hoursPerDay: 6,
            startDate: new Date("2026-01-05T00:00:00.000Z"),
            endDate: new Date("2026-01-16T00:00:00.000Z"),
            status: "CONFIRMED",
            metadata: null,
            resource: {
              id: "res_1",
              displayName: "Alice",
              eid: "EMP-1",
              chapter: "Delivery",
              lcrCents: 10000,
            },
            project,
            roleEntity: null,
          },
        ]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "res_1",
            countryId: "country_de",
            federalState: "BY",
            metroCityId: "city_munich",
            country: { code: "DE" },
            metroCity: { name: "Muenchen" },
          },
        ]),
      },
    });

    const result = await caller.getProjectContextDetail({
      projectId: "project_ctx",
    });

    expect(result.project).toEqual(
      expect.objectContaining({
        id: "project_ctx",
        shortCode: "GDM",
      }),
    );
    expect(result.summary).toEqual(
      expect.objectContaining({
        demandCount: 1,
        assignmentCount: 1,
        conflictedAssignmentCount: 1,
        overlayCount: 1,
      }),
    );
    expect(result.assignmentConflicts).toEqual([
      expect.objectContaining({
        assignmentId: "asg_project",
        crossProjectOverlapCount: 1,
        overlaps: expect.arrayContaining([
          expect.objectContaining({
            projectShortCode: "OTH",
            sameProject: false,
          }),
        ]),
      }),
    ]);
    expect(result.holidayOverlays).toEqual([
      expect.objectContaining({
        startDate: "2026-01-06",
      }),
    ]);
  });

  it("returns detailed project shift preview metadata and validation", async () => {
    const projectFindUnique = vi.fn().mockImplementation((args: { select?: Record<string, unknown> }) => {
      if (args.select && "budgetCents" in args.select) {
        return Promise.resolve({
          id: "project_shift",
          budgetCents: 100000,
          winProbability: 100,
          startDate: new Date("2026-01-05T00:00:00.000Z"),
          endDate: new Date("2026-01-16T00:00:00.000Z"),
        });
      }

      return Promise.resolve({
        id: "project_shift",
        name: "Gelddruckmaschine",
        shortCode: "GDM",
        status: "ACTIVE",
        responsiblePerson: "Larissa",
        startDate: new Date("2026-01-05T00:00:00.000Z"),
        endDate: new Date("2026-01-16T00:00:00.000Z"),
      });
    });

    const caller = createAdminCaller({
      project: {
        findUnique: projectFindUnique,
      },
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      assignment: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    });

    const result = await caller.getShiftPreviewDetail({
      projectId: "project_shift",
      newStartDate: new Date("2026-01-19T00:00:00.000Z"),
      newEndDate: new Date("2026-01-30T00:00:00.000Z"),
    });

    expect(result.project).toEqual({
      id: "project_shift",
      name: "Gelddruckmaschine",
      shortCode: "GDM",
      status: "ACTIVE",
      responsiblePerson: "Larissa",
      startDate: "2026-01-05",
      endDate: "2026-01-16",
    });
    expect(result.requestedShift).toEqual({
      newStartDate: "2026-01-19",
      newEndDate: "2026-01-30",
    });
    expect(result.preview).toEqual({
      valid: true,
      errors: [],
      warnings: [],
      conflictDetails: [],
      costImpact: {
        currentTotalCents: 0,
        newTotalCents: 0,
        deltaCents: 0,
        budgetCents: 100000,
        budgetUtilizationBefore: 0,
        budgetUtilizationAfter: 0,
        wouldExceedBudget: false,
      },
    });
  });

  it("blocks USER role from broad timeline detail reads", async () => {
    const db = {
      demandRequirement: {
        findMany: vi.fn(),
      },
      assignment: {
        findMany: vi.fn(),
      },
      resource: {
        findMany: vi.fn(),
      },
      project: {
        findMany: vi.fn(),
      },
    };

    const caller = createProtectedCaller(db);
    await expect(
      caller.getEntriesDetail({
        startDate: "2026-01-05",
        endDate: "2026-01-09",
      }),
    ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" }));
  });

  it("blocks USER role from project timeline context reads", async () => {
    const db = {
      project: {
        findUnique: vi.fn(),
      },
      demandRequirement: {
        findMany: vi.fn(),
      },
      assignment: {
        findMany: vi.fn(),
      },
      resource: {
        findMany: vi.fn(),
      },
    };

    const caller = createProtectedCaller(db);
    await expect(
      caller.getProjectContextDetail({ projectId: "project_ctx" }),
    ).rejects.toThrow(expect.objectContaining({ code: "FORBIDDEN" }));
  });

  it("returns budget status details for a project", async () => {
    vi.mocked(listAssignmentBookings).mockResolvedValue([
      {
        id: "asg_confirmed",
        projectId: "project_budget",
        resourceId: "res_1",
        startDate: new Date("2026-01-05T00:00:00.000Z"),
        endDate: new Date("2026-01-09T00:00:00.000Z"),
        hoursPerDay: 8,
        dailyCostCents: 10000,
        status: "CONFIRMED",
        project: null,
        resource: null,
      },
      {
        id: "asg_proposed",
        projectId: "project_budget",
        resourceId: "res_2",
        startDate: new Date("2026-01-12T00:00:00.000Z"),
        endDate: new Date("2026-01-16T00:00:00.000Z"),
        hoursPerDay: 4,
        dailyCostCents: 5000,
        status: "PROPOSED",
        project: null,
        resource: null,
      },
    ] as never);

    const projectFindUnique = vi.fn().mockResolvedValue({
      id: "project_budget",
      name: "Gelddruckmaschine",
      shortCode: "GDM",
      budgetCents: 100000,
      winProbability: 80,
      startDate: new Date("2026-01-05T00:00:00.000Z"),
      endDate: new Date("2026-01-31T00:00:00.000Z"),
    });

    const caller = createAdminCaller({
      project: {
        findUnique: projectFindUnique,
      },
    });

    const result = await caller.getBudgetStatus({ projectId: "project_budget" });

    expect(projectFindUnique).toHaveBeenCalledWith({
      where: { id: "project_budget" },
      select: {
        id: true,
        name: true,
        shortCode: true,
        budgetCents: true,
        winProbability: true,
        startDate: true,
        endDate: true,
      },
    });
    expect(listAssignmentBookings).toHaveBeenCalledWith(expect.anything(), {
      projectIds: ["project_budget"],
    });
    expect(result).toEqual(
      expect.objectContaining({
        projectName: "Gelddruckmaschine",
        projectCode: "GDM",
        totalAllocations: 2,
        budgetCents: 100000,
        confirmedCents: 50000,
        proposedCents: 25000,
        allocatedCents: 75000,
        remainingCents: 25000,
        winProbabilityWeightedCents: 60000,
      }),
    );
    expect(result.utilizationPercent).toBeCloseTo(75, 5);
    expect(result.warnings).toEqual([
      expect.objectContaining({
        level: "info",
        code: "BUDGET_INFO",
      }),
    ]);
  });

  it("returns NOT_FOUND for budget status when the project does not exist", async () => {
    const caller = createAdminCaller({
      project: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
    });

    await expect(
      caller.getBudgetStatus({ projectId: "project_missing" }),
    ).rejects.toThrow(expect.objectContaining({
      code: "NOT_FOUND",
      message: "Project not found",
    }));
  });
});
