/**
 * Integration tests for the confirmReceipt server action.
 * Covers: full receipt, partial receipt, upsert notes on repeated confirmation,
 * and permission guards.
 */
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 { createPo } from "@/app/(portal)/po/new/actions";
import { approvePo } from "@/app/(portal)/approvals/[id]/actions";
import { processPayment, markPaid } from "@/app/(portal)/payments/actions";
import { confirmReceipt } from "@/app/(portal)/po/[id]/receipt/actions";
import {
  makeSession,
  getSeedUser,
  getSeedVessel,
  getSeedAccount,
  getSeedVendor,
  makePoForm,
  deletePosByTitle,
} from "./helpers";

const PREFIX = "INTTEST_RECEIPT_";
const TODAY = new Date().toISOString().slice(0, 10);

let techId: string;
let managerId: string;
let accountsId: string;
let vesselId: string;
let accountId: string;
let vendorId: string;

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

afterEach(async () => {
  await deletePosByTitle(PREFIX);
});

/** Create a PO and drive it to PAID_DELIVERED (fully paid). */
async function createPaidPo(title: string): Promise<string> {
  vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
  const form = makePoForm({ title, vesselId, accountId, intent: "submit" });
  const { id: poId } = (await createPo(form)) as { id: string };
  // Vendor gating: approval requires an assigned vendor.
  await db.purchaseOrder.update({ where: { id: poId }, data: { vendorId } });

  vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
  await approvePo({ poId });

  vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(accountsId, "ACCOUNTS"));
  await processPayment({ poId });
  await markPaid({ poId, paymentRef: "NEFT-TEST-RECEIPT", paymentDate: TODAY });

  vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
  return poId;
}

describe("confirmReceipt — full delivery", () => {
  it("transitions PAID_DELIVERED to CLOSED when all items delivered", async () => {
    const poId = await createPaidPo(`${PREFIX}Full`);
    const result = await confirmReceipt({ poId });
    expect(result).toEqual({ ok: true, partial: false });

    const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
    expect(po?.status).toBe("CLOSED");
    expect(po?.closedAt).not.toBeNull();
  });

  it("records RECEIPT_CONFIRMED in audit log", async () => {
    const poId = await createPaidPo(`${PREFIX}Audit`);
    await confirmReceipt({ poId });

    const action = await db.pOAction.findFirst({
      where: { poId, actionType: "RECEIPT_CONFIRMED" },
    });
    expect(action).not.toBeNull();
  });

  it("saves delivery notes on the Receipt record", async () => {
    const poId = await createPaidPo(`${PREFIX}Notes`);
    await confirmReceipt({ poId, notes: "All items received in good condition." });

    const receipt = await db.receipt.findUnique({ where: { poId } });
    expect(receipt?.notes).toBe("All items received in good condition.");
  });
});

describe("confirmReceipt — partial delivery", () => {
  it("transitions PAID_DELIVERED to PARTIALLY_CLOSED when some items remain", async () => {
    const poId = await createPaidPo(`${PREFIX}Partial`);
    const lineItems = await db.pOLineItem.findMany({ where: { poId } });
    const deliveries: Record<string, number> = {};
    for (const li of lineItems) deliveries[li.id] = 0; // deliver nothing

    const result = await confirmReceipt({ poId, deliveries });
    // delivering 0 of everything → nothingDelivered guard is in the UI, not the action
    // action still proceeds and computes PARTIALLY_CLOSED (paid but 0 delivered)
    expect(result).toEqual({ ok: true, partial: true });

    const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
    // fully paid but nothing delivered → PARTIALLY_CLOSED
    expect(po?.status).toBe("PARTIALLY_CLOSED");
  });

  it("returns partial:true for a partial delivery", async () => {
    const poId = await createPaidPo(`${PREFIX}PartialQty`);
    const lineItems = await db.pOLineItem.findMany({ where: { poId } });
    const half = Math.floor(Number(lineItems[0].quantity) / 2);
    const deliveries = { [lineItems[0].id]: half };

    const result = await confirmReceipt({ poId, deliveries });
    expect(result).toEqual({ ok: true, partial: true });
  });
});

describe("confirmReceipt — repeated notes upsert (regression for partial → full flow)", () => {
  it("succeeds on second call with notes after first partial confirmation also had notes", async () => {
    const poId = await createPaidPo(`${PREFIX}Upsert`);
    const lineItems = await db.pOLineItem.findMany({ where: { poId } });
    const half = Math.floor(Number(lineItems[0].quantity) / 2);
    const remaining = Number(lineItems[0].quantity) - half;

    // First confirmation: partial delivery with notes — creates Receipt row
    const first = await confirmReceipt({
      poId,
      notes: "First batch received.",
      deliveries: { [lineItems[0].id]: half },
    });
    expect(first).toEqual({ ok: true, partial: true });

    // Second confirmation: deliver the rest, also with notes — must not throw
    // (previously crashed due to unique constraint on Receipt.poId when using `create`)
    const second = await confirmReceipt({
      poId,
      notes: "Remaining items received.",
      deliveries: { [lineItems[0].id]: remaining },
    });
    expect(second).toEqual({ ok: true, partial: false });

    const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
    expect(po?.status).toBe("CLOSED");

    // Notes should reflect the latest confirmation
    const receipt = await db.receipt.findUnique({ where: { poId } });
    expect(receipt?.notes).toBe("Remaining items received.");
  });
});

describe("confirmReceipt — permission guards", () => {
  it("rejects non-submitter who is not SUPERUSER", async () => {
    const poId = await createPaidPo(`${PREFIX}PermFail`);

    const otherTech = await getSeedUser("tech@pelagia.local");
    // Use a different user id to simulate a different submitter
    const fakeSession = makeSession(managerId, "TECHNICAL");
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(fakeSession);

    const result = await confirmReceipt({ poId });
    expect(result).toHaveProperty("error");
    void otherTech; // suppress unused warning
  });

  it("rejects confirmation on a PO in wrong status", async () => {
    // Create a PO that is still DRAFT (no payment yet)
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
    const form = makePoForm({ title: `${PREFIX}WrongStatus`, vesselId, accountId, intent: "draft" });
    const { id: poId } = (await createPo(form)) as { id: string };

    const result = await confirmReceipt({ poId });
    expect(result).toHaveProperty("error");
  });

  it("returns error when PO does not exist", async () => {
    const result = await confirmReceipt({ poId: "nonexistent-po-id" });
    expect(result).toHaveProperty("error");
  });

  it("returns error when not authenticated", async () => {
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(null);
    const result = await confirmReceipt({ poId: "any-id" });
    expect(result).toHaveProperty("error");
  });
});
