import { PGlite } from '@electric-sql/pglite'
import { eq } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/pglite'
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { describe, expect, it } from 'vitest'
import { ScopeViolationError, type ScopedQuerier } from './index.js'
import { createFkIsolation } from './isolation-fk.js'

const itemsTable = pgTable('isolation_items', {
  id: uuid('id').primaryKey().defaultRandom(),
  tenantId: uuid('tenant_id').notNull(),
  label: text('label').notNull(),
})

const orgIdTable = pgTable('isolation_org_items', {
  id: uuid('id').primaryKey().defaultRandom(),
  orgId: uuid('org_id').notNull(),
  label: text('label').notNull(),
})

const unregisteredTable = pgTable('isolation_public_items', {
  id: uuid('id').primaryKey().defaultRandom(),
  label: text('label').notNull(),
})

const missingColumnTable = pgTable('isolation_no_tenant', {
  id: uuid('id').primaryKey().defaultRandom(),
  label: text('label').notNull(),
})

const schema = {
  items: itemsTable,
  orgItems: orgIdTable,
  publicItems: unregisteredTable,
  noTenant: missingColumnTable,
}
type Schema = typeof schema
const distDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../dist')

const TENANT_A = '00000000-0000-4000-8000-000000000001'
const TENANT_B = '00000000-0000-4000-8000-000000000002'

async function createFixture() {
  const client = new PGlite()
  const db = drizzle(client, { schema })
  await client.exec(`
    CREATE TABLE isolation_items (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      tenant_id uuid NOT NULL,
      label text NOT NULL
    );
    CREATE TABLE isolation_org_items (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      org_id uuid NOT NULL,
      label text NOT NULL
    );
    CREATE TABLE isolation_public_items (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      label text NOT NULL
    );
    CREATE TABLE isolation_no_tenant (
      id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
      label text NOT NULL
    );
  `)
  await db.insert(itemsTable).values([
    { tenantId: TENANT_A, label: 'a-only' },
    { tenantId: TENANT_B, label: 'b-only' },
  ])
  return db
}

function fk(tables: readonly (typeof itemsTable)[] = [itemsTable]) {
  return createFkIsolation<Schema>({ tables })
}

describe('createFkIsolation', () => {
  it('SELECT through scopeQuerier returns only the active tenant rows', async () => {
    const db = await createFixture()
    const { scopeQuerier } = fk()
    const scoped = scopeQuerier(db, TENANT_A)
    const rows = (await scoped.select({ label: itemsTable.label }).from(itemsTable)) as Array<{
      label: string
    }>
    expect(rows.map((r) => r.label)).toEqual(['a-only'])
  })

  it('INSERT without tenantId stamps the active tenant and typechecks', async () => {
    const db = await createFixture()
    const { scopeQuerier } = fk()
    const scoped = scopeQuerier(db, TENANT_A)
    await scoped.insert(itemsTable).values({ label: 'stamped' })
    const raw = await db.select().from(itemsTable).where(eq(itemsTable.label, 'stamped'))
    expect(raw).toHaveLength(1)
    expect(raw[0]?.tenantId).toBe(TENANT_A)
  })

  it('UPDATE is tenant-constrained and cannot mutate another tenant row', async () => {
    const db = await createFixture()
    const { scopeQuerier } = fk()
    const scoped = scopeQuerier(db, TENANT_A)
    const updateBuilder = scoped.update(itemsTable).set({ label: 'hijacked' }) as {
      where: (condition: ReturnType<typeof eq>) => Promise<unknown>
    }
    await updateBuilder.where(eq(itemsTable.label, 'b-only'))
    const victim = await db
      .select()
      .from(itemsTable)
      .where(eq(itemsTable.label, 'b-only'))
    expect(victim[0]?.label).toBe('b-only')
  })

  it('DELETE is tenant-constrained and cannot remove another tenant row', async () => {
    const db = await createFixture()
    const { scopeQuerier } = fk()
    const scoped = scopeQuerier(db, TENANT_A)
    await scoped.delete(itemsTable).where(eq(itemsTable.label, 'b-only'))
    const remaining = await db.select().from(itemsTable)
    expect(remaining).toHaveLength(2)
  })

  it('throws ScopeViolation when an UPDATE tries to reassign the tenant column', async () => {
    const db = await createFixture()
    const { scopeQuerier } = fk()
    const scoped = scopeQuerier(db, TENANT_A)
    expect(() =>
      scoped.update(itemsTable).set({ tenantId: TENANT_B } as never),
    ).toThrow(ScopeViolationError)
    // the would-be victim row is untouched (no silent cross-tenant reassignment)
    const aRow = await db.select().from(itemsTable).where(eq(itemsTable.label, 'a-only'))
    expect(aRow[0]?.tenantId).toBe(TENANT_A)
  })

  it('throws ScopeViolation for an unregistered table', async () => {
    const db = await createFixture()
    const { scopeQuerier } = fk()
    const scoped = scopeQuerier(db, TENANT_A)
    expect(() => scoped.select().from(unregisteredTable)).toThrow(ScopeViolationError)
  })

  it('throws ScopeViolation for a registered table missing the tenant column', async () => {
    const db = await createFixture()
    const { scopeQuerier } = createFkIsolation<Schema>({
      tables: [missingColumnTable],
    })
    const scoped = scopeQuerier(db, TENANT_A)
    expect(() => scoped.select().from(missingColumnTable)).toThrow(ScopeViolationError)
  })

  it('throws ScopeViolation on raw execute escape attempt', () => {
    const db = {} as never
    const { scopeQuerier } = fk()
    const scoped = scopeQuerier(db, TENANT_A) as ScopedQuerier & {
      execute: unknown
    }
    expect(() => scoped.execute).toThrow(ScopeViolationError)
  })

  it('throws ScopeViolation on $client escape attempt', () => {
    const db = {} as never
    const { scopeQuerier } = fk()
    const scoped = scopeQuerier(db, TENANT_A) as ScopedQuerier & {
      $client: unknown
    }
    expect(() => scoped.$client).toThrow(ScopeViolationError)
  })

  it('honours a configurable tenant column name', async () => {
    const client = new PGlite()
    const db = drizzle(client, { schema })
    await client.exec(`
      CREATE TABLE isolation_org_items (
        id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
        org_id uuid NOT NULL,
        label text NOT NULL
      );
    `)
    await db.insert(orgIdTable).values({ orgId: TENANT_A, label: 'scoped' })
    const { scopeQuerier } = createFkIsolation<Schema, 'org_id'>({
      column: 'org_id',
      tables: [orgIdTable],
    })
    const scoped = scopeQuerier(db, TENANT_A)
    const rows = (await scoped.select().from(orgIdTable)) as Array<{ label: string }>
    expect(rows).toHaveLength(1)
    expect(rows[0]?.label).toBe('scoped')
  })

  it('built package subpath throws the same ScopeViolationError class exported by the barrel', async () => {
    const [{ ScopeViolationError: DistScopeViolationError }, { createFkIsolation: createDistFkIsolation }] =
      await Promise.all([
        import(pathToFileURL(path.join(distDir, 'index.js')).href),
        import(pathToFileURL(path.join(distDir, 'isolation-fk.js')).href),
      ])

    const db = await createFixture()
    const scoped = createDistFkIsolation({ tables: [itemsTable] }).scopeQuerier(db, TENANT_A)

    let thrown: unknown
    try {
      scoped.select().from(unregisteredTable)
    } catch (error) {
      thrown = error
    }

    expect(thrown).toBeInstanceOf(DistScopeViolationError)
  })
})
