import { sql } from 'drizzle-orm'
import { integer, pgTable, text } from 'drizzle-orm/pg-core'
import { it, expect } from 'vitest'
import {
  assertTransactionIdentity,
  getTransactionIdentity,
  isTransactionCapabilityError,
  type PostgresTransaction,
} from '../index.js'
import { createPgliteClient } from './pglite.js'

const users = pgTable('users', {
  id: integer('id').primaryKey().generatedAlwaysAsIdentity(),
  name: text('name').notNull(),
})

const schema = { users }

it('round-trips insert and relational query typed from schema', async () => {
  const db = createPgliteClient({ schema })

  await db.execute(
    sql`CREATE TABLE users (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name text NOT NULL)`,
  )

  await db.insert(users).values({ name: 'alice' })

  const rows = await db.query.users.findMany()
  expect(rows).toEqual([{ id: 1, name: 'alice' }])
})

it('rolls back when .transaction() callback throws', async () => {
  const db = createPgliteClient({ schema })

  await db.execute(
    sql`CREATE TABLE users (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name text NOT NULL)`,
  )

  await expect(
    db.transaction(async (tx) => {
      await tx.insert(users).values({ name: 'rollback-me' })
      throw new Error('rollback')
    }),
  ).rejects.toThrow('rollback')

  const rows = await db.query.users.findMany()
  expect(rows).toHaveLength(0)
})

it('rejects independent and completed transaction capabilities before effects', async () => {
  const db = createPgliteClient({ schema })
  await db.execute(
    sql`CREATE TABLE users (id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, name text NOT NULL)`,
  )
  let completed: PostgresTransaction<typeof schema> | undefined
  let escapedWrite: PromiseLike<unknown> | undefined
  let escapedRelationalRead: PromiseLike<unknown> | undefined
  let escapedQueryDescriptor: object | undefined

  const identity = await db.transaction(async (tx) => {
    completed = tx
    escapedWrite = tx.insert(users).values({ name: 'escaped' })
    escapedRelationalRead = tx.query.users.findMany()
    escapedQueryDescriptor = Object.getOwnPropertyDescriptor(tx, 'query')?.value as object
    expect(Reflect.defineProperty(tx, 'extra', { value: true })).toBe(false)
    expect(Reflect.deleteProperty(tx, 'query')).toBe(false)
    expect(Reflect.setPrototypeOf(tx, {})).toBe(false)
    expect(() => Object.preventExtensions(tx)).toThrow(TypeError)
    expect(Object.isExtensible(tx)).toBe(true)
    expect(Reflect.defineProperty(tx.query, 'extra', { value: true })).toBe(false)
    expect(Reflect.deleteProperty(tx.query, 'users')).toBe(false)
    expect(Reflect.setPrototypeOf(tx.query, {})).toBe(false)
    expect(() => Object.preventExtensions(tx.query)).toThrow(TypeError)
    expect(Object.isExtensible(tx.query)).toBe(true)
    expect(getTransactionIdentity(tx)).toBe(getTransactionIdentity(tx))
    return getTransactionIdentity(tx)
  })

  expect(() => getTransactionIdentity(completed!)).toThrowError(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
  await expect(completed!.execute(sql`select 1`)).rejects.toEqual(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
  await expect(Promise.resolve(escapedWrite!)).rejects.toEqual(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
  await expect(Promise.resolve(escapedRelationalRead!)).rejects.toEqual(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
  expect(() => Object.getOwnPropertyDescriptors(escapedQueryDescriptor!)).toThrowError(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
  expect(Object.getOwnPropertyDescriptor(completed!, 'session')).toBeUndefined()
  expect(() => Object.getPrototypeOf(completed!)).toThrowError(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
  expect(() => Object.isExtensible(completed!)).toThrowError(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
  await expect(db.select().from(users)).resolves.toHaveLength(0)
  let escapedNestedEffects = 0
  await expect(
    completed!.transaction(async () => {
      escapedNestedEffects += 1
    }),
  ).rejects.toEqual(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
  expect(escapedNestedEffects).toBe(0)

  let effects = 0
  await db.transaction(async (tx) => {
    try {
      assertTransactionIdentity(tx, identity)
    } catch (error) {
      if (!isTransactionCapabilityError(error)) throw error
      expect(error.reason).toBe('identity-mismatch')
      return
    }
    effects += 1
  })

  expect(effects).toBe(0)

  let rejected: PostgresTransaction<typeof schema> | undefined
  await expect(
    db.transaction(async (tx) => {
      rejected = tx
      throw new Error('rollback')
    }),
  ).rejects.toThrow('rollback')
  expect(() => getTransactionIdentity(rejected!)).toThrowError(
    expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
  )
})

it('mints a distinct nested identity and restores the outer identity', async () => {
  const db = createPgliteClient({ schema })
  let nested: PostgresTransaction<typeof schema> | undefined

  await db.transaction(async (outer) => {
    const outerIdentity = getTransactionIdentity(outer)
    await expect(outer.execute<{ value: number }>(sql`select 1 as value`)).resolves.toEqual([
      { value: 1 },
    ])

    let nestedEffects = 0
    await outer.transaction(async (inner) => {
      nested = inner
      expect(getTransactionIdentity(inner)).not.toBe(outerIdentity)

      try {
        assertTransactionIdentity(inner, outerIdentity)
      } catch (error) {
        if (!isTransactionCapabilityError(error)) throw error
        expect(error.reason).toBe('identity-mismatch')
        return
      }
      nestedEffects += 1
    })
    expect(nestedEffects).toBe(0)

    expect(getTransactionIdentity(outer)).toBe(outerIdentity)
    expect(() => getTransactionIdentity(nested!)).toThrowError(
      expect.objectContaining({ code: 'transaction-capability', reason: 'inactive' }),
    )
  })
})
