import { describe, expect, it, vi } from "vitest";
import { simulateProjectScenario } from "../router/scenario-simulation.js";

vi.mock("../lib/resource-capacity.js", () => ({
  calculateEffectiveAvailableHours: vi.fn().mockReturnValue(160),
  calculateEffectiveBookedHours: vi.fn().mockReturnValue(0),
  loadResourceDailyAvailabilityContexts: vi.fn().mockResolvedValue(new Map()),
}));

const DEFAULT_AVAILABILITY = {
  monday: 8,
  tuesday: 8,
  wednesday: 8,
  thursday: 8,
  friday: 8,
  saturday: 0,
  sunday: 0,
};

const baseProject = {
  id: "project_1",
  name: "Test Project",
  budgetCents: null,
  orderType: "CHARGEABLE",
  startDate: new Date("2026-04-01T00:00:00.000Z"),
  endDate: new Date("2026-04-30T00:00:00.000Z"),
};

function makeResource(overrides: Record<string, unknown> = {}) {
  return {
    id: "resource_1",
    displayName: "Alice Tester",
    eid: "EMP-001",
    lcrCents: 5000,
    availability: DEFAULT_AVAILABILITY,
    chargeabilityTarget: 80,
    skills: [],
    countryId: "DE",
    federalState: null,
    metroCityId: null,
    country: { code: "DE" },
    metroCity: null,
    ...overrides,
  };
}

function makeAssignment(overrides: Record<string, unknown> = {}) {
  return {
    id: "assignment_1",
    resourceId: "resource_1",
    projectId: "project_1",
    hoursPerDay: 8,
    startDate: new Date("2026-04-01T00:00:00.000Z"),
    endDate: new Date("2026-04-10T00:00:00.000Z"),
    status: "ACTIVE",
    resource: makeResource(),
    ...overrides,
  };
}

function makeUtilizationAssignment(overrides: Record<string, unknown> = {}) {
  return {
    id: "assignment_1",
    resourceId: "resource_1",
    projectId: "project_1",
    hoursPerDay: 8,
    startDate: new Date("2026-04-01T00:00:00.000Z"),
    endDate: new Date("2026-04-10T00:00:00.000Z"),
    ...overrides,
  };
}

function makeChange(overrides: Record<string, unknown> = {}) {
  return {
    resourceId: "resource_1",
    startDate: new Date("2026-04-01T00:00:00.000Z"),
    endDate: new Date("2026-04-10T00:00:00.000Z"),
    hoursPerDay: 8,
    ...overrides,
  };
}

/**
 * Build a db mock where assignment.findMany is called twice:
 * 1. for currentAssignments (project assignments)
 * 2. for allAssignmentsForResources (utilization context)
 */
function makeDb(overrides: {
  project?: unknown;
  currentAssignments?: unknown[];
  utilizationAssignments?: unknown[];
  resources?: unknown[];
} = {}) {
  const projectValue = "project" in overrides ? overrides.project : baseProject;
  return {
    project: {
      findUnique: vi.fn().mockResolvedValue(projectValue),
    },
    assignment: {
      findMany: vi
        .fn()
        .mockResolvedValueOnce(overrides.currentAssignments ?? [])
        .mockResolvedValueOnce(overrides.utilizationAssignments ?? []),
    },
    resource: {
      findMany: vi.fn().mockResolvedValue(overrides.resources ?? [makeResource()]),
    },
  };
}

