/**
 * Integration tests for the Crewing Phase 2 requisition + relief server actions:
 * raise / cancel / transition, relief request + convert, and the shared
 * autoRaiseRequisition helper. Mirrors the admin-ranks test setup.
 *
 * The Requisition/ReliefRequest/CrewAction tables are introduced in this phase,
 * so afterEach wipes them wholesale (no pre-existing rows to preserve).
 */
import { vi, describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
import React from "react";
// The list page's JSX compiles to classic React.createElement in the node runner.
(globalThis as unknown as { React: typeof React }).React = React;

vi.mock("@/auth", () => ({ auth: vi.fn() }));
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
vi.mock("next/navigation", () => ({ redirect: vi.fn(), notFound: vi.fn() }));
vi.mock("@/lib/feature-flags", () => ({ CREWING_ENABLED: true, INVENTORY_ENABLED: true }));
vi.mock("@/lib/notifier", () => ({ notify: vi.fn(), notifyCrew: vi.fn() }));
// We read the page element's props directly; the client component is irrelevant.
vi.mock("@/app/(portal)/crewing/requisitions/requisitions-manager", () => ({ RequisitionsManager: () => null }));

import { auth } from "@/auth";
import { db } from "@/lib/db";
import {
  raiseRequisition,
  cancelRequisition,
  transitionRequisition,
  requestReliefCover,
  convertReliefToRequisition,
} from "@/app/(portal)/crewing/requisitions/actions";
import RequisitionsPage from "@/app/(portal)/crewing/requisitions/page";
import { autoRaiseRequisition } from "@/lib/requisition-service";
import { makeSession, getSeedUser, fd } from "./helpers";
import type { Role } from "@prisma/client";

let managerId: string;
let manningId: string;
let siteStaffId: string;
let rankId: string;
let vesselId: string;

const SS_EMAIL = "sitestaff@itreq.local";

const as = (userId: string, role: Role) =>
  vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(userId, role));

beforeAll(async () => {
  managerId = (await getSeedUser("manager@pelagia.local")).id;
  manningId = (await getSeedUser("manning@pelagia.local")).id;
  const ss = await db.user.upsert({
    where: { email: SS_EMAIL },
    update: { role: "SITE_STAFF", isActive: true },
    create: { employeeId: "ITREQ-SS", email: SS_EMAIL, name: "Site Staff Test", role: "SITE_STAFF" },
  });
  siteStaffId = ss.id;
  rankId = (await db.rank.findFirstOrThrow()).id;
  vesselId = (await db.vessel.findFirstOrThrow()).id;
});

afterEach(async () => {
  await db.crewAction.deleteMany({});
  await db.application.deleteMany({});
  await db.crewMember.deleteMany({});
  await db.reliefRequest.deleteMany({});
  await db.requisition.deleteMany({});
  vi.clearAllMocks();
});

afterAll(async () => {
  await db.user.deleteMany({ where: { email: SS_EMAIL } });
});

describe("raiseRequisition", () => {
  it("creates an OPEN requisition with a REQ- code and an audit action", async () => {
    as(managerId, "MANAGER");
    const res = await raiseRequisition(fd({ rankId, vesselId, reason: "NEW_VACANCY", notes: "Urgent" }));
    expect("ok" in res && res.ok).toBe(true);

    const req = await db.requisition.findFirstOrThrow({ include: { actions: true } });
    expect(req.status).toBe("OPEN");
    expect(req.code).toMatch(/^REQ-\d+$/);
    expect(req.autoRaised).toBe(false);
    expect(req.raisedById).toBe(managerId);
    expect(req.actions).toHaveLength(1);
    expect(req.actions[0].actionType).toBe("REQUISITION_RAISED");
  });

  it("requires a vessel or site", async () => {
    as(managerId, "MANAGER");
    const res = await raiseRequisition(fd({ rankId, reason: "NEW_VACANCY" }));
    expect("error" in res).toBe(true);
    expect(await db.requisition.count()).toBe(0);
  });

  it("is rejected for a role without raise_requisition (site staff)", async () => {
    as(siteStaffId, "SITE_STAFF");
    const res = await raiseRequisition(fd({ rankId, vesselId }));
    expect(res).toEqual({ error: "Unauthorized" });
    expect(await db.requisition.count()).toBe(0);
  });
});

