/**
 * Integration tests for PO cancellation and supersede linkage.
 * Covers: cancel from any state (MANAGER/SUPERUSER, reason required), exclusion
 * from spend aggregation, and linking a cancelled PO to an existing replacement.
 *
 * POs are built directly via db.create (not the makePoForm helper) so the test is
 * self-contained and cleans up cascade-safely (POAction has no onDelete: Cascade).
 */
import { vi, describe, it, expect, beforeAll, afterEach } from "vitest";

vi.mock("@/auth", () => ({ auth: vi.fn() }));
vi.mock("next/cache", () => ({ revalidatePath: vi.fn() }));
vi.mock("@/lib/notifier", () => ({ notify: vi.fn() }));

import { auth } from "@/auth";
import { db } from "@/lib/db";
import { cancelPo, supersedePo } from "@/app/(portal)/po/[id]/actions";
import { POST_APPROVAL_STATUSES } from "@/lib/utils";
import { makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor } from "./helpers";
import type { POStatus } from "@prisma/client";

const mockedAuth = vi.mocked(auth);
const PREFIX = "INTTEST_CANCEL_";
let techId: string;
let managerId: string;
let vesselId: string;
let accountId: string;
let vendorId: string;
let seq = 0;

beforeAll(async () => {
  const [tech, mgr, vessel, account, vendor] = await Promise.all([
    getSeedUser("tech@pelagia.local"),
    getSeedUser("manager@pelagia.local"),
    getSeedVessel("MV Galatea"),
    getSeedAccount("700201"),
    getSeedVendor("Apar Industries Ltd"),
  ]);
  techId = tech.id; managerId = mgr.id;
  vesselId = vessel.id; accountId = account.id; vendorId = vendor.id;
});

afterEach(async () => {
  const pos = await db.purchaseOrder.findMany({ where: { title: { startsWith: PREFIX } }, select: { id: true } });
  const ids = pos.map((p) => p.id);
  if (ids.length === 0) return;
  await db.purchaseOrder.updateMany({ where: { id: { in: ids } }, data: { supersededById: null } });
  await db.pOAction.deleteMany({ where: { poId: { in: ids } } });
  await db.purchaseOrder.deleteMany({ where: { id: { in: ids } } });
});

async function makePo(label: string, status: POStatus): Promise<string> {
  seq += 1;
  const po = await db.purchaseOrder.create({
    data: {
      poNumber: `CANCELTEST-${seq}-${label}`,
      title: `${PREFIX}${label}`,
      status,
      totalAmount: 1180,
      currency: "INR",
      vesselId,
      accountId,
      submitterId: techId,
      ...(status === "MGR_APPROVED" ? { vendorId, approvedAt: new Date() } : {}),
      lineItems: { create: [{ name: "Test Item", quantity: 10, unit: "pc", unitPrice: 100, totalPrice: 1180, gstRate: 0.18, sortOrder: 0 }] },
      actions: { create: { actionType: "CREATED", actorId: techId } },
    },
  });
  return po.id;
}

