// src/lib/support/staff-actions.test.ts — staff support actions (postgres, ELEVA-105).
// Integration tests — require Postgres + the test DB migrated. Skipped unless DB_TESTS=1.
import { beforeEach, describe, expect, mock, test } from "bun:test";
import { eq } from "drizzle-orm";

// server-only throws outside Next.js; stub it.
mock.module("server-only", () => ({}));
// next/cache primitives throw outside a Next request scope; stub them.
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
}));

// Mutable identity + staff flag so individual tests can flip them.
const state = { userId: "staff-a", isStaff: true };

mock.module("@/lib/active-org", () => ({
  getActiveUserId: async () => state.userId,
  getActiveOrgId: async () => "demo-org",
}));
mock.module("@/lib/auth/super-admin", () => ({
  isCurrentUserSuperAdmin: async () => state.isStaff,
  assertSuperAdmin: async () => {
    if (!state.isStaff) throw new Error("Forbidden: super-admin only");
  },
  isSuperAdmin: () => state.isStaff,
  emailInCsv: () => state.isStaff,
}));

const RUN_DB = process.env.DB_TESTS === "1";

describe.skipIf(!RUN_DB)("support staff actions (postgres)", () => {
  let db: typeof import("@/lib/db/client").db;
  let schema: typeof import("@/lib/db/domain-schema");
  let authSchema: typeof import("@/lib/db/auth-schema");

  const ORG = "demo-org";
  const CLIENT = "client-x";
  const STAFF_A = "staff-a";
  const STAFF_B = "staff-b";

  async function ensureUser(id: string) {
    await db
      .insert(authSchema.user)
      .values({
        id,
        name: `User ${id}`,
        email: `${id}@seo-insight.local`,
        emailVerified: true,
        createdAt: new Date(),
        updatedAt: new Date(),
      })
      .onConflictDoNothing();
  }

  async function ensureOrg(id: string) {
    await db
      .insert(authSchema.organization)
      .values({ id, name: "Demo Org", slug: id, createdAt: new Date() })
      .onConflictDoNothing();
  }

  let seq = 0;
  async function makeTicket(opts: {
    priority?: "low" | "medium" | "high";
    status?: "open" | "in_progress" | "resolved";
    assignedTo?: string | null;
    createdAt?: Date;
  }) {
    seq += 1;
    const id = `t-${seq}-${Math.random().toString(36).slice(2, 8)}`;
    await db.insert(schema.supportTickets).values({
      id,
      title: `Ticket ${id}`,
      status: opts.status ?? "open",
      priority: opts.priority ?? "medium",
      userId: CLIENT,
      orgId: ORG,
      assignedTo: opts.assignedTo ?? null,
      ...(opts.createdAt ? { createdAt: opts.createdAt } : {}),
    });
    return id;
  }

  beforeEach(async () => {
    ({ db } = await import("@/lib/db/client"));
    schema = await import("@/lib/db/domain-schema");
    authSchema = await import("@/lib/db/auth-schema");
    const { assertTestDb } = await import("@/lib/db/assert-test-db");
    await assertTestDb();
    await db.delete(schema.supportMessages);
    await db.delete(schema.supportTickets);
    await ensureUser(CLIENT);
    await ensureUser(STAFF_A);
    await ensureUser(STAFF_B);
    await ensureOrg(ORG);
    state.userId = STAFF_A;
    state.isStaff = true;
  });

  test("getStaffInboxAction throws for a non-staff caller", async () => {
    const { getStaffInboxAction } = await import("./staff-actions");
    state.isStaff = false;
    await expect(getStaffInboxAction("open")).rejects.toThrow(/super-admin/i);
  });

  test("filter 'open' excludes resolved tickets", async () => {
    const { getStaffInboxAction } = await import("./staff-actions");
    await makeTicket({ status: "open" });
    await makeTicket({ status: "in_progress" });
    await makeTicket({ status: "resolved" });
    const rows = await getStaffInboxAction("open");
    expect(rows.length).toBe(2);
    expect(rows.every((r) => r.status !== "resolved")).toBe(true);
  });

  test("filter 'unassigned' excludes assigned tickets", async () => {
    const { getStaffInboxAction } = await import("./staff-actions");
    await makeTicket({ assignedTo: null });
    await makeTicket({ assignedTo: STAFF_A });
    const rows = await getStaffInboxAction("unassigned");
    expect(rows.length).toBe(1);
    expect(rows[0].assignedTo).toBeNull();
  });

  test("filter 'mine' returns only tickets assigned to the caller (active)", async () => {
    const { getStaffInboxAction } = await import("./staff-actions");
    await makeTicket({ assignedTo: STAFF_A });
    await makeTicket({ assignedTo: STAFF_B });
    await makeTicket({ assignedTo: null });
    await makeTicket({ assignedTo: STAFF_A, status: "resolved" });
    const rows = await getStaffInboxAction("mine");
    expect(rows.length).toBe(1);
    expect(rows[0].assignedTo).toBe(STAFF_A);
  });

  test("inbox sorts by priority (high → low) then oldest-first", async () => {
    const { getStaffInboxAction } = await import("./staff-actions");
    const old = new Date("2026-01-01T00:00:00Z");
    const newer = new Date("2026-06-01T00:00:00Z");
    const low = await makeTicket({ priority: "low" });
    const highNew = await makeTicket({ priority: "high", createdAt: newer });
    const highOld = await makeTicket({ priority: "high", createdAt: old });
    const med = await makeTicket({ priority: "medium" });
    const rows = await getStaffInboxAction("open");
    const ids = rows.map((r) => r.id);
    // high (oldest first), then medium, then low.
    expect(ids).toEqual([highOld, highNew, med, low]);
  });

  test("inbox joins creator + assignee names", async () => {
    const { getStaffInboxAction } = await import("./staff-actions");
    await makeTicket({ assignedTo: STAFF_B });
    const [row] = await getStaffInboxAction("open");
    expect(row.creatorName).toBe(`User ${CLIENT}`);
    expect(row.creatorEmail).toBe(`${CLIENT}@seo-insight.local`);
    expect(row.assigneeName).toBe(`User ${STAFF_B}`);
  });

  test("assignTicketAction sets and clears assignedTo", async () => {
    const { assignTicketAction } = await import("./staff-actions");
    const id = await makeTicket({ assignedTo: null });
    await assignTicketAction(id, STAFF_B);
    let [t] = await db
      .select()
      .from(schema.supportTickets)
      .where(eq(schema.supportTickets.id, id));
    expect(t.assignedTo).toBe(STAFF_B);
    await assignTicketAction(id, null);
    [t] = await db
      .select()
      .from(schema.supportTickets)
      .where(eq(schema.supportTickets.id, id));
    expect(t.assignedTo).toBeNull();
  });

  test("assignTicketAction throws for a non-staff caller", async () => {
    const { assignTicketAction } = await import("./staff-actions");
    const id = await makeTicket({});
    state.isStaff = false;
    await expect(assignTicketAction(id, STAFF_A)).rejects.toThrow(
      /super-admin/i,
    );
  });

  test("selfAssignAction assigns the ticket to the caller", async () => {
    const { selfAssignAction } = await import("./staff-actions");
    const id = await makeTicket({ assignedTo: null });
    state.userId = STAFF_B;
    await selfAssignAction(id);
    const [t] = await db
      .select()
      .from(schema.supportTickets)
      .where(eq(schema.supportTickets.id, id));
    expect(t.assignedTo).toBe(STAFF_B);
  });

  test("setTicketStatusAction updates the status", async () => {
    const { setTicketStatusAction } = await import("./staff-actions");
    const id = await makeTicket({ status: "open" });
    await setTicketStatusAction(id, "in_progress");
    let [t] = await db
      .select()
      .from(schema.supportTickets)
      .where(eq(schema.supportTickets.id, id));
    expect(t.status).toBe("in_progress");
    await setTicketStatusAction(id, "resolved");
    [t] = await db
      .select()
      .from(schema.supportTickets)
      .where(eq(schema.supportTickets.id, id));
    expect(t.status).toBe("resolved");
  });
});
