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,
    getDashboardOverview: vi.fn(),
    getDashboardPeakTimes: vi.fn(),
    getDashboardDemand: vi.fn(),
    getDashboardTopValueResources: vi.fn(),
    getDashboardChargeabilityOverview: vi.fn(),
    getDashboardBudgetForecast: vi.fn(),
    getDashboardSkillGaps: vi.fn(),
    getDashboardSkillGapSummary: vi.fn(),
    getDashboardProjectHealth: vi.fn(),
  };
});

vi.mock("../lib/cache.js", () => ({
  cacheGet: vi.fn().mockResolvedValue(null),
  cacheSet: vi.fn().mockResolvedValue(undefined),
}));

vi.mock("../lib/anonymization.js", () => ({
  anonymizeResources: vi.fn((resources: unknown[]) => resources.map((resource) => ({ ...resource, anonymized: true }))),
  getAnonymizationDirectory: vi.fn().mockResolvedValue({}),
}));

import {
  getDashboardBudgetForecast,
  getDashboardChargeabilityOverview,
  getDashboardDemand,
  getDashboardOverview,
  getDashboardPeakTimes,
  getDashboardProjectHealth,
  getDashboardSkillGapSummary,
  getDashboardTopValueResources,
} from "@capakraken/application";
import { cacheGet } from "../lib/cache.js";
import { anonymizeResources } from "../lib/anonymization.js";
import {
  getDashboardBudgetForecastDetail,
  getDashboardChargeabilityOverviewRead,
  getDashboardDetail,
  getDashboardProjectHealthDetail,
  getDashboardStatisticsDetail,
} from "../router/dashboard-procedure-support.js";

function createContext() {
  return {
    db: {} as never,
    session: {
      user: { email: "controller@example.com", name: "Controller", image: null },
      expires: "2099-01-01T00:00:00.000Z",
    },
    dbUser: {
      id: "user_1",
      systemRole: SystemRole.CONTROLLER,
      permissionOverrides: null,
    },
  };
}

