/**
 * Integration tests for the Crewing Phase 3b recruitment pipeline actions.
 * The Application/Gate/Salary/Bank/EPF tables are introduced in this phase, so
 * afterEach wipes the crewing lifecycle tables wholesale.
 */
import { vi, describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";

vi.mock("@/auth", () => ({ auth: vi.fn() }));
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
vi.mock("@/lib/feature-flags", () => ({ CREWING_ENABLED: true, INVENTORY_ENABLED: true }));
vi.mock("@/lib/notifier", () => ({ notify: vi.fn(), notifyCrew: vi.fn() }));

import { auth } from "@/auth";
import { db } from "@/lib/db";
import {
  addApplication,
  advanceStage,
  recordReferenceCheck,
  verifyDocuments,
  agreeSalary,
  approveSalary,
  recordInterviewResult,
  requestInterviewWaiver,
  approveInterviewWaiver,
  selectCandidate,
  rejectApplication,
} from "@/app/(portal)/crewing/applications/actions";
import { makeSession, getSeedUser, fd } from "./helpers";
import type { ApplicationStage, Role } from "@prisma/client";

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

const SS_EMAIL = "sitestaff@itapp.local";
const as = (userId: string, role: Role) =>
  vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(userId, role));

let seq = 0;
async function freshRequisition() {
  seq += 1;
  return db.requisition.create({ data: { code: `REQ-T${seq}`, rankId, vesselId, reason: "NEW_VACANCY", status: "OPEN" } });
}
async function freshCandidate(type: "NEW" | "EX_HAND" = "NEW") {
  return db.crewMember.create({ data: { name: type === "EX_HAND" ? "Ex Hand" : "New Cand", type, status: type === "EX_HAND" ? "EX_HAND" : "CANDIDATE", source: type === "EX_HAND" ? "EX_HAND" : "CAREERS", appliedRankId: rankId } });
}
async function newApplication(type: "NEW" | "EX_HAND" = "NEW") {
  const [req, cand] = await Promise.all([freshRequisition(), freshCandidate(type)]);
  as(managerId, "MANAGER");
  const res = await addApplication(fd({ requisitionId: req.id, crewMemberId: cand.id }));
  if (!("ok" in res)) throw new Error("addApplication failed");
  return { applicationId: res.id!, requisitionId: req.id, crewMemberId: cand.id };
}
const setStage = (id: string, stage: ApplicationStage) => db.application.update({ where: { id }, data: { stage } });

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: "ITAPP-SS", email: SS_EMAIL, name: "SS App", 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.salaryStructure.deleteMany({});
  await db.applicationGate.deleteMany({});
  await db.referenceCheck.deleteMany({});
  await db.seafarerDocument.deleteMany({});
  await db.application.deleteMany({});
  await db.bankDetail.deleteMany({});
  await db.epfDetail.deleteMany({});
  await db.requisition.deleteMany({});
  await db.crewMember.deleteMany({});
  vi.clearAllMocks();
});

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

describe("addApplication", () => {
  it("creates a SHORTLISTED application and moves the requisition into SHORTLISTING", async () => {
    const { applicationId, requisitionId } = await newApplication();
    const app = await db.application.findUniqueOrThrow({ where: { id: applicationId } });
    expect(app.stage).toBe("SHORTLISTED");
    expect((await db.requisition.findUniqueOrThrow({ where: { id: requisitionId } })).status).toBe("SHORTLISTING");
  });

  it("rejects a duplicate candidate on the same requisition", async () => {
    const { requisitionId, crewMemberId } = await newApplication();
    as(managerId, "MANAGER");
    const res = await addApplication(fd({ requisitionId, crewMemberId }));
    expect("error" in res).toBe(true);
  });
});

