import { describe, expect, it, vi } from "vitest";
import {
  getDashboardSkillGaps,
  getDashboardSkillGapSummary,
  getDashboardTopValueResources,
  getDashboardProjectHealth,
  getDashboardPeakTimes,
} from "../index.js";

// ---------------------------------------------------------------------------
// getDashboardSkillGaps
// ---------------------------------------------------------------------------

describe("getDashboardSkillGaps", () => {
  it("computes gaps between open demands and skilled resources", async () => {
    const db = {
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([
          {
            role: "Compositor",
            roleId: "role_comp",
            roleEntity: { name: "Compositor" },
            headcount: 3,
            metadata: null,
          },
          {
            role: "FX Artist",
            roleId: "role_fx",
            roleEntity: { name: "FX Artist" },
            headcount: 2,
            metadata: null,
          },
        ]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          {
            skills: [
              { name: "Compositor", level: 4 },
              { name: "FX Artist", level: 2 },
            ],
          },
          { skills: [{ name: "Compositor", level: 3 }] },
        ]),
      },
    };

    const result = await getDashboardSkillGaps(db as never);

    // FX Artist: demand 2, supply 0 (level 2 < 3) → gap -2 (largest shortage first)
    // Compositor: demand 3, supply 2 → gap -1
    expect(result).toHaveLength(2);
    expect(result[0]).toEqual({ skill: "FX Artist", demand: 2, supply: 0, gap: -2 });
    expect(result[1]).toEqual({ skill: "Compositor", demand: 3, supply: 2, gap: -1 });
  });

  it("returns empty array when there are no open demands", async () => {
    const db = {
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      resource: {
        findMany: vi.fn(),
      },
    };

    const result = await getDashboardSkillGaps(db as never);

    expect(result).toEqual([]);
    // resource.findMany should never be called when there are no demands
    expect(db.resource.findMany).not.toHaveBeenCalled();
  });

  it("extracts required skills from demand metadata when present", async () => {
    const db = {
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([
          {
            role: "Generalist",
            roleId: "role_gen",
            roleEntity: { name: "Generalist" },
            headcount: 2,
            metadata: { requiredSkills: ["Houdini", "Nuke"] },
          },
        ]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          { skills: [{ name: "Houdini", level: 5 }] },
          {
            skills: [
              { name: "Nuke", level: 3 },
              { name: "Houdini", level: 3 },
            ],
          },
        ]),
      },
    };

    const result = await getDashboardSkillGaps(db as never);

    // Both skills demanded: 2 each
    // Houdini supply: 2, gap 0; Nuke supply: 1, gap -1
    const nukeRow = result.find((row) => row.skill === "Nuke");
    const houdiniRow = result.find((row) => row.skill === "Houdini");
    expect(nukeRow).toEqual({ skill: "Nuke", demand: 2, supply: 1, gap: -1 });
    expect(houdiniRow).toEqual({ skill: "Houdini", demand: 2, supply: 2, gap: 0 });
    // Sorted by gap ascending → Nuke first
    expect(result[0]?.skill).toBe("Nuke");
  });

  it("caps output at 10 rows sorted by largest shortage first", async () => {
    const manyDemands = Array.from({ length: 12 }, (_, i) => ({
      role: `Role_${i}`,
      roleId: `role_${i}`,
      roleEntity: { name: `Role_${i}` },
      headcount: i + 1,
      metadata: null,
    }));

    const db = {
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue(manyDemands),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const result = await getDashboardSkillGaps(db as never);

    expect(result).toHaveLength(10);
    // All gaps are negative (no supply); sorted by ascending gap → largest demand first
    for (let i = 0; i < result.length - 1; i++) {
      expect(result[i]?.gap ?? 0).toBeLessThanOrEqual(result[i + 1]?.gap ?? 0);
    }
  });
});

// ---------------------------------------------------------------------------
// getDashboardSkillGapSummary
// ---------------------------------------------------------------------------