describe("dashboard procedure support", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("derives statistics detail from the canonical overview", async () => {
    vi.mocked(getDashboardOverview).mockResolvedValue({
      totalResources: 12,
      activeResources: 10,
      inactiveResources: 2,
      totalProjects: 7,
      activeProjects: 4,
      inactiveProjects: 3,
      totalAllocations: 21,
      activeAllocations: 18,
      cancelledAllocations: 3,
      approvedVacations: 6,
      totalEstimates: 9,
      budgetSummary: {
        totalBudgetCents: 1_234_56,
        totalCostCents: 654_32,
        avgUtilizationPercent: 53,
      },
      budgetBasis: {
        remainingBudgetCents: 58_024,
        budgetedProjects: 5,
        unbudgetedProjects: 2,
        trackedAssignmentCount: 18,
        windowStart: null,
        windowEnd: null,
      },
      projectsByStatus: [
        { status: "ACTIVE", count: 4 },
        { status: "DRAFT", count: 2 },
        { status: "DONE", count: 1 },
      ],
      chapterUtilization: [
        { chapter: "CGI", resourceCount: 5, avgChargeabilityTarget: 78 },
        { chapter: "Compositing", resourceCount: 3, avgChargeabilityTarget: 74 },
      ],
      recentActivity: [],
    });

    const result = await getDashboardStatisticsDetail(createContext());

    expect(result).toEqual({
      activeResources: 10,
      totalProjects: 7,
      activeProjects: 4,
      totalAllocations: 21,
      approvedVacations: 6,
      totalEstimates: 9,
      totalBudget: "1.234,56 EUR",
      projectsByStatus: {
        ACTIVE: 4,
        DRAFT: 2,
        DONE: 1,
      },
      topChapters: [
        { chapter: "CGI", count: 5 },
        { chapter: "Compositing", count: 3 },
      ],
    });
  });

  it("builds the isolated skill gap detail section without overview reads", async () => {
    vi.mocked(getDashboardSkillGapSummary).mockResolvedValue({
      roleGaps: [
        { role: "Pipeline TD", needed: 4, filled: 1, gap: 3, fillRate: 25 },
      ],
      totalOpenPositions: 3,
      skillSupplyTop10: [{ skill: "houdini", resourceCount: 5 }],
      resourcesByRole: [{ role: "Pipeline TD", count: 2 }],
    });

    const result = await getDashboardDetail(createContext(), { section: "skill_gaps" });

    expect(result).toEqual({
      skillGaps: {
        totalOpenPositions: 3,
        roleGaps: [
          { role: "Pipeline TD", gap: 3, needed: 4, filled: 1, fillRate: 25 },
        ],
        topSkillsInSupply: [{ skill: "houdini", resourceCount: 5 }],
        resourcesByRole: [{ role: "Pipeline TD", count: 2 }],
      },
    });
    expect(getDashboardSkillGapSummary).toHaveBeenCalledTimes(1);
    expect(getDashboardOverview).not.toHaveBeenCalled();
    expect(getDashboardPeakTimes).not.toHaveBeenCalled();
    expect(getDashboardDemand).not.toHaveBeenCalled();
    expect(getDashboardProjectHealth).not.toHaveBeenCalled();
  });

  it("builds the assistant-facing dashboard detail payload", async () => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-03-15T00:00:00.000Z"));
    vi.mocked(getDashboardOverview).mockResolvedValue({
      totalResources: 12,
      activeResources: 10,
      inactiveResources: 2,
      totalProjects: 7,
      activeProjects: 4,
      inactiveProjects: 3,
      totalAllocations: 21,
      activeAllocations: 18,
      cancelledAllocations: 3,
      approvedVacations: 6,
      totalEstimates: 9,
      budgetSummary: {
        totalBudgetCents: 1_234_56,
        totalCostCents: 654_32,
        avgUtilizationPercent: 53,
      },
      budgetBasis: {
        remainingBudgetCents: 58_024,
        budgetedProjects: 5,
        unbudgetedProjects: 2,
        trackedAssignmentCount: 18,
        windowStart: "2026-03-01T00:00:00.000Z",
        windowEnd: "2026-06-30T00:00:00.000Z",
      },
      projectsByStatus: [],
      chapterUtilization: [
        { chapter: "CGI", resourceCount: 5, avgChargeabilityTarget: 78 },
      ],
      recentActivity: [],
    });
    vi.mocked(getDashboardPeakTimes).mockResolvedValue([
      {
        period: "2026-03",
        totalHours: 160.34,
        capacityHours: 200.12,
        utilizationPct: 80,
        derivation: {
          periodStart: "2026-03-01",
          periodEnd: "2026-03-31",
          calendarContextCount: 1,
          resourceCount: 5,
          groupCount: 2,
          baseAvailableHours: 216,
          effectiveAvailableHours: 200.12,
          publicHolidayHoursDeduction: 8,
          absenceDayEquivalent: 1,
          absenceHoursDeduction: 7.88,
          calendarLocations: [{ countryCode: "DE", federalState: "BY", metroCityName: "Munich" }],
          remainingCapacityHours: 39.78,
          overbookedHours: 0,
        },
      },
    ]);
    vi.mocked(getDashboardTopValueResources).mockResolvedValue([
      {
        id: "res_1",
        eid: "E-001",
        displayName: "Ada Lovelace",
        chapter: "CGI",
        lcrCents: 12345,
        valueScore: 98,
        valueScoreBreakdown: {
          skillDepth: 94,
          skillBreadth: 86,
          costEfficiency: 82,
          chargeability: 88,
          experience: 96,
          total: 98,
        },
        valueScoreUpdatedAt: new Date("2026-03-05T00:00:00.000Z"),
        countryCode: "DE",
        countryName: "Germany",
        federalState: "BY",
        metroCityName: "Munich",
      },
    ]);
    vi.mocked(getDashboardDemand).mockResolvedValue([
      {
        name: "Apollo",
        shortCode: "APO",
        requiredFTEs: 3,
        resourceCount: 1,
        allocatedHours: 80,
        derivation: {
          periodStart: "2026-03-01",
          periodEnd: "2026-06-30",
          periodWorkingHoursBase: 672,
          requiredHours: 2016,
          requiredFTEs: 3,
          fillPct: 40,
          demandSource: "DEMAND_REQUIREMENTS",
          calendarLocations: [{ countryCode: "DE", countryName: "Germany" }],
        },
      },
    ]);
    vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue({
      rows: [
        {
          id: "res_1",
          eid: "E-001",
          displayName: "Ada Lovelace",
          chapter: "CGI",
          chargeabilityTarget: 78,
          actualChargeability: 64,
          expectedChargeability: 72,
          derivation: {
            weeklyAvailabilityHours: 40,
            baseWorkingDays: 23,
            effectiveWorkingDayEquivalent: 21,
            baseAvailableHours: 184,
            effectiveAvailableHours: 168,
            publicHolidayCount: 1,
            publicHolidayWorkdayCount: 1,
            publicHolidayHoursDeduction: 8,
            absenceDayEquivalent: 1,
            absenceHoursDeduction: 8,
            actualBookedHours: 107.5,
            expectedBookedHours: 121,
            targetBookedHours: 131,
            unassignedHours: 47,
          },
        },
      ],
      top: [],
      watchlist: [],
      month: "2026-03",
    });
    vi.mocked(getDashboardProjectHealth).mockResolvedValue([
      {
        id: "project_1",
        projectName: "Apollo",
        shortCode: "APO",
        status: "ACTIVE",
        clientId: "client_1",
        clientName: "Acme",
        budgetHealth: 62,
        staffingHealth: 55,
        timelineHealth: 48,
        compositeScore: 54,
        budgetUtilizationPercent: 81,
        remainingBudgetCents: 45_000,
        demandHeadcountTotal: 5,
        demandHeadcountFilled: 3,
        demandHeadcountOpen: 2,
        demandRequirementCount: 2,
        plannedEndDate: new Date("2026-07-31T00:00:00.000Z"),
        daysUntilEndDate: 138,
        timelineStatus: "DUE_SOON",
        derivation: {
          periodStart: "2026-03-01",
          periodEnd: "2026-06-30",
          calendarContextCount: 1,
          holidayAwareAssignmentCount: 3,
          fallbackAssignmentCount: 0,
          baseSpentCents: 170_000,
          adjustedSpentCents: 165_000,
          publicHolidayDayEquivalent: 1,
          publicHolidayCostDeductionCents: 5_000,
          absenceDayEquivalent: 0.5,
          absenceCostDeductionCents: 3_000,
        },
      },
    ]);
    vi.mocked(getDashboardSkillGapSummary).mockResolvedValue({
      roleGaps: [
        { role: "Pipeline TD", needed: 4, filled: 1, gap: 3, fillRate: 25 },
        { role: "Lighting TD", needed: 2, filled: 1, gap: 1, fillRate: 50 },
      ],
      totalOpenPositions: 4,
      skillSupplyTop10: [
        { skill: "houdini", resourceCount: 5 },
        { skill: "nuke", resourceCount: 4 },
      ],
      resourcesByRole: [
        { role: "Compositor", count: 6 },
        { role: "Pipeline TD", count: 2 },
      ],
    });

    try {
      const result = await getDashboardDetail(createContext(), { section: "all" });

      expect(result).toEqual({
        peakTimes: [
          {
            month: "2026-03",
            totalHours: 160.3,
            totalHoursPerDay: 160.3,
            capacityHours: 200.1,
            utilizationPct: 80,
            calendarContextCount: 1,
            calendarLocations: [{ countryCode: "DE", federalState: "BY", metroCityName: "Munich" }],
            explainability: {
              periodStart: "2026-03-01",
              periodEnd: "2026-03-31",
              resourceCount: 5,
              groupCount: 2,
              baseAvailableHours: 216,
              effectiveAvailableHours: 200.12,
              publicHolidayHoursDeduction: 8,
              absenceDayEquivalent: 1,
              absenceHoursDeduction: 7.88,
              remainingCapacityHours: 39.78,
              overbookedHours: 0,
            },
          },
        ],
        topResources: [
          {
            name: "Ada Lovelace",
            eid: "E-001",
            chapter: "CGI",
            lcr: "123,45 EUR",
            valueScore: 98,
            valueScoreBreakdown: {
              skillDepth: 94,
              skillBreadth: 86,
              costEfficiency: 82,
              chargeability: 88,
              experience: 96,
              total: 98,
            },
            valueScoreUpdatedAt: "2026-03-05T00:00:00.000Z",
            countryCode: "DE",
            countryName: "Germany",
            federalState: "BY",
            metroCityName: "Munich",
          },
        ],
        demandPipeline: [
          {
            project: "Apollo (APO)",
            needed: 2,
            requiredFTEs: 3,
            allocatedResources: 1,
            allocatedHours: 80,
            calendarLocations: [{ countryCode: "DE", countryName: "Germany" }],
            explainability: {
              periodStart: "2026-03-01",
              periodEnd: "2026-06-30",
              periodWorkingHoursBase: 672,
              requiredHours: 2016,
              fillPct: 40,
              demandSource: "DEMAND_REQUIREMENTS",
              calendarContextCount: 1,
            },
          },
        ],
        chargeabilityByChapter: [
          {
            chapter: "CGI",
            headcount: 1,
            avgTargetPct: 78,
            avgActualPct: 64,
            avgExpectedPct: 72,
            gapToTargetPct: 14,
            avgTarget: "78%",
            avgActual: "64%",
            avgExpected: "72%",
            explainability: {
              month: "2026-03",
              resourceCount: 1,
              derivedHeadcount: 1,
              baseAvailableHours: 184,
              effectiveAvailableHours: 168,
              actualBookedHours: 107.5,
              expectedBookedHours: 121,
              targetBookedHours: 131,
              publicHolidayHoursDeduction: 8,
              absenceDayEquivalent: 1,
              absenceHoursDeduction: 8,
              unassignedHours: 47,
            },
          },
        ],
        projectHealth: [
          {
            project: "Apollo (APO)",
            status: "ACTIVE",
            overall: 54,
            rating: "at_risk",
            budget: 62,
            staffing: 55,
            timeline: 48,
            timelineStatus: "DUE_SOON",
            daysUntilEndDate: 138,
            demandHeadcountOpen: 2,
            explainability: {
              demandHeadcountTotal: 5,
              demandHeadcountFilled: 3,
              demandHeadcountOpen: 2,
              demandRequirementCount: 2,
              plannedEndDate: "2026-07-31T00:00:00.000Z",
              budgetUtilizationPercent: 81,
              remainingBudgetCents: 45_000,
              calendarContextCount: 1,
              holidayAwareAssignmentCount: 3,
              publicHolidayCostDeductionCents: 5_000,
              absenceCostDeductionCents: 3_000,
            },
          },
        ],
        skillGaps: {
          totalOpenPositions: 4,
          roleGaps: [
            { role: "Pipeline TD", gap: 3, needed: 4, filled: 1, fillRate: 25 },
            { role: "Lighting TD", gap: 1, needed: 2, filled: 1, fillRate: 50 },
          ],
          topSkillsInSupply: [
            { skill: "houdini", resourceCount: 5 },
            { skill: "nuke", resourceCount: 4 },
          ],
          resourcesByRole: [
            { role: "Compositor", count: 6 },
            { role: "Pipeline TD", count: 2 },
          ],
        },
      });

      expect(getDashboardChargeabilityOverview).toHaveBeenCalledWith(
        expect.anything(),
        expect.objectContaining({
          includeProposed: false,
          topN: 10,
          watchlistThreshold: 15,
        }),
      );
    } finally {
      vi.useRealTimers();
    }
  });

  it("anonymizes chargeability overview payloads before returning them", async () => {
    vi.mocked(getDashboardChargeabilityOverview).mockResolvedValue({
      avgChargeability: 72,
      top: [{ id: "res_1" }],
      watchlist: [{ id: "res_2" }],
    });

    const result = await getDashboardChargeabilityOverviewRead(createContext(), {
      includeProposed: false,
      topN: 10,
      watchlistThreshold: 15,
    });

    expect(anonymizeResources).toHaveBeenCalledTimes(2);
    expect(result).toEqual({
      avgChargeability: 72,
      top: [{ id: "res_1", anonymized: true }],
      watchlist: [{ id: "res_2", anonymized: true }],
    });
  });

  it("reuses cached budget forecast rows for detail payloads", async () => {
    vi.mocked(cacheGet).mockResolvedValueOnce([
      {
        projectId: "project_1",
        projectName: "Apollo",
        shortCode: "APO",
        clientId: "client_1",
        clientName: "Acme",
        budgetCents: 500_000,
        spentCents: 320_000,
        remainingCents: 180_000,
        burnRate: 120_000,
        estimatedExhaustionDate: "2026-05-31",
        pctUsed: 64,
        activeAssignmentCount: 3,
        calendarLocations: [
          {
            countryCode: "DE",
            countryName: "Germany",
            federalState: "BY",
            metroCityName: "Munich",
            activeAssignmentCount: 2,
            burnRateCents: 80_000,
          },
        ],
        derivation: {
          periodStart: "2026-03-01",
          periodEnd: "2026-03-31",
          calendarContextCount: 1,
          holidayAwareAssignmentCount: 2,
          fallbackAssignmentCount: 1,
          baseBurnRateCents: 130_000,
          adjustedBurnRateCents: 120_000,
          publicHolidayDayEquivalent: 1,
          publicHolidayCostDeductionCents: 5_000,
          absenceDayEquivalent: 0.5,
          absenceCostDeductionCents: 5_000,
        },
      },
    ]);

    const result = await getDashboardBudgetForecastDetail(createContext());

    expect(getDashboardBudgetForecast).not.toHaveBeenCalled();
    expect(result).toEqual({
      forecasts: [
        {
          projectId: "project_1",
          projectName: "Apollo",
          shortCode: "APO",
          clientId: "client_1",
          clientName: "Acme",
          budget: "5.000,00 EUR",
          budgetCents: 500_000,
          spent: "3.200,00 EUR",
          spentCents: 320_000,
          remaining: "1.800,00 EUR",
          remainingCents: 180_000,
          projected: "5.000,00 EUR",
          projectedCents: 500_000,
          burnRate: "1.200,00 EUR",
          burnRateCents: 120_000,
          utilization: "64%",
          estimatedExhaustionDate: "2026-05-31",
          activeAssignmentCount: 3,
          calendarLocations: [
            {
              countryCode: "DE",
              countryName: "Germany",
              federalState: "BY",
              metroCityName: "Munich",
              activeAssignmentCount: 2,
              burnRateCents: 80_000,
            },
          ],
          derivation: {
            periodStart: "2026-03-01",
            periodEnd: "2026-03-31",
            calendarContextCount: 1,
            holidayAwareAssignmentCount: 2,
            fallbackAssignmentCount: 1,
            baseBurnRateCents: 130_000,
            adjustedBurnRateCents: 120_000,
            publicHolidayDayEquivalent: 1,
            publicHolidayCostDeductionCents: 5_000,
            absenceDayEquivalent: 0.5,
            absenceCostDeductionCents: 5_000,
          },
          burnStatus: "on_track",
        },
      ],
    });
  });

  it("reuses cached project health rows for detail payloads", async () => {
    vi.mocked(cacheGet).mockResolvedValueOnce([
      {
        id: "project_1",
        projectName: "Apollo",
        shortCode: "APO",
        status: "ACTIVE",
        clientId: "client_1",
        clientName: "Acme",
        budgetHealth: 78,
        staffingHealth: 66,
        timelineHealth: 55,
        compositeScore: 66,
        budgetCents: 500_000,
        spentCents: 320_000,
        remainingBudgetCents: 180_000,
        budgetUtilizationPercent: 64,
        demandHeadcountTotal: 5,
        demandHeadcountFilled: 3,
        demandHeadcountOpen: 2,
        demandRequirementCount: 2,
        plannedEndDate: new Date("2026-06-30T00:00:00.000Z"),
        daysUntilEndDate: 107,
        timelineStatus: "ON_TRACK" as const,
        calendarLocations: [
          {
            countryCode: "DE",
            countryName: "Germany",
            federalState: "BY",
            metroCityName: "Munich",
            assignmentCount: 3,
            spentCents: 320_000,
          },
        ],
        derivation: {
          periodStart: "2026-03-01",
          periodEnd: "2026-03-31",
          calendarContextCount: 1,
          holidayAwareAssignmentCount: 2,
          fallbackAssignmentCount: 1,
          baseSpentCents: 330_000,
          adjustedSpentCents: 320_000,
          publicHolidayDayEquivalent: 1,
          publicHolidayCostDeductionCents: 5_000,
          absenceDayEquivalent: 0.5,
          absenceCostDeductionCents: 5_000,
        },
      },
    ]);

    const result = await getDashboardProjectHealthDetail(createContext());

    expect(getDashboardProjectHealth).not.toHaveBeenCalled();
    expect(result).toEqual({
      projects: [
        {
          projectId: "project_1",
          projectName: "Apollo",
          shortCode: "APO",
          status: "ACTIVE",
          overall: 66,
          budget: 78,
          staffing: 66,
          timeline: 55,
          rating: "at_risk",
          budgetBasis: {
            budgetCents: 500_000,
            spentCents: 320_000,
            remainingBudgetCents: 180_000,
            budgetUtilizationPercent: 64,
            calendarLocations: [
              {
                countryCode: "DE",
                countryName: "Germany",
                federalState: "BY",
                metroCityName: "Munich",
                assignmentCount: 3,
                spentCents: 320_000,
              },
            ],
            derivation: {
              periodStart: "2026-03-01",
              periodEnd: "2026-03-31",
              calendarContextCount: 1,
              holidayAwareAssignmentCount: 2,
              fallbackAssignmentCount: 1,
              baseSpentCents: 330_000,
              adjustedSpentCents: 320_000,
              publicHolidayDayEquivalent: 1,
              publicHolidayCostDeductionCents: 5_000,
              absenceDayEquivalent: 0.5,
              absenceCostDeductionCents: 5_000,
            },
          },
          staffingBasis: {
            demandHeadcountTotal: 5,
            demandHeadcountFilled: 3,
            demandHeadcountOpen: 2,
            demandRequirementCount: 2,
          },
          timelineBasis: {
            plannedEndDate: "2026-06-30T00:00:00.000Z",
            daysUntilEndDate: 107,
            timelineStatus: "ON_TRACK",
          },
          context: {
            clientId: "client_1",
            clientName: "Acme",
          },
        },
      ],
      summary: {
        healthy: 0,
        atRisk: 1,
        critical: 0,
      },
    });
  });
});