describe("happy path to PROPOSED", () => {
  it("walks shortlist → competency → docs(+bank/EPF) → salary → manager approval", async () => {
    const { applicationId, crewMemberId } = await newApplication();
    as(manningId, "MANNING");
    expect("ok" in (await advanceStage(applicationId, "start_competency"))).toBe(true);
    await recordReferenceCheck(fd({ applicationId, refereeName: "Capt. Rao", outcome: "positive" }));
    expect("ok" in (await advanceStage(applicationId, "verify_competency"))).toBe(true);
    expect("ok" in (await verifyDocuments(fd({ applicationId, accountNumber: "123456", ifsc: "HDFC0001", uan: "UAN99" })))).toBe(true);

    // Bank/EPF captured at the docs gate
    expect((await db.bankDetail.findUniqueOrThrow({ where: { crewMemberId } })).accountNumber).toBe("123456");
    expect((await db.epfDetail.findUniqueOrThrow({ where: { crewMemberId } })).uan).toBe("UAN99");
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("SALARY_AGREEMENT");

    // MPO agrees salary → SALARY gate pending
    await agreeSalary(fd({ applicationId, rateBasis: "MONTHLY", basic: "45000" }));
    const gate = await db.applicationGate.findFirstOrThrow({ where: { applicationId, gate: "SALARY" } });
    expect(gate.result).toBe("PENDING");

    // MPO cannot approve salary
    as(manningId, "MANNING");
    expect(await approveSalary(applicationId)).toEqual({ error: "Unauthorized" });

    // Manager approves → PROPOSED, structure approved
    as(managerId, "MANAGER");
    expect("ok" in (await approveSalary(applicationId))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("PROPOSED");
    expect((await db.salaryStructure.findFirstOrThrow({ where: { applicationId } })).approvedById).toBe(managerId);
  });
});

describe("interview → selection", () => {
  it("MPO records pass → Manager selects → SELECTED + requisition SELECTED", async () => {
    const { applicationId, requisitionId } = await newApplication();
    await setStage(applicationId, "INTERVIEW");

    as(manningId, "MANNING");
    expect("ok" in (await recordInterviewResult(applicationId, true))).toBe(true);
    expect((await db.applicationGate.findFirstOrThrow({ where: { applicationId, gate: "SELECTION" } })).result).toBe("PENDING");

    // MPO cannot select
    expect(await selectCandidate(applicationId)).toEqual({ error: "Unauthorized" });

    as(managerId, "MANAGER");
    expect("ok" in (await selectCandidate(applicationId))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("SELECTED");
    expect((await db.requisition.findUniqueOrThrow({ where: { id: requisitionId } })).status).toBe("SELECTED");
  });

  it("a failed interview rejects the application", async () => {
    const { applicationId } = await newApplication();
    await setStage(applicationId, "INTERVIEW");
    as(manningId, "MANNING");
    await recordInterviewResult(applicationId, false, "Did not meet the bar");
    const app = await db.application.findUniqueOrThrow({ where: { id: applicationId } });
    expect(app.stage).toBe("REJECTED");
    expect(app.rejectedReason).toBe("Did not meet the bar");
  });

  it("cannot select before an interview result or waiver", async () => {
    const { applicationId } = await newApplication();
    await setStage(applicationId, "INTERVIEW");
    as(managerId, "MANAGER");
    const res = await selectCandidate(applicationId);
    expect("error" in res).toBe(true);
  });
});

describe("interview waiver (ex-hands, R2)", () => {
  it("MPO requests, Manager approves, then selection works without an interview", async () => {
    const { applicationId } = await newApplication("EX_HAND");
    await setStage(applicationId, "INTERVIEW");

    as(manningId, "MANNING");
    expect("ok" in (await requestInterviewWaiver(applicationId, "20 yrs with us"))).toBe(true);
    expect((await db.applicationGate.findFirstOrThrow({ where: { applicationId, gate: "WAIVER" } })).result).toBe("PENDING");

    as(managerId, "MANAGER");
    expect("ok" in (await approveInterviewWaiver(applicationId))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).interviewWaived).toBe(true);

    expect("ok" in (await selectCandidate(applicationId))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("SELECTED");
  });

  it("is refused for a non-ex-hand candidate", async () => {
    const { applicationId } = await newApplication("NEW");
    await setStage(applicationId, "INTERVIEW");
    as(manningId, "MANNING");
    const res = await requestInterviewWaiver(applicationId);
    expect("error" in res).toBe(true);
  });
});

describe("vetting gates (C3/C5)", () => {
  it("blocks completing competency & references until a reference is recorded (C5)", async () => {
    const { applicationId } = await newApplication();
    as(manningId, "MANNING");
    await advanceStage(applicationId, "start_competency"); // → COMPETENCY_AND_REFERENCES
    // No reference recorded yet → cannot advance.
    expect("error" in (await advanceStage(applicationId, "verify_competency"))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("COMPETENCY_AND_REFERENCES");
    // Record one → now it advances.
    await recordReferenceCheck(fd({ applicationId, refereeName: "Capt. Rao", outcome: "positive" }));
    expect("ok" in (await advanceStage(applicationId, "verify_competency"))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("DOC_VERIFICATION");
  });

  it("blocks document verification when a required document on file is expired (C3)", async () => {
    const { applicationId, requisitionId, crewMemberId } = await newApplication();
    await setStage(applicationId, "DOC_VERIFICATION");
    const reqRank = (await db.requisition.findUniqueOrThrow({ where: { id: requisitionId } })).rankId;
    await db.rankDocRequirement.upsert({
      where: { rankId_docType: { rankId: reqRank, docType: "MEDICAL_FITNESS" } },
      update: { isMandatory: true },
      create: { rankId: reqRank, docType: "MEDICAL_FITNESS", isMandatory: true },
    });
    await db.seafarerDocument.create({ data: { crewMemberId, docType: "MEDICAL_FITNESS", expiryDate: new Date("2020-01-01") } });

    as(manningId, "MANNING");
    expect("error" in (await verifyDocuments(fd({ applicationId })))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("DOC_VERIFICATION");

    // Renew the document → advancement proceeds.
    await db.seafarerDocument.updateMany({ where: { crewMemberId }, data: { expiryDate: new Date("2030-01-01") } });
    expect("ok" in (await verifyDocuments(fd({ applicationId })))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("SALARY_AGREEMENT");
  });
});

describe("rejection", () => {
  it("MPO rejects from a mid stage", async () => {
    const { applicationId } = await newApplication();
    await setStage(applicationId, "DOC_VERIFICATION");
    as(manningId, "MANNING");
    expect("ok" in (await rejectApplication(applicationId, "Docs not in order"))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: applicationId } })).stage).toBe("REJECTED");
  });

  it("site staff cannot drive the pipeline", async () => {
    const { applicationId } = await newApplication();
    as(siteStaffId, "SITE_STAFF");
    expect(await advanceStage(applicationId, "start_competency")).toEqual({ error: "Unauthorized" });
    expect(await rejectApplication(applicationId, "x")).toEqual({ error: "Unauthorized" });
  });
});
