// src/lib/support/actions.test.ts — support ticketing client actions (postgres).
// 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: "client-a", orgId: "demo-org", isStaff: false };

mock.module("@/lib/active-org", () => ({
  getActiveUserId: async () => state.userId,
  getActiveOrgId: async () => state.orgId,
}));
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 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_A = "client-a";
  const CLIENT_B = "client-b";
  const STAFF = "staff-user";

  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();
  }

  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_A);
    await ensureUser(CLIENT_B);
    await ensureUser(STAFF);
    await ensureOrg(ORG);
    // default identity
    state.userId = CLIENT_A;
    state.orgId = ORG;
    state.isStaff = false;
  });

  test("createTicketAction inserts the ticket + first client message", async () => {
    const { createTicketAction } = await import("./actions");
    const id = await createTicketAction({
      title: "Mon souci",
      content: "Ça marche pas",
      priority: "high",
    });
    expect(id).toBeTruthy();

    const [ticket] = await db
      .select()
      .from(schema.supportTickets)
      .where(eq(schema.supportTickets.id, id));
    expect(ticket.title).toBe("Mon souci");
    expect(ticket.status).toBe("open");
    expect(ticket.priority).toBe("high");
    expect(ticket.userId).toBe(CLIENT_A);
    expect(ticket.orgId).toBe(ORG);

    const msgs = await db
      .select()
      .from(schema.supportMessages)
      .where(eq(schema.supportMessages.ticketId, id));
    expect(msgs.length).toBe(1);
    expect(msgs[0].senderType).toBe("client");
    expect(msgs[0].senderId).toBe(CLIENT_A);
    expect(msgs[0].content).toBe("Ça marche pas");
  });

  test("createTicketAction rejects an empty title", async () => {
    const { createTicketAction } = await import("./actions");
    await expect(
      createTicketAction({ title: "   ", content: "ok", priority: "low" }),
    ).rejects.toThrow();
  });

  test("createTicketAction rejects empty content", async () => {
    const { createTicketAction } = await import("./actions");
    await expect(
      createTicketAction({ title: "Titre", content: "", priority: "low" }),
    ).rejects.toThrow();
  });

  test("postMessageAction by the owner appends a client message", async () => {
    const { createTicketAction, postMessageAction, getTicketThreadAction } =
      await import("./actions");
    const id = await createTicketAction({
      title: "T",
      content: "first",
      priority: "medium",
    });
    await postMessageAction(id, "une réponse");
    const thread = await getTicketThreadAction(id);
    expect(thread?.messages.length).toBe(2);
    expect(thread?.messages[1].content).toBe("une réponse");
    expect(thread?.messages[1].senderType).toBe("client");
  });

  test("postMessageAction throws on a resolved ticket (lock)", async () => {
    const { createTicketAction, postMessageAction } = await import("./actions");
    const id = await createTicketAction({
      title: "T",
      content: "first",
      priority: "medium",
    });
    await db
      .update(schema.supportTickets)
      .set({ status: "resolved" })
      .where(eq(schema.supportTickets.id, id));
    await expect(postMessageAction(id, "trop tard")).rejects.toThrow(/résolu/i);
  });

  test("postMessageAction by a non-owner non-staff throws", async () => {
    const { createTicketAction, postMessageAction } = await import("./actions");
    const id = await createTicketAction({
      title: "T",
      content: "first",
      priority: "medium",
    });
    // Switch to a stranger (not owner, not staff).
    state.userId = CLIENT_B;
    state.isStaff = false;
    await expect(postMessageAction(id, "intrus")).rejects.toThrow(/refusé/i);
  });

  test("a super-admin posting yields senderType 'staff'", async () => {
    const { createTicketAction, postMessageAction, getTicketThreadAction } =
      await import("./actions");
    const id = await createTicketAction({
      title: "T",
      content: "first",
      priority: "medium",
    });
    // Staff (different user) replies.
    state.userId = STAFF;
    state.isStaff = true;
    await postMessageAction(id, "réponse staff");
    const thread = await getTicketThreadAction(id);
    const last = thread?.messages.at(-1);
    expect(last?.senderType).toBe("staff");
    expect(last?.senderId).toBe(STAFF);
  });

  test("getMyTicketsAction returns only the caller's tickets", async () => {
    const { createTicketAction, getMyTicketsAction } = await import(
      "./actions"
    );
    await createTicketAction({ title: "A1", content: "x", priority: "low" });
    await createTicketAction({ title: "A2", content: "x", priority: "low" });
    // Client B creates one.
    state.userId = CLIENT_B;
    await createTicketAction({ title: "B1", content: "x", priority: "low" });

    state.userId = CLIENT_A;
    const mine = await getMyTicketsAction();
    expect(mine.length).toBe(2);
    expect(mine.every((t) => t.userId === CLIENT_A)).toBe(true);
  });

  test("getTicketThreadAction: owner ok, stranger gets null", async () => {
    const { createTicketAction, getTicketThreadAction } = await import(
      "./actions"
    );
    const id = await createTicketAction({
      title: "T",
      content: "first",
      priority: "medium",
    });
    // Owner sees it.
    expect(await getTicketThreadAction(id)).not.toBeNull();
    // Stranger (non-owner, non-staff) sees null.
    state.userId = CLIENT_B;
    state.isStaff = false;
    expect(await getTicketThreadAction(id)).toBeNull();
    // Staff sees it.
    state.userId = STAFF;
    state.isStaff = true;
    expect(await getTicketThreadAction(id)).not.toBeNull();
  });
});
