/**
 * Tests for admin users management — db/adminUsers + endpoints.
 *
 * Covers:
 *   - listAdminUsers: pagination cursor + plan-filter + search
 *   - updateAdminUser: plan-validation + ban-stamping + voice
 *   - softDeleteAdminUser: PII-scrubbing + idempotency
 *   - GET endpoint: 401 ohne admin-secret
 *   - PATCH endpoint: 401 ohne admin-secret + happy path
 *   - DELETE endpoint: 401 + happy path
 */
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";

// Snapshot der globalen Nitro-Stubs (siehe tests/setup.ts) damit wir nach
// Endpoint-Tests die Originale wiederherstellen können — sonst leakt
// `getHeader`-mock auf andere Test-Files (singleFork-Pool).
const g = globalThis as Record<string, unknown>;
const originalStubs = {
  getHeader: g.getHeader,
  getQuery: g.getQuery,
  getRouterParam: g.getRouterParam,
  readBody: g.readBody,
  useRuntimeConfig: g.useRuntimeConfig,
};

// ─── Prisma mock ─────────────────────────────────────────────────────────────

const prismaMock = vi.hoisted(() => ({
  profile: {
    findMany: vi.fn(),
    findUnique: vi.fn(),
    update: vi.fn(),
  },
}));

vi.mock("../../server/utils/prisma", () => ({
  usePrisma: () => prismaMock,
}));

import {
  listAdminUsers,
  updateAdminUser,
  softDeleteAdminUser,
} from "../../server/db/adminUsers";

beforeEach(() => {
  vi.clearAllMocks();
  // useRuntimeConfig stub gibt adminSecret für endpoint-tests
  g.useRuntimeConfig = vi.fn(() => ({
    adminSecret: "test-secret",
    public: { supabase: { url: "", key: "" } },
  }));
});

afterEach(() => {
  // Globale Nitro-Stubs zurücksetzen — sonst leakt getHeader-mock auf
  // andere Test-Files (singleFork-Pool teilt sich Modul-Globals).
  for (const [k, v] of Object.entries(originalStubs)) {
    g[k] = v;
  }
});

// ─── listAdminUsers ──────────────────────────────────────────────────────────

describe("listAdminUsers — pagination + nextCursor", () => {
  it("returns nextCursor when more rows exist (limit+1 fetched)", async () => {
    // Simuliere 3 rows bei limit=2 → over-fetch ist 3, also nextCursor = items[1].id
    const fakeRows = [
      makeRow("aaa", { plan: "free" }),
      makeRow("bbb", { plan: "pro" }),
      makeRow("ccc", { plan: "free" }),
    ];
    prismaMock.profile.findMany.mockResolvedValueOnce(fakeRows);

    const result = await listAdminUsers({ limit: 2 });

    expect(result.items).toHaveLength(2);
    expect(result.items.map((r) => r.id)).toEqual(["aaa", "bbb"]);
    expect(result.nextCursor).toBe("bbb");
    expect(prismaMock.profile.findMany).toHaveBeenCalledWith(
      expect.objectContaining({
        take: 3, // limit + 1
        where: expect.objectContaining({ deletedAt: null }),
      }),
    );
  });

  it("nextCursor is null when no more rows", async () => {
    prismaMock.profile.findMany.mockResolvedValueOnce([
      makeRow("only", { plan: "legend" }),
    ]);
    const result = await listAdminUsers({ limit: 50 });
    expect(result.nextCursor).toBeNull();
    expect(result.items).toHaveLength(1);
  });

  it("applies plan-filter + search-term to where-clause", async () => {
    prismaMock.profile.findMany.mockResolvedValueOnce([]);
    await listAdminUsers({ plan: "pro", q: "Chahine" });
    const callArgs = prismaMock.profile.findMany.mock.calls[0]![0]!;
    expect(callArgs.where.plan).toBe("pro");
    expect(callArgs.where.OR).toEqual([
      { nickname: { contains: "Chahine", mode: "insensitive" } },
      { username: { contains: "Chahine", mode: "insensitive" } },
    ]);
  });
});

// ─── updateAdminUser ─────────────────────────────────────────────────────────

describe("updateAdminUser — validates plan + stamps bannedAt", () => {
  it("rejects unknown plan-values with 400", async () => {
    await expect(
      updateAdminUser("user-id", { plan: "enterprise" }),
    ).rejects.toMatchObject({ statusCode: 400 });
    expect(prismaMock.profile.update).not.toHaveBeenCalled();
  });

  it("stamps bannedAt when banned=true and clears reason on un-ban", async () => {
    prismaMock.profile.update.mockResolvedValueOnce(
      makeRow("u1", { banned: true, bannedAt: new Date() }),
    );
    await updateAdminUser("u1", { banned: true });
    const dataArg = prismaMock.profile.update.mock.calls[0]![0]!.data;
    expect(dataArg.banned).toBe(true);
    expect(dataArg.bannedAt).toBeInstanceOf(Date);

    prismaMock.profile.update.mockResolvedValueOnce(
      makeRow("u1", { banned: false, bannedAt: null }),
    );
    await updateAdminUser("u1", { banned: false });
    const dataArg2 = prismaMock.profile.update.mock.calls[1]![0]!.data;
    expect(dataArg2.banned).toBe(false);
    expect(dataArg2.bannedAt).toBeNull();
    expect(dataArg2.bannedReason).toBeNull();
  });

  it("rejects empty patch (no allowed fields → 400)", async () => {
    await expect(updateAdminUser("user-id", {})).rejects.toMatchObject({
      statusCode: 400,
    });
  });
});