describe("getDashboardSkillGapSummary", () => {
  it("returns role gaps, open positions total, skill supply and resource-by-role counts", async () => {
    const db = {
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([
          {
            role: "Compositor",
            headcount: 3,
            roleEntity: { name: "Compositor" },
            _count: { assignments: 1 },
          },
          {
            role: "Compositor",
            headcount: 2,
            roleEntity: { name: "Compositor" },
            _count: { assignments: 2 },
          },
          {
            role: "FX Artist",
            headcount: 2,
            roleEntity: { name: "FX Artist" },
            _count: { assignments: 0 },
          },
        ]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          {
            skills: [{ name: "Houdini" }, { skill: "Nuke" }],
            areaRole: { name: "FX Artist" },
          },
          {
            skills: [{ name: "Houdini" }],
            areaRole: { name: "Compositor" },
          },
        ]),
      },
    };

    const result = await getDashboardSkillGapSummary(db as never);

    // Compositor: needed=5, filled=min(1,3)+min(2,2)=1+2=3, gap=2
    // FX Artist: needed=2, filled=0, gap=2
    expect(result.roleGaps).toHaveLength(2);
    expect(result.totalOpenPositions).toBe(4);

    const compositorGap = result.roleGaps.find((row) => row.role === "Compositor");
    expect(compositorGap).toMatchObject({ needed: 5, filled: 3, gap: 2, fillRate: 60 });

    const fxGap = result.roleGaps.find((row) => row.role === "FX Artist");
    expect(fxGap).toMatchObject({ needed: 2, filled: 0, gap: 2, fillRate: 0 });

    // Skill supply: "houdini" appears twice
    expect(result.skillSupplyTop10[0]).toEqual({ skill: "houdini", resourceCount: 2 });

    // Resources by role
    expect(result.resourcesByRole).toContainEqual({ role: "FX Artist", count: 1 });
    expect(result.resourcesByRole).toContainEqual({ role: "Compositor", count: 1 });
  });

  it("returns empty role gaps and zero open positions when all demands are filled", async () => {
    const db = {
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([
          {
            role: "Compositor",
            headcount: 2,
            roleEntity: { name: "Compositor" },
            _count: { assignments: 3 }, // more than headcount → capped at headcount
          },
        ]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const result = await getDashboardSkillGapSummary(db as never);

    expect(result.roleGaps).toEqual([]);
    expect(result.totalOpenPositions).toBe(0);
  });

  it("returns empty summary when no demands exist", async () => {
    const db = {
      demandRequirement: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const result = await getDashboardSkillGapSummary(db as never);

    expect(result.roleGaps).toEqual([]);
    expect(result.totalOpenPositions).toBe(0);
    expect(result.skillSupplyTop10).toEqual([]);
    expect(result.resourcesByRole).toEqual([]);
  });
});

// ---------------------------------------------------------------------------
// getDashboardTopValueResources
// ---------------------------------------------------------------------------

describe("getDashboardTopValueResources", () => {
  it("returns resources with value scores for authorised roles", async () => {
    const db = {
      systemSettings: {
        findUnique: vi.fn().mockResolvedValue({
          scoreVisibleRoles: ["ADMIN", "MANAGER"],
        }),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "res_1",
            eid: "E001",
            displayName: "Alice",
            chapter: "CGI",
            valueScore: 95,
            valueScoreBreakdown: {
              skillDepth: 20,
              skillBreadth: 15,
              costEfficiency: 20,
              chargeability: 20,
              experience: 20,
              total: 95,
            },
            valueScoreUpdatedAt: new Date("2026-01-01T00:00:00.000Z"),
            lcrCents: 80_000,
            country: { code: "DE", name: "Germany" },
            federalState: "BY",
            metroCity: { name: "Munich" },
          },
        ]),
      },
    };

    const result = await getDashboardTopValueResources(db as never, {
      limit: 10,
      userRole: "ADMIN",
    });

    expect(result).toHaveLength(1);
    expect(result[0]).toMatchObject({
      id: "res_1",
      eid: "E001",
      displayName: "Alice",
      chapter: "CGI",
      valueScore: 95,
      lcrCents: 80_000,
      countryCode: "DE",
      countryName: "Germany",
      federalState: "BY",
      metroCityName: "Munich",
    });
    expect(result[0]?.valueScoreBreakdown).toMatchObject({ total: 95 });
  });

  it("returns empty array for roles not in the visible-roles list", async () => {
    const db = {
      systemSettings: {
        findUnique: vi.fn().mockResolvedValue({
          scoreVisibleRoles: ["ADMIN"],
        }),
      },
      resource: {
        findMany: vi.fn(),
      },
    };

    const result = await getDashboardTopValueResources(db as never, {
      limit: 10,
      userRole: "VIEWER",
    });

    expect(result).toEqual([]);
    expect(db.resource.findMany).not.toHaveBeenCalled();
  });

  it("falls back to default visible roles when settings are null", async () => {
    const db = {
      systemSettings: {
        findUnique: vi.fn().mockResolvedValue(null),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    // Default visible roles are ["ADMIN", "MANAGER"]
    const adminResult = await getDashboardTopValueResources(db as never, {
      limit: 5,
      userRole: "ADMIN",
    });
    expect(adminResult).toEqual([]);
    expect(db.resource.findMany).toHaveBeenCalledOnce();

    const viewerResult = await getDashboardTopValueResources(db as never, {
      limit: 5,
      userRole: "VIEWER",
    });
    expect(viewerResult).toEqual([]);
    // findMany should not have been called for VIEWER
    expect(db.resource.findMany).toHaveBeenCalledOnce();
  });

  it("normalises a malformed valueScoreBreakdown to null", async () => {
    const db = {
      systemSettings: {
        findUnique: vi.fn().mockResolvedValue({
          scoreVisibleRoles: ["ADMIN"],
        }),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "res_2",
            eid: "E002",
            displayName: "Bob",
            chapter: null,
            valueScore: 42,
            valueScoreBreakdown: { corrupted: true }, // not a valid breakdown
            valueScoreUpdatedAt: null,
            lcrCents: 60_000,
            country: null,
            federalState: null,
            metroCity: null,
          },
        ]),
      },
    };

    const result = await getDashboardTopValueResources(db as never, {
      limit: 5,
      userRole: "ADMIN",
    });

    expect(result[0]?.valueScoreBreakdown).toBeNull();
    expect(result[0]?.countryCode).toBeNull();
    expect(result[0]?.metroCityName).toBeNull();
  });
});