describe("simulateProjectScenario", () => {
  it("throws NOT_FOUND when project does not exist", async () => {
    const db = makeDb({ project: null });

    await expect(
      simulateProjectScenario(db as never, {
        projectId: "project_missing",
        changes: [makeChange()],
      }),
    ).rejects.toMatchObject({
      code: "NOT_FOUND",
      message: "Project not found",
    });
  });

  it("returns delta.headcount of 0 when existing assignment is carried through unchanged", async () => {
    const assignment = makeAssignment();
    // Pass the existing assignment as a change with its assignmentId (modify path, same data)
    const db = makeDb({
      currentAssignments: [assignment],
      utilizationAssignments: [makeUtilizationAssignment()],
      resources: [makeResource()],
    });

    const result = await simulateProjectScenario(db as never, {
      projectId: "project_1",
      changes: [
        makeChange({ assignmentId: "assignment_1", resourceId: "resource_1" }),
      ],
    });

    // baseline has 1 assignment; scenario also has 1 entry (the modified one)
    expect(result.delta.headcount).toBe(0);
  });

  it("remove change decreases headcount delta", async () => {
    const assignment = makeAssignment();
    const db = makeDb({
      currentAssignments: [assignment],
      utilizationAssignments: [],
      resources: [],
    });

    const result = await simulateProjectScenario(db as never, {
      projectId: "project_1",
      changes: [
        makeChange({ assignmentId: "assignment_1", remove: true }),
      ],
    });

    // baseline: 1 assignment, scenario: 0 entries → delta = -1
    expect(result.delta.headcount).toBe(-1);
    expect(result.baseline.headcount).toBe(1);
    expect(result.scenario.headcount).toBe(0);
  });

  it("new resource change increases headcount delta", async () => {
    const assignment = makeAssignment();
    const newResource = makeResource({ id: "resource_2", displayName: "Bob New" });
    const db = makeDb({
      currentAssignments: [assignment],
      utilizationAssignments: [
        makeUtilizationAssignment(),
        makeUtilizationAssignment({ id: "assignment_util_2", resourceId: "resource_2" }),
      ],
      resources: [makeResource(), newResource],
    });

    const result = await simulateProjectScenario(db as never, {
      projectId: "project_1",
      changes: [
        // new assignment (no assignmentId) for resource_2
        makeChange({ resourceId: "resource_2" }),
      ],
    });

    // baseline: 1, scenario: 1 existing (carried through) + 1 new = 2 → delta = +1
    expect(result.delta.headcount).toBe(1);
    expect(result.baseline.headcount).toBe(1);
    expect(result.scenario.headcount).toBe(2);
  });

  it("emits budget warning when scenario exceeds project budget", async () => {
    const { calculateEffectiveBookedHours } = await import("../lib/resource-capacity.js");
    // Return a large number of hours so cost = hours * lcrCents > budgetCents
    vi.mocked(calculateEffectiveBookedHours).mockReturnValue(1000);

    const projectWithTightBudget = {
      ...baseProject,
      budgetCents: 100, // tiny budget: 100 cents
    };
    const expensiveResource = makeResource({ lcrCents: 50000 });
    const db = makeDb({
      project: projectWithTightBudget,
      currentAssignments: [],
      utilizationAssignments: [makeUtilizationAssignment({ resourceId: "resource_1" })],
      resources: [expensiveResource],
    });

    const result = await simulateProjectScenario(db as never, {
      projectId: "project_1",
      changes: [makeChange({ resourceId: "resource_1" })],
    });

    const budgetWarning = result.warnings.find((w) => /exceeds budget/i.test(w));
    expect(budgetWarning).toBeDefined();

    // Reset mock
    vi.mocked(calculateEffectiveBookedHours).mockReturnValue(0);
  });

  it("reports skill coverage when scenario adds new skills", async () => {
    // Baseline has no assignments (no skills). Scenario adds one resource with a skill.
    const resourceWithSkill = makeResource({
      id: "resource_skill",
      skills: [{ skill: "Blender" }],
    });
    const db = makeDb({
      currentAssignments: [],
      utilizationAssignments: [makeUtilizationAssignment({ resourceId: "resource_skill" })],
      resources: [resourceWithSkill],
    });

    const result = await simulateProjectScenario(db as never, {
      projectId: "project_1",
      changes: [makeChange({ resourceId: "resource_skill" })],
    });

    // baseline skillCount = 0, scenario skillCount = 1
    // skillCoveragePct: baselineSkillCount === 0 && scenarioSkillCount > 0 → 100
    expect(result.scenario.skillCount).toBe(1);
    expect(result.delta.skillCoveragePct).toBe(100);
  });
});