describe("cancelRequisition", () => {
  it("a Manager withdraws an OPEN requisition with a reason", async () => {
    as(managerId, "MANAGER");
    await raiseRequisition(fd({ rankId, vesselId }));
    const req = await db.requisition.findFirstOrThrow();

    const res = await cancelRequisition(req.id, "Vacancy no longer needed");
    expect("ok" in res && res.ok).toBe(true);

    const after = await db.requisition.findUniqueOrThrow({ where: { id: req.id } });
    expect(after.status).toBe("CANCELLED");
    expect(after.cancellationReason).toBe("Vacancy no longer needed");
    expect(after.cancelledAt).not.toBeNull();
  });

  it("requires a reason", async () => {
    as(managerId, "MANAGER");
    await raiseRequisition(fd({ rankId, vesselId }));
    const req = await db.requisition.findFirstOrThrow();
    const res = await cancelRequisition(req.id, "   ");
    expect("error" in res).toBe(true);
  });

  it("cannot withdraw once past shortlisting", async () => {
    as(managerId, "MANAGER");
    await raiseRequisition(fd({ rankId, vesselId }));
    const req = await db.requisition.findFirstOrThrow();
    await db.requisition.update({ where: { id: req.id }, data: { status: "INTERVIEWING" } });

    const res = await cancelRequisition(req.id, "too late");
    expect("error" in res).toBe(true);
    expect((await db.requisition.findUniqueOrThrow({ where: { id: req.id } })).status).toBe("INTERVIEWING");
  });

  it("the MPO may also withdraw (holds cancel_requisition per §6)", async () => {
    as(managerId, "MANAGER");
    await raiseRequisition(fd({ rankId, vesselId }));
    const req = await db.requisition.findFirstOrThrow();
    as(manningId, "MANNING");
    const res = await cancelRequisition(req.id, "sourced elsewhere");
    expect("ok" in res && res.ok).toBe(true);
    expect((await db.requisition.findUniqueOrThrow({ where: { id: req.id } })).status).toBe("CANCELLED");
  });

  it("is rejected for a role without cancel_requisition (site staff)", async () => {
    as(managerId, "MANAGER");
    await raiseRequisition(fd({ rankId, vesselId }));
    const req = await db.requisition.findFirstOrThrow();
    as(siteStaffId, "SITE_STAFF");
    const res = await cancelRequisition(req.id, "nope");
    expect(res).toEqual({ error: "Unauthorized" });
  });
});

describe("transitionRequisition", () => {
  it("Manager selects from INTERVIEWING; MPO cannot", async () => {
    as(managerId, "MANAGER");
    await raiseRequisition(fd({ rankId, vesselId }));
    const req = await db.requisition.findFirstOrThrow();
    await db.requisition.update({ where: { id: req.id }, data: { status: "INTERVIEWING" } });

    as(manningId, "MANNING");
    expect(await transitionRequisition(req.id, "mark_selected")).toEqual({ error: "Unauthorized" });

    as(managerId, "MANAGER");
    const ok = await transitionRequisition(req.id, "mark_selected");
    expect("ok" in ok && ok.ok).toBe(true);
    expect((await db.requisition.findUniqueOrThrow({ where: { id: req.id } })).status).toBe("SELECTED");
  });

  it("marks FILLED and stamps filledAt", async () => {
    as(managerId, "MANAGER");
    await raiseRequisition(fd({ rankId, vesselId }));
    const req = await db.requisition.findFirstOrThrow();
    await db.requisition.update({ where: { id: req.id }, data: { status: "SELECTED" } });

    as(manningId, "MANNING");
    const res = await transitionRequisition(req.id, "mark_filled");
    expect("ok" in res && res.ok).toBe(true);
    const after = await db.requisition.findUniqueOrThrow({ where: { id: req.id }, include: { actions: true } });
    expect(after.status).toBe("FILLED");
    expect(after.filledAt).not.toBeNull();
    expect(after.actions.some((a) => a.actionType === "REQUISITION_FILLED")).toBe(true);
  });
});

