// src/lib/mcp/auth.test.ts — resolveMcpOrg (postgres).
// Integration tests — require Postgres + the test DB migrated. Skipped unless DB_TESTS=1.
import { beforeEach, describe, expect, mock, test } from "bun:test";

// server-only throws outside Next.js; stub it.
mock.module("server-only", () => ({}));
// keys-actions (imported transitively) calls revalidatePath at module load via
// its action exports — stub next/cache to keep it inert.
mock.module("next/cache", () => ({
  revalidatePath: () => {},
  revalidateTag: () => {},
  updateTag: () => {},
  unstable_cache: (fn: unknown) => fn,
}));

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

describe.skipIf(!RUN_DB)("resolveMcpOrg (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 USER = "seed-owner";

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

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

  /** Insert a key for ORG, returning the clear key. */
  async function seedKey(opts?: { revoked?: boolean }): Promise<string> {
    const { generateApiKey } = await import("./keys");
    const { key, prefix, hash } = generateApiKey();
    await db.insert(schema.mcpApiKeys).values({
      id: `key-${prefix}-${Math.random().toString(36).slice(2)}`,
      orgId: ORG,
      userId: USER,
      name: "Test key",
      prefix,
      keyHash: hash,
      revokedAt: opts?.revoked ? new Date() : null,
    });
    return key;
  }

  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.mcpApiKeys);
    await db.delete(schema.orgAddons);
    await ensureUser(USER);
    await ensureOrg(ORG, "Demo Org", "demo-org");
  });

  async function enableAddon() {
    await db
      .insert(schema.orgAddons)
      .values({ orgId: ORG, addon: ADDON })
      .onConflictDoNothing();
  }

  test("missing Authorization header → 401", async () => {
    const { resolveMcpOrg } = await import("./auth");
    const r = await resolveMcpOrg(null);
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(401);
  });

  test("non-bearer / blank token → 401", async () => {
    const { resolveMcpOrg } = await import("./auth");
    expect((await resolveMcpOrg("Basic abc")).ok).toBe(false);
    expect((await resolveMcpOrg("Bearer    ")).ok).toBe(false);
  });

  test("unknown key → 401", async () => {
    const { resolveMcpOrg } = await import("./auth");
    const r = await resolveMcpOrg("Bearer eli_does_not_exist");
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(401);
  });

  test("revoked key → 401", async () => {
    await enableAddon();
    const key = await seedKey({ revoked: true });
    const { resolveMcpOrg } = await import("./auth");
    const r = await resolveMcpOrg(`Bearer ${key}`);
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(401);
  });

  test("valid key but add-on disabled → 403", async () => {
    const key = await seedKey();
    const { resolveMcpOrg } = await import("./auth");
    const r = await resolveMcpOrg(`Bearer ${key}`);
    expect(r.ok).toBe(false);
    if (!r.ok) expect(r.status).toBe(403);
  });

  test("valid key + add-on enabled → resolves orgId and bumps lastUsedAt", async () => {
    await enableAddon();
    const key = await seedKey();
    const { resolveMcpOrg } = await import("./auth");
    const r = await resolveMcpOrg(`Bearer ${key}`);
    expect(r.ok).toBe(true);
    if (r.ok) expect(r.orgId).toBe(ORG);

    // lastUsedAt is best-effort; give the fire-and-forget update a tick.
    await new Promise((res) => setTimeout(res, 50));
    const { eq } = await import("drizzle-orm");
    const { hashApiKey } = await import("./keys");
    const [row] = await db
      .select()
      .from(schema.mcpApiKeys)
      .where(eq(schema.mcpApiKeys.keyHash, hashApiKey(key)));
    expect(row.lastUsedAt).not.toBeNull();
  });

  test("each valid resolve increments requestCount (0 → 1 → 2)", async () => {
    await enableAddon();
    const key = await seedKey();
    const { resolveMcpOrg } = await import("./auth");
    const { eq } = await import("drizzle-orm");
    const { hashApiKey } = await import("./keys");

    async function countAndLastUsed(): Promise<{
      requestCount: number;
      lastUsedAt: Date | null;
    }> {
      const [row] = await db
        .select({
          requestCount: schema.mcpApiKeys.requestCount,
          lastUsedAt: schema.mcpApiKeys.lastUsedAt,
        })
        .from(schema.mcpApiKeys)
        .where(eq(schema.mcpApiKeys.keyHash, hashApiKey(key)));
      return row;
    }

    expect((await countAndLastUsed()).requestCount).toBe(0);

    await resolveMcpOrg(`Bearer ${key}`);
    await new Promise((res) => setTimeout(res, 60));
    const after1 = await countAndLastUsed();
    expect(after1.requestCount).toBe(1);
    expect(after1.lastUsedAt).not.toBeNull();

    await resolveMcpOrg(`Bearer ${key}`);
    await new Promise((res) => setTimeout(res, 60));
    expect((await countAndLastUsed()).requestCount).toBe(2);
  });
});
