/**
 * Integration tests for manager approval server actions.
 * Covers: M-02 (approve / approve+note), M-03 (reject), M-04 (request edits, vendor ID), S-06 (provide vendor ID), S-07 (resubmit after edits).
 */
import { vi, describe, it, expect, beforeAll, beforeEach, 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 { updatePo } from "@/app/(portal)/po/[id]/edit/actions";
import {
  approvePo, rejectPo, requestEdits, requestVendorId,
} from "@/app/(portal)/approvals/[id]/actions";
import { provideVendorId } from "@/app/(portal)/po/[id]/actions";
import {
  makeSession, getSeedUser, getSeedVessel, getSeedAccount, getSeedVendor,
  makePoForm, deletePosByTitle,
} from "./helpers";

const PREFIX = "INTTEST_APPROVAL_";
let techId: string;
let managerId: string;
let vesselId: string;
let accountId: string;
let vendorId: string;

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

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

// Helper: create a PO in MGR_REVIEW state
async function createSubmittedPo(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 result = await createPo(form);
  const id = (result as { id: string }).id;
  // Vendor gating: a vendor must be assigned before a PO can be approved.
  // Attach the seeded verified vendor directly (test setup) so approval-path tests run.
  await db.purchaseOrder.update({ where: { id }, data: { vendorId } });
  return id;
}

// ── M-02: Approve ─────────────────────────────────────────────────────────────

describe("M-02 — approve PO", () => {
  it("transitions PO from MGR_REVIEW to MGR_APPROVED", async () => {
    const poId = await createSubmittedPo(`${PREFIX}Approve`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));

    const result = await approvePo({ poId });
    expect(result).toEqual({ ok: true });

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

  it("stores managerNote when approving with note", async () => {
    const poId = await createSubmittedPo(`${PREFIX}ApproveNote`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));

    await approvePo({ poId, note: "Approved — expedite delivery", withNote: true });

    const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
    expect(po?.managerNote).toBe("Approved — expedite delivery");
    const action = await db.pOAction.findFirst({
      where: { poId, actionType: "APPROVED_WITH_NOTE" },
    });
    expect(action).not.toBeNull();
  });

  it("notifies submitter and accounts on approval", async () => {
    const { notify } = await import("@/lib/notifier");
    vi.mocked(notify).mockClear();
    const poId = await createSubmittedPo(`${PREFIX}ApproveNotify`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));

    await approvePo({ poId });
    expect(vi.mocked(notify)).toHaveBeenCalledWith(
      expect.objectContaining({ event: "PO_APPROVED" })
    );
  });

  it("returns error when TECHNICAL role tries to approve", async () => {
    const poId = await createSubmittedPo(`${PREFIX}ApproveForbidden`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
    const result = await approvePo({ poId });
    expect(result).toHaveProperty("error");
  });

  it("returns error when PO is not in MGR_REVIEW state", async () => {
    // Create a DRAFT PO, don't submit
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
    const form = makePoForm({ title: `${PREFIX}ApproveDraft`, vesselId, accountId, intent: "draft" });
    const { id: poId } = (await createPo(form)) as { id: string };

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

// ── M-03: Reject ──────────────────────────────────────────────────────────────

describe("M-03 — reject PO", () => {
  it("transitions PO from MGR_REVIEW to REJECTED with note", async () => {
    const poId = await createSubmittedPo(`${PREFIX}Reject`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));

    const result = await rejectPo({ poId, note: "Budget exceeded for this quarter" });
    expect(result).toEqual({ ok: true });

    const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
    expect(po?.status).toBe("REJECTED");
    expect(po?.managerNote).toBe("Budget exceeded for this quarter");
  });

  it("creates a REJECTED action entry in the audit trail", async () => {
    const poId = await createSubmittedPo(`${PREFIX}RejectAudit`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
    await rejectPo({ poId, note: "Not needed" });

    const action = await db.pOAction.findFirst({ where: { poId, actionType: "REJECTED" } });
    expect(action?.note).toBe("Not needed");
  });

  it("notifies submitter on rejection", async () => {
    const { notify } = await import("@/lib/notifier");
    vi.mocked(notify).mockClear();
    const poId = await createSubmittedPo(`${PREFIX}RejectNotify`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));

    await rejectPo({ poId, note: "See notes" });
    expect(vi.mocked(notify)).toHaveBeenCalledWith(
      expect.objectContaining({ event: "PO_REJECTED" })
    );
  });
});

// ── M-04: Request edits ──────────────────────────────────────────────────────

describe("M-04 — request edits", () => {
  it("transitions PO to EDITS_REQUESTED with manager note", async () => {
    const poId = await createSubmittedPo(`${PREFIX}Edits`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));

    const result = await requestEdits({ poId, note: "Please add vendor ID" });
    expect(result).toEqual({ ok: true });

    const po = await db.purchaseOrder.findUnique({ where: { id: poId } });
    expect(po?.status).toBe("EDITS_REQUESTED");
    expect(po?.managerNote).toBe("Please add vendor ID");
  });
});

// ── M-04: Request vendor ID ──────────────────────────────────────────────────

describe("M-04 — request vendor ID", () => {
  it("transitions PO to VENDOR_ID_PENDING", async () => {
    const poId = await createSubmittedPo(`${PREFIX}VendorIdReq`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));

    const result = await requestVendorId({ poId });
    expect(result).toEqual({ ok: true });

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

// ── S-06: Provide vendor ID ──────────────────────────────────────────────────

describe("S-06 — provide vendor ID", () => {
  it("transitions VENDOR_ID_PENDING back to MGR_REVIEW", async () => {
    const poId = await createSubmittedPo(`${PREFIX}ProvideVendor`);
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
    await requestVendorId({ poId });

    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
    const result = await provideVendorId({ poId, vendorId });
    expect(result).toEqual({ ok: true });

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

// ── Inventory update on approval ─────────────────────────────────────────────

describe("inventory — updated at MGR_APPROVED, not at closure", () => {
  it("increments site inventory for line items with productId on approval", async () => {
    const site = await db.site.findFirstOrThrow({ where: { code: "BOM" } });
    const product = await db.product.findFirstOrThrow({ where: { code: "LUBE-EP80W90" } });

    const before = await db.itemInventory.findUnique({
      where: { productId_siteId: { productId: product.id, siteId: site.id } },
    });
    const qtyBefore = Number(before?.quantity ?? 0);

    const po = await db.purchaseOrder.create({
      data: {
        poNumber: `INVTEST-${Date.now()}`,
        title: `${PREFIX}InvApproval`,
        status: "MGR_REVIEW",
        totalAmount: 1000,
        currency: "INR",
        vesselId,
        accountId,
        vendorId,
        siteId: site.id,
        submitterId: techId,
        submittedAt: new Date(),
        lineItems: {
          create: [{
            name: "Gear Oil 80W90",
            quantity: 5,
            unit: "L",
            unitPrice: 182,
            totalPrice: 910,
            gstRate: 0.18,
            sortOrder: 0,
            productId: product.id,
          }],
        },
        actions: { create: { actionType: "SUBMITTED", actorId: techId } },
      },
    });

    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
    const result = await approvePo({ poId: po.id });
    expect(result).toEqual({ ok: true });

    const after = await db.itemInventory.findUnique({
      where: { productId_siteId: { productId: product.id, siteId: site.id } },
    });
    expect(Number(after?.quantity)).toBe(qtyBefore + 5);
  });

  it("skips inventory update for line items without a productId", async () => {
    const site = await db.site.findFirstOrThrow({ where: { code: "BOM" } });
    const countBefore = await db.itemInventory.count({ where: { siteId: site.id } });

    const po = await db.purchaseOrder.create({
      data: {
        poNumber: `INVTEST-NOPROD-${Date.now()}`,
        title: `${PREFIX}InvNoProduct`,
        status: "MGR_REVIEW",
        totalAmount: 500,
        currency: "INR",
        vesselId,
        accountId,
        vendorId,
        siteId: site.id,
        submitterId: techId,
        submittedAt: new Date(),
        lineItems: {
          create: [{
            name: "Ad-hoc supply",
            quantity: 2,
            unit: "pc",
            unitPrice: 100,
            totalPrice: 200,
            gstRate: 0.18,
            sortOrder: 0,
            productId: null,
          }],
        },
        actions: { create: { actionType: "SUBMITTED", actorId: techId } },
      },
    });

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

    const countAfter = await db.itemInventory.count({ where: { siteId: site.id } });
    expect(countAfter).toBe(countBefore);
  });

  it("does not touch inventory for vessel-only POs (no siteId)", async () => {
    const totalBefore = await db.itemInventory.count();

    const po = await db.purchaseOrder.create({
      data: {
        poNumber: `INVTEST-VESSEL-${Date.now()}`,
        title: `${PREFIX}InvVessel`,
        status: "MGR_REVIEW",
        totalAmount: 300,
        currency: "INR",
        vesselId,
        accountId,
        vendorId,
        submitterId: techId,
        submittedAt: new Date(),
        lineItems: {
          create: [{
            name: "Vessel supply",
            quantity: 3,
            unit: "pc",
            unitPrice: 50,
            totalPrice: 150,
            gstRate: 0.18,
            sortOrder: 0,
          }],
        },
        actions: { create: { actionType: "SUBMITTED", actorId: techId } },
      },
    });

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

    const totalAfter = await db.itemInventory.count();
    expect(totalAfter).toBe(totalBefore);
  });
});

// ── S-07: Edit and resubmit ──────────────────────────────────────────────────

describe("S-07 — edit and resubmit after edits requested", () => {
  it("resubmitting from EDITS_REQUESTED transitions to MGR_REVIEW", async () => {
    const poId = await createSubmittedPo(`${PREFIX}Resubmit`);

    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(managerId, "MANAGER"));
    await requestEdits({ poId, note: "Update line items" });

    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
    const form = makePoForm({ title: `${PREFIX}Resubmit`, vesselId, accountId, intent: "resubmit" });
    const result = await updatePo(poId, form);
    expect(result).toEqual({ id: poId });

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

  it("saving edits without resubmitting stays as DRAFT (save intent)", async () => {
    // Create a DRAFT PO
    vi.mocked(auth as unknown as () => Promise<unknown>).mockResolvedValue(makeSession(techId, "TECHNICAL"));
    const form = makePoForm({ title: `${PREFIX}SaveDraft`, vesselId, accountId, intent: "draft" });
    const { id: poId } = (await createPo(form)) as { id: string };

    const editForm = makePoForm({ title: `${PREFIX}SaveDraft`, vesselId, accountId, intent: "draft" });
    const result = await updatePo(poId, editForm);
    expect(result).toEqual({ id: poId });

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