// ---------------------------------------------------------------------------
// getDashboardProjectHealth
// ---------------------------------------------------------------------------

describe("getDashboardProjectHealth", () => {
  it("computes budget, staffing and timeline health scores for an active project", async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-04-10T00:00:00.000Z"));

    try {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "proj_1",
              name: "Alpha",
              shortCode: "ALPHA",
              status: "ACTIVE",
              budgetCents: 100_000,
              endDate: new Date("2026-12-31T00:00:00.000Z"), // ON_TRACK
              clientId: "client_1",
              client: { name: "ACME" },
              demandRequirements: [
                {
                  id: "dr_1",
                  headcount: 2,
                  status: "CONFIRMED",
                  assignments: [{ id: "a1" }], // 1 of 2 filled
                },
              ],
            },
          ]),
        },
        assignment: {
          findMany: vi.fn().mockResolvedValue([
            {
              projectId: "proj_1",
              startDate: new Date("2026-03-01T00:00:00.000Z"),
              endDate: new Date("2026-03-03T00:00:00.000Z"),
              dailyCostCents: 10_000,
              resource: {
                id: "res_1",
                availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
                countryId: null,
                federalState: null,
                metroCityId: null,
                country: null,
                metroCity: null,
              },
            },
          ]),
        },
      };

      const result = await getDashboardProjectHealth(db as never);

      expect(result).toHaveLength(1);
      const row = result[0]!;
      expect(row.shortCode).toBe("ALPHA");
      expect(row.clientName).toBe("ACME");
      // budgetCents=100_000, spentCents=20_000 (2 working days Mon+Tue)
      // pctUsed=20 → budgetHealth=80
      expect(row.budgetHealth).toBe(80);
      expect(row.spentCents).toBe(20_000);
      expect(row.budgetUtilizationPercent).toBe(20);
      // staffingHealth: 1 filled / 2 needed = 50
      expect(row.staffingHealth).toBe(50);
      // timelineHealth: end date is in the future → 100
      expect(row.timelineHealth).toBe(100);
      expect(row.timelineStatus).toBe("ON_TRACK");
      // compositeScore = round((80+50+100)/3) = round(76.67) = 77
      expect(row.compositeScore).toBe(77);
      expect(row.demandHeadcountTotal).toBe(2);
      expect(row.demandHeadcountFilled).toBe(1);
      expect(row.demandHeadcountOpen).toBe(1);
    } finally {
      vi.useRealTimers();
    }
  });

  it("returns empty array when no active projects exist", async () => {
    const db = {
      project: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      assignment: {
        findMany: vi.fn(),
      },
    };

    const result = await getDashboardProjectHealth(db as never);

    expect(result).toEqual([]);
    expect(db.assignment.findMany).not.toHaveBeenCalled();
  });

  it("marks a project OVERDUE and sets timelineHealth=0 when end date has passed", async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-04-10T00:00:00.000Z"));

    try {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "proj_2",
              name: "Beta",
              shortCode: "BETA",
              status: "ACTIVE",
              budgetCents: null,
              endDate: new Date("2026-01-01T00:00:00.000Z"), // in the past
              clientId: null,
              client: null,
              demandRequirements: [],
            },
          ]),
        },
        assignment: {
          findMany: vi.fn().mockResolvedValue([]),
        },
      };

      const result = await getDashboardProjectHealth(db as never);

      expect(result).toHaveLength(1);
      const row = result[0]!;
      expect(row.timelineHealth).toBe(0);
      expect(row.timelineStatus).toBe("OVERDUE");
      // No budget defined → budgetHealth=100, no staffing demands → staffingHealth=100
      // compositeScore = round((100+100+0)/3) = 67
      expect(row.compositeScore).toBe(67);
    } finally {
      vi.useRealTimers();
    }
  });

  it("sorts results by composite score ascending (worst-health project first)", async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-04-10T00:00:00.000Z"));

    try {
      const db = {
        project: {
          findMany: vi.fn().mockResolvedValue([
            {
              id: "proj_good",
              name: "Healthy",
              shortCode: "GOOD",
              status: "ACTIVE",
              budgetCents: 100_000,
              endDate: new Date("2026-12-31T00:00:00.000Z"),
              clientId: null,
              client: null,
              demandRequirements: [],
            },
            {
              id: "proj_bad",
              name: "Overdue",
              shortCode: "BAD",
              status: "ACTIVE",
              budgetCents: 1_000,
              endDate: new Date("2025-01-01T00:00:00.000Z"), // overdue
              clientId: null,
              client: null,
              demandRequirements: [
                {
                  id: "dr_1",
                  headcount: 5,
                  status: "CONFIRMED",
                  assignments: [], // 0 filled
                },
              ],
            },
          ]),
        },
        assignment: {
          findMany: vi.fn().mockResolvedValue([
            {
              projectId: "proj_bad",
              startDate: new Date("2026-03-01T00:00:00.000Z"),
              endDate: new Date("2026-03-31T00:00:00.000Z"),
              dailyCostCents: 100,
              resource: {
                id: "res_1",
                availability: { monday: 8, tuesday: 8, wednesday: 8, thursday: 8, friday: 8 },
                countryId: null,
                federalState: null,
                metroCityId: null,
                country: null,
                metroCity: null,
              },
            },
          ]),
        },
      };

      const result = await getDashboardProjectHealth(db as never);

      expect(result).toHaveLength(2);
      // "bad" project has lower composite score → sorted first
      expect(result[0]?.shortCode).toBe("BAD");
      expect(result[1]?.shortCode).toBe("GOOD");
      expect(result[0]!.compositeScore).toBeLessThan(result[1]!.compositeScore);
    } finally {
      vi.useRealTimers();
    }
  });
});

