/**
 * Integration tests that lock in the Manager-only "return/decline" gates and the
 * remaining verification gates across the crewing pipeline — the reconciliation
 * rulings most likely to regress silently:
 *   - R8: salary/selection approval (and their *returns*) are Manager-only.
 *   - R2: an interview waiver can never reach a NEW candidate by any path.
 *   - R11/§8.11: PPE / next-of-kin verify gates (MPO) + bank reject-with-remarks.
 *   - §5.4/H3: only an MPO_VERIFIED appraisal can be Manager-approved.
 * Forward happy-paths are already covered by applications/verification/appraisal
 * suites; these focus on the negative and role-gating edges.
 */
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 {
  returnSalary,
  returnSelection,
  requestInterviewWaiver,
  declineInterviewWaiver,
} from "@/app/(portal)/crewing/applications/actions";
import { verifyBankEpf, verifyPpe, verifyNextOfKin } from "@/app/(portal)/crewing/verification/actions";
import { raiseAppraisal, approveAppraisal } from "@/app/(portal)/crewing/appraisals/actions";
import { makeSession, getSeedUser, fd } from "./helpers";
import type { ApplicationStage, GateResult, Role } from "@prisma/client";

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

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

let seq = 0;
async function applicationAt(
  stage: ApplicationStage,
  opts: { type?: "NEW" | "EX_HAND"; interviewResult?: "PENDING" | "ACCEPTED" } = {}
) {
  seq += 1;
  const req = await db.requisition.create({ data: { code: `REQ-G${seq}`, rankId, vesselId, reason: "NEW_VACANCY", status: "SHORTLISTING" } });
  const cand = await db.crewMember.create({
    data: {
      name: opts.type === "EX_HAND" ? "Ex G" : "New G",
      type: opts.type ?? "NEW",
      status: opts.type === "EX_HAND" ? "EX_HAND" : "CANDIDATE",
      source: opts.type === "EX_HAND" ? "EX_HAND" : "CAREERS",
      appliedRankId: rankId,
    },
  });
  const app = await db.application.create({
    data: { requisitionId: req.id, crewMemberId: cand.id, stage, type: opts.type ?? "NEW", interviewResult: opts.interviewResult ?? "PENDING" },
  });
  return { appId: app.id, reqId: req.id, candId: cand.id };
}

const gate = (applicationId: string, gateType: "SALARY" | "SELECTION" | "WAIVER", result: GateResult = "PENDING") =>
  db.applicationGate.create({ data: { applicationId, gate: gateType, result } });