describe("cancelPo", () => {
  it("cancels a DRAFT PO with a reason and writes an audit row", async () => {
    const poId = await makePo("Draft", "DRAFT");
    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);

    const result = await cancelPo({ poId, reason: "Duplicate order" });
    expect(result).toEqual({ ok: true });

    const po = await db.purchaseOrder.findUniqueOrThrow({ where: { id: poId } });
    expect(po.status).toBe("CANCELLED");
    expect(po.cancelledAt).not.toBeNull();
    expect(po.cancellationReason).toBe("Duplicate order");

    const action = await db.pOAction.findFirst({ where: { poId, actionType: "CANCELLED" } });
    expect(action?.note).toBe("Duplicate order");
  });

  it("cancels an already-APPROVED PO (cancellable from any state)", async () => {
    const poId = await makePo("Approved", "MGR_APPROVED");
    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);
    const result = await cancelPo({ poId, reason: "Vendor backed out" });
    expect(result).toEqual({ ok: true });
    const po = await db.purchaseOrder.findUniqueOrThrow({ where: { id: poId } });
    expect(po.status).toBe("CANCELLED");
  });

  it("a cancelled PO drops out of the spend aggregation filter", async () => {
    const poId = await makePo("Spend", "MGR_APPROVED");
    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);
    await cancelPo({ poId, reason: "Excluded from spend" });

    expect(POST_APPROVAL_STATUSES as readonly string[]).not.toContain("CANCELLED");
    const stillCounted = await db.purchaseOrder.findFirst({
      where: { id: poId, status: { in: [...POST_APPROVAL_STATUSES] } },
    });
    expect(stillCounted).toBeNull();
  });

  it("requires a reason", async () => {
    const poId = await makePo("NoReason", "DRAFT");
    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);
    const result = await cancelPo({ poId, reason: "   " });
    expect(result).toEqual({ error: "A cancellation reason is required." });
  });

  it("refuses a role without cancel_po (TECHNICAL)", async () => {
    const poId = await makePo("Forbidden", "DRAFT");
    mockedAuth.mockResolvedValue(makeSession(techId, "TECHNICAL") as never);
    const result = await cancelPo({ poId, reason: "nope" });
    expect(result).toHaveProperty("error");
    const po = await db.purchaseOrder.findUniqueOrThrow({ where: { id: poId } });
    expect(po.status).toBe("DRAFT");
  });

  it("refuses to cancel an already-cancelled PO", async () => {
    const poId = await makePo("Twice", "DRAFT");
    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);
    await cancelPo({ poId, reason: "first" });
    const result = await cancelPo({ poId, reason: "second" });
    expect(result).toEqual({ error: "This purchase order is already cancelled." });
  });
});

describe("supersedePo", () => {
  it("links a cancelled PO to an existing replacement (reciprocal)", async () => {
    const cancelledId = await makePo("Old", "DRAFT");
    const replacementId = await makePo("New", "DRAFT");
    const replacement = await db.purchaseOrder.findUniqueOrThrow({ where: { id: replacementId } });

    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);
    await cancelPo({ poId: cancelledId, reason: "Replaced" });
    const result = await supersedePo({ poId: cancelledId, replacementPoNumber: replacement.poNumber });
    expect(result).toEqual({ ok: true });

    const old = await db.purchaseOrder.findUniqueOrThrow({ where: { id: cancelledId } });
    expect(old.supersededById).toBe(replacementId);

    const repl = await db.purchaseOrder.findUniqueOrThrow({
      where: { id: replacementId },
      include: { supersedes: { select: { id: true } } },
    });
    expect(repl.supersedes.map((s) => s.id)).toContain(cancelledId);
  });

  it("refuses to supersede a PO that is not cancelled", async () => {
    const poId = await makePo("NotCancelled", "DRAFT");
    const otherId = await makePo("Other", "DRAFT");
    const other = await db.purchaseOrder.findUniqueOrThrow({ where: { id: otherId } });
    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);
    const result = await supersedePo({ poId, replacementPoNumber: other.poNumber });
    expect(result).toEqual({ error: "Only a cancelled purchase order can be superseded." });
  });

  it("rejects an unknown replacement PO number", async () => {
    const poId = await makePo("Unknown", "DRAFT");
    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);
    await cancelPo({ poId, reason: "x" });
    const result = await supersedePo({ poId, replacementPoNumber: "PMS/ZZZ/0000/2000-01" });
    expect(result).toHaveProperty("error");
  });

  it("rejects self-supersede", async () => {
    const poId = await makePo("Self", "DRAFT");
    const po = await db.purchaseOrder.findUniqueOrThrow({ where: { id: poId } });
    mockedAuth.mockResolvedValue(makeSession(managerId, "MANAGER") as never);
    await cancelPo({ poId, reason: "x" });
    const result = await supersedePo({ poId, replacementPoNumber: po.poNumber });
    expect(result).toEqual({ error: "A purchase order cannot supersede itself." });
  });
});
