/**
 * Integration tests for Crewing Phase 4b leave & attendance: apply/decide leave
 * (Manager), the clash auto-backfill (required strength = 1), and attendance
 * recording with MPO/Manager lockout.
 */
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 { applyLeave, decideLeave } from "@/app/(portal)/crewing/leave/actions";
import { saveAttendance } from "@/app/(portal)/crewing/attendance/actions";
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@itla.local";
const as = (userId: string, role: Role) =>
  vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(userId, role));

async function makeAssignment(name: string, rId = rankId) {
  const cm = await db.crewMember.create({ data: { name, status: "EMPLOYEE", type: "NEW", source: "CAREERS" } });
  return db.crewAssignment.create({ data: { status: "ACTIVE", signOnDate: new Date("2026-01-01"), crewMemberId: cm.id, rankId: rId, vesselId } });
}

async function applyAndGetId(assignmentId: string, from = "2026-07-01", to = "2026-07-10") {
  as(siteStaffId, "SITE_STAFF");
  const res = await applyLeave(fd({ assignmentId, type: "ANNUAL", fromDate: from, toDate: to }));
  if (!("ok" in res)) throw new Error("applyLeave failed");
  return res.id!;
}

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: "ITLA-SS", email: SS_EMAIL, name: "SS LA", 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.attendance.deleteMany({});
  await db.leaveRequest.deleteMany({});
  await db.crewAssignment.deleteMany({});
  await db.requisition.deleteMany({});
  await db.vesselRankRequirement.deleteMany({});
  await db.crewMember.deleteMany({});
  vi.clearAllMocks();
});

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

describe("apply / decide leave", () => {
  it("site staff apply, Manager approves → assignment ON_LEAVE", async () => {
    const a = await makeAssignment("Solo Crew");
    const leaveId = await applyAndGetId(a.id);
    expect((await db.leaveRequest.findUniqueOrThrow({ where: { id: leaveId } })).status).toBe("APPLIED");

    as(managerId, "MANAGER");
    expect("ok" in (await decideLeave(leaveId, true))).toBe(true);
    expect((await db.leaveRequest.findUniqueOrThrow({ where: { id: leaveId } })).status).toBe("APPROVED");
    expect((await db.crewAssignment.findUniqueOrThrow({ where: { id: a.id } })).status).toBe("ON_LEAVE");
  });

  it("apply is rejected for the MPO (no apply_leave)", async () => {
    const a = await makeAssignment("X");
    as(manningId, "MANNING");
    expect(await applyLeave(fd({ assignmentId: a.id, fromDate: "2026-07-01", toDate: "2026-07-02" }))).toEqual({ error: "Unauthorized" });
  });

  it("decline requires a reason and is Manager-only", async () => {
    const a = await makeAssignment("Y");
    const leaveId = await applyAndGetId(a.id);
    as(managerId, "MANAGER");
    expect("error" in (await decideLeave(leaveId, false, "  "))).toBe(true);
    as(siteStaffId, "SITE_STAFF");
    expect(await decideLeave(leaveId, false, "no")).toEqual({ error: "Unauthorized" });
    as(managerId, "MANAGER");
    expect("ok" in (await decideLeave(leaveId, false, "Operational needs"))).toBe(true);
    expect((await db.leaveRequest.findUniqueOrThrow({ where: { id: leaveId } })).status).toBe("REJECTED");
  });
});

describe("clash auto-backfill (required strength = 1)", () => {
  it("auto-raises a LEAVE requisition when the only same-rank cover goes on leave", async () => {
    const a = await makeAssignment("Only One");
    const leaveId = await applyAndGetId(a.id);
    as(managerId, "MANAGER");
    await decideLeave(leaveId, true);

    const req = await db.requisition.findFirst({ where: { autoRaised: true } });
    expect(req).not.toBeNull();
    expect(req!.reason).toBe("LEAVE");
    expect(req!.rankId).toBe(rankId);
    expect(req!.vesselId).toBe(vesselId);
  });

  it("does NOT auto-raise when another active same-rank crew remains (default strength 1)", async () => {
    const a = await makeAssignment("Going On Leave");
    await makeAssignment("Stays Active"); // same rank + vessel, active
    const leaveId = await applyAndGetId(a.id);
    as(managerId, "MANAGER");
    await decideLeave(leaveId, true);
    expect(await db.requisition.count({ where: { autoRaised: true } })).toBe(0);
  });

  it("auto-raises when a configured required strength exceeds the remaining cover (Option A)", async () => {
    // Require 2 of this rank on the vessel; with one remaining after leave → clash.
    await db.vesselRankRequirement.create({ data: { vesselId, rankId, minStrength: 2 } });
    const a = await makeAssignment("Going On Leave");
    await makeAssignment("Stays Active");
    const leaveId = await applyAndGetId(a.id);
    as(managerId, "MANAGER");
    await decideLeave(leaveId, true);
    expect(await db.requisition.count({ where: { autoRaised: true } })).toBe(1);
  });
});

describe("attendance", () => {
  it("site staff record attendance (upsert)", async () => {
    const a = await makeAssignment("Marked");
    as(siteStaffId, "SITE_STAFF");
    expect("ok" in (await saveAttendance(a.id, [{ date: "2026-07-01", status: "PRESENT" }, { date: "2026-07-02", status: "ABSENT" }]))).toBe(true);
    expect(await db.attendance.count({ where: { assignmentId: a.id } })).toBe(2);
    // Re-saving the same day updates rather than duplicating.
    await saveAttendance(a.id, [{ date: "2026-07-01", status: "HALF_DAY" }]);
    expect(await db.attendance.count({ where: { assignmentId: a.id } })).toBe(2);
    expect((await db.attendance.findFirstOrThrow({ where: { assignmentId: a.id, status: "HALF_DAY" } })).status).toBe("HALF_DAY");
  });

  it("the MPO and the Manager cannot record attendance (R5/§6)", async () => {
    const a = await makeAssignment("NoMark");
    as(manningId, "MANNING");
    expect(await saveAttendance(a.id, [{ date: "2026-07-01", status: "PRESENT" }])).toEqual({ error: "Unauthorized" });
    as(managerId, "MANAGER");
    expect(await saveAttendance(a.id, [{ date: "2026-07-01", status: "PRESENT" }])).toEqual({ error: "Unauthorized" });
    expect(await db.attendance.count({ where: { assignmentId: a.id } })).toBe(0);
  });
});