// ---------------------------------------------------------------------------
// getDashboardPeakTimes – resource-level grouping
// ---------------------------------------------------------------------------

describe("getDashboardPeakTimes (resource groupBy)", () => {
  it("groups booked hours by individual resource name", async () => {
    const db = {
      assignment: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "assign_1",
            projectId: "proj_1",
            resourceId: "res_1",
            status: "ACTIVE",
            startDate: new Date("2026-03-02T00:00:00.000Z"),
            endDate: new Date("2026-03-02T00:00:00.000Z"),
            hoursPerDay: 6,
            dailyCostCents: 0,
            project: {
              id: "proj_1",
              name: "Alpha",
              shortCode: "ALPHA",
              status: "ACTIVE",
              orderType: "FIXED",
            },
            resource: { id: "res_1", displayName: "Alice", chapter: "CGI" },
          },
          {
            id: "assign_2",
            projectId: "proj_1",
            resourceId: "res_2",
            status: "ACTIVE",
            startDate: new Date("2026-03-02T00:00:00.000Z"),
            endDate: new Date("2026-03-02T00:00:00.000Z"),
            hoursPerDay: 4,
            dailyCostCents: 0,
            project: {
              id: "proj_1",
              name: "Alpha",
              shortCode: "ALPHA",
              status: "ACTIVE",
              orderType: "FIXED",
            },
            resource: { id: "res_2", displayName: "Bob", chapter: "Lighting" },
          },
        ]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([
          {
            id: "res_1",
            displayName: "Alice",
            chapter: "CGI",
            availability: { monday: 6, tuesday: 6, wednesday: 6, thursday: 6, friday: 6 },
            countryId: null,
            federalState: null,
            metroCityId: null,
            country: null,
            metroCity: null,
          },
          {
            id: "res_2",
            displayName: "Bob",
            chapter: "Lighting",
            availability: { monday: 4, tuesday: 4, wednesday: 4, thursday: 4, friday: 4 },
            countryId: null,
            federalState: null,
            metroCityId: null,
            country: null,
            metroCity: null,
          },
        ]),
      },
    };

    const result = await getDashboardPeakTimes(db as never, {
      startDate: new Date("2026-03-02T00:00:00.000Z"),
      endDate: new Date("2026-03-02T00:00:00.000Z"),
      granularity: "month",
      groupBy: "resource",
    });

    expect(result).toHaveLength(1);
    const period = result[0]!;
    expect(period.period).toBe("2026-03");
    const alice = period.groups.find((g) => g.name === "Alice");
    const bob = period.groups.find((g) => g.name === "Bob");
    expect(alice).toBeDefined();
    expect(alice?.hours).toBe(6);
    expect(alice?.capacityHours).toBe(6);
    expect(alice?.utilizationPct).toBe(100);
    expect(bob).toBeDefined();
    expect(bob?.hours).toBe(4);
    expect(bob?.capacityHours).toBe(4);
  });

  it("returns empty periods list when date range yields no resources and no assignments", async () => {
    const db = {
      assignment: {
        findMany: vi.fn().mockResolvedValue([]),
      },
      resource: {
        findMany: vi.fn().mockResolvedValue([]),
      },
    };

    const result = await getDashboardPeakTimes(db as never, {
      startDate: new Date("2026-03-01T00:00:00.000Z"),
      endDate: new Date("2026-03-01T00:00:00.000Z"),
      granularity: "week",
      groupBy: "project",
    });

    expect(result).toHaveLength(1);
    const period = result[0]!;
    expect(period.groups).toEqual([]);
    expect(period.totalHours).toBe(0);
    expect(period.capacityHours).toBe(0);
    expect(period.utilizationPct).toBe(0);
  });
});