beforeAll(async () => {
  managerId = (await getSeedUser("manager@pelagia.local")).id;
  manningId = (await getSeedUser("manning@pelagia.local")).id;
  accountsId = (await getSeedUser("accounts@pelagia.local")).id;
  const ss = await db.user.upsert({ where: { email: SS_EMAIL }, update: { role: "SITE_STAFF", isActive: true }, create: { employeeId: "ITGATES-SS", email: SS_EMAIL, name: "SS Gates", 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.appraisal.deleteMany({});
  await db.salaryStructure.deleteMany({});
  await db.applicationGate.deleteMany({});
  await db.referenceCheck.deleteMany({});
  await db.application.deleteMany({});
  await db.nextOfKin.deleteMany({});
  await db.ppeIssue.deleteMany({});
  await db.bankDetail.deleteMany({});
  await db.epfDetail.deleteMany({});
  await db.crewAssignment.deleteMany({});
  await db.requisition.deleteMany({});
  await db.crewMember.deleteMany({});
  vi.clearAllMocks();
});

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

describe("salary return is Manager-only and audited (R8)", () => {
  it("MPO cannot return salary; Manager needs a reason; reason rejects the SALARY gate", async () => {
    const { appId } = await applicationAt("SALARY_AGREEMENT");
    await db.salaryStructure.create({ data: { applicationId: appId, rateBasis: "MONTHLY", basic: 60000 } });
    await gate(appId, "SALARY");

    as(manningId, "MANNING");
    expect(await returnSalary(appId, "Too high")).toEqual({ error: "Unauthorized" });

    as(managerId, "MANAGER");
    expect("error" in (await returnSalary(appId, "   "))).toBe(true); // reason required
    expect("ok" in (await returnSalary(appId, "Re-negotiate basic"))).toBe(true);

    expect((await db.applicationGate.findFirstOrThrow({ where: { applicationId: appId, gate: "SALARY" } })).result).toBe("REJECTED");
    // Audited as a return, not as a forward "salary agreed".
    expect(await db.crewAction.count({ where: { applicationId: appId, actionType: "SALARY_RETURNED" } })).toBe(1);
  });
});

describe("selection return is Manager-only (R8)", () => {
  it("MPO cannot return a selection; Manager return resets the interview result and rejects the gate", async () => {
    const { appId } = await applicationAt("INTERVIEW", { interviewResult: "ACCEPTED" });
    await gate(appId, "SELECTION");

    as(manningId, "MANNING");
    expect(await returnSelection(appId, "Reconsider")).toEqual({ error: "Unauthorized" });

    as(managerId, "MANAGER");
    expect("ok" in (await returnSelection(appId, "Pending references"))).toBe(true);
    const app = await db.application.findUniqueOrThrow({ where: { id: appId } });
    expect(app.interviewResult).toBe("PENDING");
    expect((await db.applicationGate.findFirstOrThrow({ where: { applicationId: appId, gate: "SELECTION" } })).result).toBe("REJECTED");
    expect(await db.crewAction.count({ where: { applicationId: appId, actionType: "SELECTION_RETURNED" } })).toBe(1);
  });
});

describe("interview waiver can never reach a NEW candidate (R2)", () => {
  it("the Manager cannot request a waiver (no request_interview_waiver) and NEW stays un-waived", async () => {
    const { appId } = await applicationAt("INTERVIEW", { type: "NEW" });
    // Manager lacks request_interview_waiver entirely.
    as(managerId, "MANAGER");
    expect(await requestInterviewWaiver(appId)).toEqual({ error: "Unauthorized" });
    // MPO can request, but the candidate type blocks it for a NEW hand.
    as(manningId, "MANNING");
    expect("error" in (await requestInterviewWaiver(appId))).toBe(true);
    expect((await db.application.findUniqueOrThrow({ where: { id: appId } })).interviewWaived).toBe(false);
  });

  it("declining a waiver is Manager-only, needs a reason, and rejects the WAIVER gate", async () => {
    const { appId } = await applicationAt("INTERVIEW", { type: "EX_HAND" });
    await gate(appId, "WAIVER");

    as(manningId, "MANNING");
    expect(await declineInterviewWaiver(appId, "No")).toEqual({ error: "Unauthorized" });

    as(managerId, "MANAGER");
    expect("error" in (await declineInterviewWaiver(appId, "  "))).toBe(true); // reason required
    expect("ok" in (await declineInterviewWaiver(appId, "Interview required"))).toBe(true);
    expect((await db.applicationGate.findFirstOrThrow({ where: { applicationId: appId, gate: "WAIVER" } })).result).toBe("REJECTED");
    expect(await db.crewAction.count({ where: { applicationId: appId, actionType: "WAIVER_DECLINED" } })).toBe(1);
  });
});

describe("bank verification reject path (Accounts, §8.11)", () => {
  it("rejecting bank details requires remarks and sets REJECTED", async () => {
    const c = await db.crewMember.create({ data: { name: "Bank Reject", status: "EMPLOYEE", type: "NEW", source: "CAREERS" } });
    await db.bankDetail.create({ data: { crewMemberId: c.id, accountNumber: "999", ifsc: "ICIC0001" } });

    as(accountsId, "ACCOUNTS");
    expect("error" in (await verifyBankEpf(c.id, "bank", false))).toBe(true); // remarks required
    expect("ok" in (await verifyBankEpf(c.id, "bank", false, "Name mismatch"))).toBe(true);
    expect((await db.bankDetail.findUniqueOrThrow({ where: { crewMemberId: c.id } })).verificationStatus).toBe("REJECTED");
  });
});

describe("PPE & next-of-kin verify gates (MPO, §8.11 follow-up)", () => {
  it("MPO verifies a next-of-kin record; site staff and Accounts cannot", async () => {
    const c = await db.crewMember.create({ data: { name: "NoK Crew", status: "EMPLOYEE", type: "NEW", source: "CAREERS" } });
    const nok = await db.nextOfKin.create({ data: { crewMemberId: c.id, name: "Spouse", relationship: "Wife", isEmergency: true } });

    as(siteStaffId, "SITE_STAFF");
    expect(await verifyNextOfKin(nok.id, true)).toEqual({ error: "Unauthorized" });
    as(accountsId, "ACCOUNTS");
    expect(await verifyNextOfKin(nok.id, true)).toEqual({ error: "Unauthorized" });

    as(manningId, "MANNING");
    expect("ok" in (await verifyNextOfKin(nok.id, true))).toBe(true);
    expect((await db.nextOfKin.findUniqueOrThrow({ where: { id: nok.id } })).verificationStatus).toBe("VERIFIED");
  });

  it("MPO rejects a PPE issue only with remarks", async () => {
    const c = await db.crewMember.create({ data: { name: "PPE Crew", status: "EMPLOYEE", type: "NEW", source: "CAREERS" } });
    const ppe = await db.ppeIssue.create({ data: { crewMemberId: c.id, item: "BOILER_SUIT", size: "L" } });

    as(manningId, "MANNING");
    expect("error" in (await verifyPpe(ppe.id, false))).toBe(true); // remarks required
    expect("ok" in (await verifyPpe(ppe.id, false, "Wrong size logged"))).toBe(true);
    expect((await db.ppeIssue.findUniqueOrThrow({ where: { id: ppe.id } })).verificationStatus).toBe("REJECTED");
  });
});

describe("appraisal approval requires MPO verification first (H3)", () => {
  it("a SUBMITTED appraisal cannot be Manager-approved without MPO verification", async () => {
    const c = await db.crewMember.create({ data: { name: "Appraisee G", status: "EMPLOYEE", type: "NEW", source: "CAREERS" } });
    const assignment = await db.crewAssignment.create({ data: { status: "ACTIVE", signOnDate: new Date("2026-01-01"), crewMemberId: c.id, rankId, vesselId } });
    as(siteStaffId, "SITE_STAFF");
    const raised = await raiseAppraisal(fd({ assignmentId: assignment.id, period: "2026", competence: "4", conduct: "4", safety: "4" }));
    if (!("ok" in raised)) throw new Error("raise failed");

    // Straight to Manager approve, skipping MPO verify → blocked by the state machine.
    as(managerId, "MANAGER");
    expect("error" in (await approveAppraisal(raised.id!, true))).toBe(true);
    expect((await db.appraisal.findUniqueOrThrow({ where: { id: raised.id! } })).status).toBe("SUBMITTED");
  });
});
