import { eq } from "drizzle-orm";
import { randomBytes } from "node:crypto";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { getDb, getSql } from "./client";
import * as schema from "./schema";
import { withTenant, withUser } from "./tenant";

/**
 * Phase 1 exit criterion (§8): "cross-tenant isolation suite green across
 * every endpoint and socket event." This is the data-layer half — it proves
 * RLS itself holds two ways round, independent of any application code that
 * might (or, per project history, might once have failed to) call it
 * correctly. HTTP/socket-level isolation tests belong next to their
 * respective controllers/gateways as those endpoints are built.
 *
 * Requires a live Postgres reachable at DATABASE_URL with migrations + RLS
 * policies applied (`pnpm --filter @slackwsh/data run db:migrate`). Skipped
 * — not failed — when DATABASE_URL isn't set, since RLS behaviour cannot be
 * mocked (ADR-003) and there is deliberately no in-memory fallback here.
 * CI (`.github/workflows/ci.yml`) runs a real Postgres service, so this
 * suite is exercised for real there even when skipped in local dev.
 */
const hasDatabase = Boolean(process.env.DATABASE_URL);

function uniqueSuffix() {
  return randomBytes(4).toString("hex");
}

afterAll(async () => {
  if (hasDatabase) await getSql().end({ timeout: 1 });
});

describe.skipIf(!hasDatabase)("row-level security — cross-tenant isolation", () => {
  let orgA: number, orgB: number, wsA: number, wsB: number, userA: number, userB: number;

  beforeAll(async () => {
    const db = getDb();
    const suffix = uniqueSuffix();
    [orgA, orgB] = await Promise.all([
      db.insert(schema.organizations).values({ name: "org-a" }).returning({ id: schema.organizations.id }).then((r) => r[0]!.id),
      db.insert(schema.organizations).values({ name: "org-b" }).returning({ id: schema.organizations.id }).then((r) => r[0]!.id),
    ]);
    userA = await db
      .insert(schema.users)
      .values({ email: `a-${suffix}@test.local`, username: `a${suffix}`, passwordHash: "x", name: "A" })
      .returning({ id: schema.users.id })
      .then((r) => r[0]!.id);
    userB = await db
      .insert(schema.users)
      .values({ email: `b-${suffix}@test.local`, username: `b${suffix}`, passwordHash: "x", name: "B" })
      .returning({ id: schema.users.id })
      .then((r) => r[0]!.id);

    wsA = await withUser(userA, async (tx) => {
      const [workspace] = await tx
        .insert(schema.workspaces)
        .values({ orgId: orgA, slug: `ws-a-${suffix}`, name: "ws-a" })
        .returning();
      if (!workspace) throw new Error("workspace insert failed");
      await tx.execute(
        // set_config for membership insert under tenant_isolation
        (await import("drizzle-orm")).sql`select set_config('app.current_workspace_id', ${String(workspace.id)}, true)`,
      );
      await tx.insert(schema.workspaceMembers).values({ workspaceId: workspace.id, userId: userA, role: "owner" });
      return workspace.id;
    });
    wsB = await withUser(userB, async (tx) => {
      const [workspace] = await tx
        .insert(schema.workspaces)
        .values({ orgId: orgB, slug: `ws-b-${suffix}`, name: "ws-b" })
        .returning();
      if (!workspace) throw new Error("workspace insert failed");
      await tx.execute(
        (await import("drizzle-orm")).sql`select set_config('app.current_workspace_id', ${String(workspace.id)}, true)`,
      );
      await tx.insert(schema.workspaceMembers).values({ workspaceId: workspace.id, userId: userB, role: "owner" });
      return workspace.id;
    });
  });

  it("cannot read another tenant's workspace row when scoped to your own", async () => {
    const rows = await withTenant({ workspaceId: wsA, userId: userA }, (tx) =>
      tx.select().from(schema.workspaces).where(eq(schema.workspaces.id, wsB)),
    );
    expect(rows).toHaveLength(0);
  });

  it("cannot read another tenant's membership rows when scoped to your own", async () => {
    const rows = await withTenant({ workspaceId: wsA, userId: userA }, (tx) =>
      tx.select().from(schema.workspaceMembers).where(eq(schema.workspaceMembers.workspaceId, wsB)),
    );
    expect(rows).toHaveLength(0);
  });

  it("self_membership lets a user list only their own workspaces across tenants", async () => {
    const rows = await withUser(userA, (tx) =>
      tx
        .select({ id: schema.workspaces.id })
        .from(schema.workspaceMembers)
        .innerJoin(schema.workspaces, eq(schema.workspaces.id, schema.workspaceMembers.workspaceId))
        .where(eq(schema.workspaceMembers.userId, userA)),
    );
    expect(rows.map((r) => r.id)).toEqual([wsA]);
  });

  it("cannot read another user's web-push subscriptions", async () => {
    await withUser(userA, (tx) =>
      tx.insert(schema.pushSubscriptions).values({
        userId: userA,
        endpoint: `https://push.example/${uniqueSuffix()}`,
        p256dh: "p256",
        auth: "auth",
      }),
    );
    const leaked = await withUser(userB, (tx) =>
      tx.select().from(schema.pushSubscriptions).where(eq(schema.pushSubscriptions.userId, userA)),
    );
    expect(leaked).toHaveLength(0);
  });
});