describe("relief requests", () => {
  it("site staff raise an OPEN relief request with an audit action", async () => {
    as(siteStaffId, "SITE_STAFF");
    const res = await requestReliefCover(fd({ rankId, vesselId, note: "Chief going on leave" }));
    expect("ok" in res && res.ok).toBe(true);

    const relief = await db.reliefRequest.findFirstOrThrow();
    expect(relief.status).toBe("OPEN");
    expect(relief.requestedById).toBe(siteStaffId);
    const action = await db.crewAction.findFirstOrThrow({ where: { actionType: "RELIEF_REQUESTED" } });
    expect((action.metadata as { reliefRequestId: string }).reliefRequestId).toBe(relief.id);
  });

  it("is rejected for the MPO (no request_relief_cover)", async () => {
    as(manningId, "MANNING");
    const res = await requestReliefCover(fd({ rankId, vesselId }));
    expect(res).toEqual({ error: "Unauthorized" });
    expect(await db.reliefRequest.count()).toBe(0);
  });

  it("MPO converts a relief request into a requisition and links them", async () => {
    as(siteStaffId, "SITE_STAFF");
    await requestReliefCover(fd({ rankId, vesselId, note: "cover" }));
    const relief = await db.reliefRequest.findFirstOrThrow();

    as(manningId, "MANNING");
    const res = await convertReliefToRequisition(fd({ reliefRequestId: relief.id, reason: "REPLACEMENT" }));
    expect("ok" in res && res.ok).toBe(true);

    const after = await db.reliefRequest.findUniqueOrThrow({ where: { id: relief.id } });
    expect(after.status).toBe("CONVERTED");
    expect(after.convertedRequisitionId).not.toBeNull();

    const req = await db.requisition.findUniqueOrThrow({
      where: { id: after.convertedRequisitionId! },
      include: { actions: true, sourceReliefRequest: true },
    });
    expect(req.status).toBe("OPEN");
    expect(req.reason).toBe("REPLACEMENT");
    expect(req.sourceReliefRequest?.id).toBe(relief.id);
    expect(req.actions.some((a) => a.actionType === "RELIEF_CONVERTED")).toBe(true);
  });

  it("refuses to convert an already-handled relief request", async () => {
    as(siteStaffId, "SITE_STAFF");
    await requestReliefCover(fd({ rankId, vesselId }));
    const relief = await db.reliefRequest.findFirstOrThrow();

    as(manningId, "MANNING");
    await convertReliefToRequisition(fd({ reliefRequestId: relief.id }));
    const second = await convertReliefToRequisition(fd({ reliefRequestId: relief.id }));
    expect("error" in second).toBe(true);
  });
});

describe("autoRaiseRequisition (shared helper)", () => {
  it("creates an autoRaised OPEN requisition with no human actor", async () => {
    const req = await autoRaiseRequisition({ rankId, vesselId, reason: "LEAVE" });
    const stored = await db.requisition.findUniqueOrThrow({ where: { id: req.id }, include: { actions: true } });
    expect(stored.autoRaised).toBe(true);
    expect(stored.raisedById).toBeNull();
    expect(stored.reason).toBe("LEAVE");
    expect(stored.status).toBe("OPEN");
    expect(stored.actions[0].actionType).toBe("REQUISITION_RAISED");
    expect(stored.actions[0].actorId).toBeNull();
  });
});

describe("requisitions list (A3)", () => {
  it("exposes a candidate count per requisition row", async () => {
    as(managerId, "MANAGER");
    const req = await db.requisition.create({ data: { code: "REQ-A3", rankId, vesselId, reason: "NEW_VACANCY", status: "SHORTLISTING" } });
    const empty = await db.requisition.create({ data: { code: "REQ-A3B", rankId, vesselId, reason: "LEAVE", status: "OPEN" } });
    for (const name of ["Cand A", "Cand B"]) {
      const c = await db.crewMember.create({ data: { name, type: "NEW", status: "CANDIDATE", source: "CAREERS" } });
      await db.application.create({ data: { requisitionId: req.id, crewMemberId: c.id, stage: "SHORTLISTED", type: "NEW" } });
    }

    const el = (await RequisitionsPage()) as unknown as {
      props: { requisitions: Array<{ id: string; candidateCount: number }> };
    };
    expect(el.props.requisitions.find((r) => r.id === req.id)?.candidateCount).toBe(2);
    expect(el.props.requisitions.find((r) => r.id === empty.id)?.candidateCount).toBe(0);
  });
});