// ─── softDeleteAdminUser ─────────────────────────────────────────────────────

describe("softDeleteAdminUser — DSGVO PII-scrub + idempotent", () => {
  it("scrubs PII fields and stamps deletedAt", async () => {
    prismaMock.profile.findUnique.mockResolvedValueOnce({ deletedAt: null });
    prismaMock.profile.update.mockResolvedValueOnce({});

    const result = await softDeleteAdminUser(
      "128df360-2008-4d6f-8aa1-bdb41ec1362f",
    );

    expect(result).toEqual({ ok: true, alreadyDeleted: false });
    const updateCall = prismaMock.profile.update.mock.calls[0]![0]!;
    expect(updateCall.where.id).toBe("128df360-2008-4d6f-8aa1-bdb41ec1362f");
    expect(updateCall.data.nickname).toBeNull();
    expect(updateCall.data.avatar).toBeNull();
    expect(updateCall.data.username).toMatch(/^deleted-[a-f0-9]{8}$/);
    expect(updateCall.data.birthYear).toBeNull();
    expect(updateCall.data.gender).toBeNull();
    expect(updateCall.data.bundesland).toBeNull();
    expect(updateCall.data.stripeCustomerId).toBeNull();
    expect(updateCall.data.deletedAt).toBeInstanceOf(Date);
  });

  it("is idempotent — returns alreadyDeleted=true on re-run", async () => {
    prismaMock.profile.findUnique.mockResolvedValueOnce({
      deletedAt: new Date("2026-01-01"),
    });
    const result = await softDeleteAdminUser("user-id");
    expect(result).toEqual({ ok: true, alreadyDeleted: true });
    expect(prismaMock.profile.update).not.toHaveBeenCalled();
  });

  it("throws 404 if user not found", async () => {
    prismaMock.profile.findUnique.mockResolvedValueOnce(null);
    await expect(softDeleteAdminUser("ghost")).rejects.toMatchObject({
      statusCode: 404,
    });
  });
});

// ─── Endpoints — Auth-Guard ──────────────────────────────────────────────────

describe("GET /api/admin/users — 401 ohne admin-secret", () => {
  it("rejects request without x-admin-secret header", async () => {
    (globalThis as Record<string, unknown>).getHeader = vi.fn(() => undefined);
    (globalThis as Record<string, unknown>).getQuery = vi.fn(() => ({}));

    const mod = await import("../../server/api/admin/users/index.get");
    const handler = mod.default as (e: unknown) => Promise<unknown>;

    await expect(handler({})).rejects.toMatchObject({ statusCode: 401 });
  });
});

describe("PATCH /api/admin/users/[id] — 401 ohne admin-secret", () => {
  it("rejects request with wrong secret", async () => {
    (globalThis as Record<string, unknown>).getHeader = vi.fn(
      () => "wrong-secret",
    );
    (globalThis as Record<string, unknown>).getRouterParam = vi.fn(
      () => "user-id",
    );
    (globalThis as Record<string, unknown>).readBody = vi.fn(async () => ({
      banned: true,
    }));

    const mod = await import("../../server/api/admin/users/[id].patch");
    const handler = mod.default as (e: unknown) => Promise<unknown>;
    await expect(handler({})).rejects.toMatchObject({ statusCode: 401 });
  });
});

describe("DELETE /api/admin/users/[id] — 401 ohne admin-secret", () => {
  it("rejects request without secret", async () => {
    (globalThis as Record<string, unknown>).getHeader = vi.fn(() => undefined);
    (globalThis as Record<string, unknown>).getRouterParam = vi.fn(
      () => "user-id",
    );

    const mod = await import("../../server/api/admin/users/[id].delete");
    const handler = mod.default as (e: unknown) => Promise<unknown>;
    await expect(handler({})).rejects.toMatchObject({ statusCode: 401 });
  });
});

// ─── Helpers ─────────────────────────────────────────────────────────────────

function makeRow(id: string, overrides: Partial<Record<string, unknown>> = {}) {
  return {
    id,
    nickname: `nick-${id}`,
    username: `user-${id}`,
    avatar: null,
    plan: "free",
    streak: 0,
    banned: false,
    bannedAt: null,
    deletedAt: null,
    createdAt: new Date(),
    lyraVoiceId: null,
    premiumUntil: null,
    proTrialExpiresAt: null,
    ...overrides,
  };
}
