import { describe, expect, it, vi } from "vitest";
import { commitDispoImportBatch } from "../index.js";
import { deriveTbdDispoProjectIdentity } from "../use-cases/dispo-import/tbd-projects.js";

function createCommitDb(overrides: Record<string, unknown> = {}) {
  const tx = {
    importBatch: {
      update: vi.fn().mockResolvedValue({}),
    },
    utilizationCategory: {
      upsert: vi.fn().mockResolvedValue({}),
      findMany: vi.fn().mockResolvedValue([{ id: "util_chg", code: "Chg" }]),
    },
    role: {
      upsert: vi.fn().mockResolvedValue({}),
      findMany: vi.fn().mockResolvedValue([{ id: "role_ad", name: "Art Director" }]),
    },
    stagedResource: {
      findMany: vi.fn().mockResolvedValue([]),
      updateMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    stagedProject: {
      findMany: vi.fn().mockResolvedValue([]),
      updateMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    stagedAssignment: {
      findMany: vi.fn().mockResolvedValue([]),
      updateMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    stagedVacation: {
      findMany: vi.fn().mockResolvedValue([]),
      count: vi.fn().mockResolvedValue(0),
      updateMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    stagedAvailabilityRule: {
      findMany: vi.fn().mockResolvedValue([]),
      updateMany: vi.fn().mockResolvedValue({ count: 0 }),
    },
    user: {
      findFirst: vi.fn().mockResolvedValue({ id: "admin_1" }),
    },
    country: {
      findMany: vi.fn().mockResolvedValue([{ id: "country_de", code: "DE" }]),
    },
    metroCity: {
      findMany: vi.fn().mockResolvedValue([]),
    },
    managementLevelGroup: {
      findMany: vi.fn().mockResolvedValue([]),
    },
    managementLevel: {
      findMany: vi.fn().mockResolvedValue([]),
    },
    client: {
      findMany: vi.fn().mockResolvedValue([{ id: "client_bmw", code: "BMW", name: "BMW" }]),
    },
    orgUnit: {
      findMany: vi.fn().mockResolvedValue([{ id: "org_ad", level: 7, name: "Art Direction" }]),
    },
    resource: {
      upsert: vi.fn().mockResolvedValue({
        id: "res_1",
        availability: {
          monday: 8,
          tuesday: 8,
          wednesday: 8,
          thursday: 8,
          friday: 8,
        },
      }),
      update: vi.fn().mockResolvedValue({}),
    },
    resourceRole: {
      upsert: vi.fn().mockResolvedValue({}),
    },
    vacationEntitlement: {
      upsert: vi.fn().mockResolvedValue({}),
    },
    project: {
      upsert: vi.fn().mockResolvedValue({ id: "proj_1" }),
    },
    assignment: {
      upsert: vi.fn().mockResolvedValue({}),
    },
    vacation: {
      findFirst: vi.fn().mockResolvedValue(null),
      create: vi.fn().mockResolvedValue({}),
      update: vi.fn().mockResolvedValue({}),
    },
    ...overrides,
  };

  const db = {
    importBatch: {
      findUnique: vi.fn().mockResolvedValue({ id: "batch_1", status: "STAGED", summary: {} }),
      update: vi.fn().mockResolvedValue({}),
    },
    stagedVacation: {
      count: vi.fn().mockResolvedValue(0),
    },
    stagedUnresolvedRecord: {
      findMany: vi.fn().mockResolvedValue([]),
    },
    $transaction: vi.fn(async (callback: (client: typeof tx) => Promise<unknown>) => callback(tx)),
    ...overrides,
  };

  return { db, tx };
}

describe("commitDispoImportBatch", () => {
  it("commits resolved staged data and keeps [tbd] unresolved rows staged", async () => {
    const { db, tx } = createCommitDb();

    db.stagedUnresolvedRecord.findMany.mockResolvedValue([
      {
        id: "unresolved_1",
        recordType: "PROJECT",
        message: 'Planning token "[tbd]" references [tbd] and requires project resolution',
        resolutionHint: "Resolve [tbd] rows before final project creation",
        normalizedData: { rawToken: "[tbd]" },
        status: "UNRESOLVED",
      },
    ]);

    tx.stagedResource.findMany.mockResolvedValue([
      {
        id: "sr_charge",
        sourceKind: "CHARGEABILITY",
        canonicalExternalId: "ada.director",
        displayName: "Ada Director",
        email: null,
        chapter: "Art Direction",
        chargeabilityTarget: 77.5,
        clientUnitName: null,
        countryCode: "DE",
        fte: 1,
        lcrCents: null,
        managementLevelGroupName: null,
        managementLevelName: null,
        metroCityName: null,
        resourceType: "EMPLOYEE",
        roleTokens: ["AD"],
        ucrCents: null,
        availability: null,
        rawPayload: {},
        warnings: [],
      },
      {
        id: "sr_roster",
        sourceKind: "ROSTER",
        canonicalExternalId: "ada.director",
        displayName: "Ada Director",
        email: null,
        chapter: "Art Direction",
        chargeabilityTarget: null,
        clientUnitName: null,
        countryCode: "DE",
        fte: 1,
        lcrCents: 1000,
        managementLevelGroupName: null,
        managementLevelName: null,
        metroCityName: null,
        resourceType: "EMPLOYEE",
        roleTokens: ["AD"],
        ucrCents: 1500,
        availability: {
          monday: 8,
          tuesday: 8,
          wednesday: 8,
          thursday: 8,
          friday: 8,
        },
        rawPayload: {
          sapOrgUnitLevelSeven: "Art Direction",
          vacationDaysPerYear: 30,
        },
        warnings: [],
      },
    ]);

    tx.stagedProject.findMany.mockResolvedValue([
      {
        id: "sp_1",
        shortCode: "11035763",
        projectKey: "11035763",
        name: "BMW Launch",
        clientCode: "BMW",
        utilizationCategoryCode: "Chg",
        allocationType: "EXT",
        orderType: "CHARGEABLE",
        winProbability: 100,
        isInternal: false,
        isTbd: false,
        startDate: new Date("2026-01-05T00:00:00.000Z"),
        endDate: new Date("2026-01-06T00:00:00.000Z"),
        warnings: [],
      },
    ]);

    tx.stagedAssignment.findMany.mockResolvedValue([
      {
        id: "sa_1",
        resourceExternalId: "ada.director",
        projectKey: "11035763",
        assignmentDate: new Date("2026-01-05T00:00:00.000Z"),
        startDate: new Date("2026-01-05T00:00:00.000Z"),
        endDate: new Date("2026-01-05T00:00:00.000Z"),
        hoursPerDay: 4,
        percentage: 50,
        roleToken: "AD",
        roleName: "Art Director",
        utilizationCategoryCode: "Chg",
        isInternal: false,
        isUnassigned: false,
        isTbd: false,
      },
      {
        id: "sa_2",
        resourceExternalId: "ada.director",
        projectKey: "11035763",
        assignmentDate: new Date("2026-01-06T00:00:00.000Z"),
        startDate: new Date("2026-01-06T00:00:00.000Z"),
        endDate: new Date("2026-01-06T00:00:00.000Z"),
        hoursPerDay: 4,
        percentage: 50,
        roleToken: "AD",
        roleName: "Art Director",
        utilizationCategoryCode: "Chg",
        isInternal: false,
        isUnassigned: false,
        isTbd: false,
      },
    ]);

    tx.stagedVacation.findMany.mockResolvedValue([
      {
        id: "sv_1",
        resourceExternalId: "ada.director",
        vacationType: "ANNUAL",
        startDate: new Date("2026-01-08T00:00:00.000Z"),
        endDate: new Date("2026-01-09T00:00:00.000Z"),
        note: "Winter vacation",
        holidayName: null,
        isHalfDay: false,
        halfDayPart: null,
      },
    ]);

    tx.stagedAvailabilityRule.findMany.mockResolvedValue([
      {
        id: "sar_1",
        resourceExternalId: "ada.director",
        effectiveStartDate: new Date("2026-01-05T00:00:00.000Z"),
        effectiveEndDate: new Date("2026-01-05T00:00:00.000Z"),
        availableHours: 4,
        percentage: 50,
        ruleType: "PART_TIME",
      },
      {
        id: "sar_2",
        resourceExternalId: "ada.director",
        effectiveStartDate: new Date("2026-01-12T00:00:00.000Z"),
        effectiveEndDate: new Date("2026-01-12T00:00:00.000Z"),
        availableHours: 4,
        percentage: 50,
        ruleType: "PART_TIME",
      },
    ]);

    const result = await commitDispoImportBatch(db as never, {
      importBatchId: "batch_1",
    });

    expect(result).toEqual({
      batchId: "batch_1",
      counts: {
        committedAssignments: 1,
        committedProjects: 1,
        committedResources: 1,
        committedVacations: 1,
        updatedEntitlements: 1,
        updatedResourceAvailabilities: 1,
        upsertedResourceRoles: 2,
      },
      unresolved: {
        blocked: 0,
        skippedTbd: 1,
      },
    });

    expect(tx.resource.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        create: expect.objectContaining({
          eid: "ada.director",
          email: "ada.director@accenture.com",
          enterpriseId: "ada.director",
          lcrCents: 1000,
          ucrCents: 1500,
        }),
      }),
    );
    expect(tx.resource.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          availability: expect.objectContaining({
            monday: 4,
            tuesday: 8,
          }),
        }),
      }),
    );
    expect(tx.assignment.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        create: expect.objectContaining({
          dailyCostCents: 4000,
          endDate: new Date("2026-01-06T00:00:00.000Z"),
          startDate: new Date("2026-01-05T00:00:00.000Z"),
        }),
      }),
    );
    expect(tx.vacationEntitlement.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        create: expect.objectContaining({
          entitledDays: 30,
          year: 2026,
        }),
      }),
    );
    expect(tx.importBatch.update).toHaveBeenCalledWith(
      expect.objectContaining({
        data: expect.objectContaining({
          status: "COMMITTED",
        }),
      }),
    );
  });

  it("commits [tbd] rows as provisional projects when explicitly enabled", async () => {
    const { db, tx } = createCommitDb();
    const rawToken = "[DAI] 590 C AMG GT Stills [tbd]{CH} HB_";
    const tbdProject = deriveTbdDispoProjectIdentity(rawToken, "Chg");

    db.stagedUnresolvedRecord.findMany.mockResolvedValue([
      {
        id: "unresolved_1",
        recordType: "PROJECT",
        message: `Planning token "${rawToken}" references [tbd] and requires project resolution`,
        resolutionHint: "Resolve [tbd] rows to a real WBS/project before commit",
        normalizedData: { rawToken },
        status: "UNRESOLVED",
      },
    ]);

    tx.stagedResource.findMany.mockResolvedValue([
      {
        id: "sr_roster",
        sourceKind: "ROSTER",
        canonicalExternalId: "h.noerenberg",
        displayName: "Hartmut Norenberg",
        email: "h.noerenberg@accenture.com",
        chapter: "Art Direction",
        chargeabilityTarget: null,
        clientUnitName: null,
        countryCode: "DE",
        fte: 1,
        lcrCents: 1000,
        managementLevelGroupName: null,
        managementLevelName: null,
        metroCityName: null,
        resourceType: "EMPLOYEE",
        roleTokens: ["AD"],
        ucrCents: 1500,
        availability: {
          monday: 8,
          tuesday: 8,
          wednesday: 8,
          thursday: 8,
          friday: 8,
        },
        rawPayload: {},
        warnings: [],
      },
    ]);

    tx.stagedProject.findMany.mockResolvedValue([
      {
        id: "sp_tbd_1",
        shortCode: tbdProject.shortCode,
        projectKey: tbdProject.projectKey,
        name: tbdProject.name,
        clientCode: "DAI",
        utilizationCategoryCode: "Chg",
        allocationType: "EXT",
        orderType: "CHARGEABLE",
        winProbability: 100,
        isInternal: false,
        isTbd: true,
        startDate: new Date("2026-02-03T00:00:00.000Z"),
        endDate: new Date("2026-02-04T00:00:00.000Z"),
        warnings: [],
        rawPayload: {
          rawTokens: [rawToken],
        },
      },
    ]);

    tx.stagedAssignment.findMany.mockResolvedValue([
      {
        id: "sa_tbd_1",
        resourceExternalId: "h.noerenberg",
        projectKey: null,
        assignmentDate: new Date("2026-02-03T00:00:00.000Z"),
        startDate: new Date("2026-02-03T00:00:00.000Z"),
        endDate: new Date("2026-02-03T00:00:00.000Z"),
        hoursPerDay: 4,
        percentage: 50,
        roleToken: "AD",
        roleName: "Art Director",
        utilizationCategoryCode: "Chg",
        isInternal: false,
        isUnassigned: false,
        isTbd: true,
        rawPayload: {
          rawToken,
        },
      },
      {
        id: "sa_tbd_2",
        resourceExternalId: "h.noerenberg",
        projectKey: null,
        assignmentDate: new Date("2026-02-04T00:00:00.000Z"),
        startDate: new Date("2026-02-04T00:00:00.000Z"),
        endDate: new Date("2026-02-04T00:00:00.000Z"),
        hoursPerDay: 4,
        percentage: 50,
        roleToken: "AD",
        roleName: "Art Director",
        utilizationCategoryCode: "Chg",
        isInternal: false,
        isUnassigned: false,
        isTbd: true,
        rawPayload: {
          rawToken,
        },
      },
    ]);

    const result = await commitDispoImportBatch(db as never, {
      importBatchId: "batch_1",
      importTbdProjects: true,
    });

    expect(result).toEqual({
      batchId: "batch_1",
      counts: {
        committedAssignments: 1,
        committedProjects: 1,
        committedResources: 1,
        committedVacations: 0,
        updatedEntitlements: 0,
        updatedResourceAvailabilities: 0,
        upsertedResourceRoles: 2,
      },
      unresolved: {
        blocked: 0,
        skippedTbd: 1,
      },
    });
    expect(tx.project.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { shortCode: tbdProject.shortCode },
        create: expect.objectContaining({
          name: tbdProject.name,
          shortCode: tbdProject.shortCode,
          status: "DRAFT",
        }),
      }),
    );
    expect(tx.assignment.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        create: expect.objectContaining({
          endDate: new Date("2026-02-04T00:00:00.000Z"),
          startDate: new Date("2026-02-03T00:00:00.000Z"),
        }),
      }),
    );
    expect(tx.stagedProject.updateMany).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { importBatchId: "batch_1" },
      }),
    );
  });

  it("blocks commit when non-[tbd] unresolved rows remain", async () => {
    const { db } = createCommitDb();

    db.stagedUnresolvedRecord.findMany.mockResolvedValue([
      {
        id: "unresolved_blocker",
        recordType: "ASSIGNMENT",
        message: "Unable to resolve project key from planning token",
        resolutionHint: "Add a WBS token",
        normalizedData: { rawToken: "[BMW] Launch" },
        status: "UNRESOLVED",
      },
    ]);

    await expect(
      commitDispoImportBatch(db as never, { importBatchId: "batch_1" }),
    ).rejects.toThrow(/blocking unresolved staged record/);

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

  it("falls back to the staged resource role when an assignment row has no role", async () => {
    const { db, tx } = createCommitDb({
      role: {
        upsert: vi.fn().mockResolvedValue({}),
        findMany: vi.fn().mockResolvedValue([
          { id: "role_ad", name: "Art Director" },
          { id: "role_pm", name: "Project Manager" },
        ]),
      },
    });

    tx.stagedResource.findMany.mockResolvedValue([
      {
        id: "sr_roster",
        sourceKind: "ROSTER",
        canonicalExternalId: "catharina.voelkle",
        displayName: "Catharina Voelkle",
        email: null,
        chapter: "Art Direction",
        chargeabilityTarget: null,
        clientUnitName: null,
        countryCode: "DE",
        fte: 1,
        lcrCents: 1000,
        managementLevelGroupName: null,
        managementLevelName: null,
        metroCityName: null,
        resourceType: "EMPLOYEE",
        roleTokens: ["AD"],
        ucrCents: 1500,
        availability: {
          monday: 8,
          tuesday: 8,
          wednesday: 8,
          thursday: 8,
          friday: 8,
        },
        rawPayload: {
          department: "Digital Designer",
        },
        warnings: [],
      },
    ]);

    tx.stagedProject.findMany.mockResolvedValue([
      {
        id: "sp_1",
        shortCode: "generic",
        projectKey: "generic",
        name: "Generic Work",
        clientCode: "BMW",
        utilizationCategoryCode: "Chg",
        allocationType: "EXT",
        orderType: "CHARGEABLE",
        winProbability: 100,
        isInternal: false,
        isTbd: false,
        startDate: new Date("2026-03-12T00:00:00.000Z"),
        endDate: new Date("2026-03-12T00:00:00.000Z"),
        warnings: [],
      },
    ]);

    tx.stagedAssignment.findMany.mockResolvedValue([
      {
        id: "sa_missing_role",
        resourceExternalId: "catharina.voelkle",
        projectKey: "generic",
        assignmentDate: new Date("2026-03-12T00:00:00.000Z"),
        startDate: new Date("2026-03-12T00:00:00.000Z"),
        endDate: new Date("2026-03-12T00:00:00.000Z"),
        hoursPerDay: 8,
        percentage: 100,
        roleToken: null,
        roleName: null,
        utilizationCategoryCode: "Chg",
        isInternal: false,
        isUnassigned: false,
        isTbd: false,
      },
    ]);

    const result = await commitDispoImportBatch(db as never, {
      importBatchId: "batch_1",
    });

    expect(result.counts.committedAssignments).toBe(1);
    expect(tx.assignment.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        create: expect.objectContaining({
          role: "Art Director",
        }),
      }),
    );
  });

  it("creates a role from the roster department when no canonical dispo role exists", async () => {
    const { db, tx } = createCommitDb({
      role: {
        upsert: vi.fn(async ({ where }: { where: { name: string } }) => ({
          id: `role_${where.name.toLowerCase().replace(/\s+/g, "_")}`,
          name: where.name,
        })),
        findMany: vi.fn().mockResolvedValue([{ id: "role_ad", name: "Art Director" }]),
      },
    });

    tx.stagedResource.findMany.mockResolvedValue([
      {
        id: "sr_roster",
        sourceKind: "ROSTER",
        canonicalExternalId: "dennis.steinschulte",
        displayName: "Dennis Steinschulte",
        email: null,
        chapter: "Product Data Management",
        chargeabilityTarget: null,
        clientUnitName: null,
        countryCode: "DE",
        fte: 1,
        lcrCents: 1000,
        managementLevelGroupName: null,
        managementLevelName: null,
        metroCityName: null,
        resourceType: "EMPLOYEE",
        roleTokens: [],
        ucrCents: 1500,
        availability: {
          monday: 8,
          tuesday: 8,
          wednesday: 8,
          thursday: 8,
          friday: 8,
        },
        rawPayload: {
          department: "Vis Logic",
        },
        warnings: [],
      },
    ]);

    tx.stagedProject.findMany.mockResolvedValue([
      {
        id: "sp_1",
        shortCode: "INT-MO",
        projectKey: "generic",
        name: "Management & Operations",
        clientCode: "BMW",
        utilizationCategoryCode: "M&O",
        allocationType: "INT",
        orderType: "NONCHARGEABLE",
        winProbability: 100,
        isInternal: true,
        isTbd: false,
        startDate: new Date("2026-01-12T00:00:00.000Z"),
        endDate: new Date("2026-01-12T00:00:00.000Z"),
        warnings: [],
      },
    ]);

    tx.stagedAssignment.findMany.mockResolvedValue([
      {
        id: "sa_missing_role",
        resourceExternalId: "dennis.steinschulte",
        projectKey: "generic",
        assignmentDate: new Date("2026-01-12T00:00:00.000Z"),
        startDate: new Date("2026-01-12T00:00:00.000Z"),
        endDate: new Date("2026-01-12T00:00:00.000Z"),
        hoursPerDay: 8,
        percentage: 100,
        roleToken: null,
        roleName: null,
        utilizationCategoryCode: "M&O",
        isInternal: true,
        isUnassigned: false,
        isTbd: false,
      },
    ]);

    const result = await commitDispoImportBatch(db as never, {
      importBatchId: "batch_1",
    });

    expect(result.counts.committedAssignments).toBe(1);
    expect(tx.role.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        where: { name: "Vis Logic" },
      }),
    );
    expect(tx.assignment.upsert).toHaveBeenCalledWith(
      expect.objectContaining({
        create: expect.objectContaining({
          role: "Vis Logic",
        }),
      }),
    );
  });

  it("rejects staged PUBLIC_HOLIDAY rows until holiday calendars are synchronized", async () => {
    const { db, tx } = createCommitDb();

    db.stagedVacation.count.mockResolvedValue(2);

    await expect(
      commitDispoImportBatch(db as never, {
        importBatchId: "batch_1",
      }),
    ).rejects.toThrow(
      'Import batch "batch_1" still contains 2 staged PUBLIC_HOLIDAY row(s). Public holidays must be synchronized through holiday calendars before commit.',
    );
  });
});
