import { eq, sql } from 'drizzle-orm'
import { Hono, type MiddlewareHandler } from 'hono'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { createPgliteClient } from '@platform-modules/db/pglite'

import { AppError } from '../errors.js'
import { fail, onError } from '../http.js'
import { periodKey, seedPeriodWallet } from '../lib/wallet.js'
import { pressZoneInitSql, pressZoneSchema, walletBalances } from '../schema.js'
const { accessGuard, authGuard, mockedRequireAuth, mockedRequirePluginAccess } = vi.hoisted(() => {
  const auth = vi.fn(async (_context: unknown, next: () => Promise<void>) => {
    await next()
  })
  const access = vi.fn(async (_context: unknown, next: () => Promise<void>) => {
    await next()
  })

  return {
    accessGuard: access,
    authGuard: auth,
    mockedRequireAuth: vi.fn(() => auth),
    mockedRequirePluginAccess: vi.fn(() => access),
  }
})

vi.mock('../mw/auth.js', () => ({
  requireAuth: mockedRequireAuth,
}))

vi.mock('../mw/access.js', () => ({
  requirePluginAccess: mockedRequirePluginAccess,
}))

import { createPluginGuards } from './plugin.js'
import { createPluginRoute } from './plugin.js'

async function createTestDb() {
  const db = createPgliteClient({ schema: pressZoneSchema })

  for (const statement of pressZoneInitSql.split(';').map((part) => part.trim()).filter(Boolean)) {
    await db.execute(sql.raw(statement))
  }

  return db
}

async function readBalance(
  db: ReturnType<typeof createPgliteClient<typeof pressZoneSchema>>,
  ownerId: string,
): Promise<bigint> {
  const [row] = await db
    .select({ balance: walletBalances.balance })
    .from(walletBalances)
    .where(eq(walletBalances.ownerId, ownerId))
    .limit(1)

  return row?.balance ?? 0n
}

type TestBindings = {}
type TestVariables = {
  account?: string
  capabilities: string[]
  principal: {
    account: string
    scopes: string[]
    userId: string
  }
}

function allowAuth(account = 'acct_123'): MiddlewareHandler<{
  Bindings: TestBindings
  Variables: TestVariables
}> {
  return async (context, next) => {
    context.set('principal', {
      account,
      userId: 'machine-user',
      scopes: ['plugin:translate'],
    })
    context.set('capabilities', ['plugin:translate'])
    await next()
  }
}

const allowAccess: MiddlewareHandler = async (_context, next) => {
  await next()
}

function denyAccess(seenAccounts: string[]): MiddlewareHandler<{
  Bindings: TestBindings
  Variables: TestVariables
}> {
  return async (context) => {
    seenAccounts.push(context.get('account') ?? 'missing')
    return fail('FORBIDDEN', 'Forbidden', 403)
  }
}

describe('createPluginRoute', () => {
  let db: Awaited<ReturnType<typeof createTestDb>>

  beforeEach(async () => {
    db = await createTestDb()
  })

  it('debits credits once and returns downstream data on the happy path', async () => {
    const walletKey = periodKey('acct_123', 'translate', '2026-07')
    await seedPeriodWallet(db, walletKey, 5n)

    const execute = vi.fn(async () => ({ translated: true }))
    const app = new Hono<{ Bindings: TestBindings }>()
    app.onError(onError)
    app.route(
      '/',
      createPluginRoute({
        access: allowAccess,
        auth: allowAuth(),
        cost: 3n,
        currentPeriod: () => '2026-07',
        execute,
        getDb: () => db,
        plugin: 'translate',
      }),
    )

    const response = await app.request('http://press-zone.test/api/plugin/translate', {
      method: 'POST',
    })

    expect(response.status).toBe(200)
    await expect(response.json()).resolves.toEqual({
      data: {
        translated: true,
      },
    })
    expect(execute).toHaveBeenCalledTimes(1)
    expect(await readBalance(db, walletKey)).toBe(2n)
  })

  it('returns 402 without recording a debit when credits are insufficient', async () => {
    const walletKey = periodKey('acct_123', 'translate', '2026-07')
    await seedPeriodWallet(db, walletKey, 2n)

    const execute = vi.fn(async () => ({ translated: true }))
    const app = new Hono<{ Bindings: TestBindings }>()
    app.onError(onError)
    app.route(
      '/',
      createPluginRoute({
        access: allowAccess,
        auth: allowAuth(),
        cost: 3n,
        currentPeriod: () => '2026-07',
        execute,
        getDb: () => db,
        plugin: 'translate',
      }),
    )

    const response = await app.request('http://press-zone.test/api/plugin/translate', {
      method: 'POST',
    })

    expect(response.status).toBe(402)
    await expect(response.json()).resolves.toEqual({
      error: {
        code: 'INSUFFICIENT_CREDITS',
        message: 'Insufficient credits',
      },
    })
    expect(execute).not.toHaveBeenCalled()
    expect(await readBalance(db, walletKey)).toBe(2n)
  })

  it('refunds the debit when the downstream handler fails after charging credits', async () => {
    const walletKey = periodKey('acct_123', 'translate', '2026-07')
    await seedPeriodWallet(db, walletKey, 4n)

    const execute = vi.fn(async () => {
      throw new AppError('DOWNSTREAM_FAILED', 'Downstream failed', 502)
    })
    const app = new Hono<{ Bindings: TestBindings }>()
    app.onError(onError)
    app.route(
      '/',
      createPluginRoute({
        access: allowAccess,
        auth: allowAuth(),
        cost: 4n,
        currentPeriod: () => '2026-07',
        execute,
        getDb: () => db,
        plugin: 'translate',
      }),
    )

    const response = await app.request('http://press-zone.test/api/plugin/translate', {
      method: 'POST',
    })

    expect(response.status).toBe(502)
    await expect(response.json()).resolves.toEqual({
      error: {
        code: 'DOWNSTREAM_FAILED',
        message: 'Downstream failed',
      },
    })
    expect(execute).toHaveBeenCalledTimes(1)
    expect(await readBalance(db, walletKey)).toBe(4n)
  })

  it('binds the account before access runs and never debits when access blocks the request', async () => {
    const walletKey = periodKey('acct_123', 'translate', '2026-07')
    await seedPeriodWallet(db, walletKey, 5n)

    const seenAccounts: string[] = []
    const execute = vi.fn(async () => ({ translated: true }))
    const app = new Hono<{ Bindings: TestBindings }>()
    app.onError(onError)
    app.route(
      '/',
      createPluginRoute({
        access: denyAccess(seenAccounts),
        auth: allowAuth(),
        cost: 3n,
        currentPeriod: () => '2026-07',
        execute,
        getDb: () => db,
        plugin: 'translate',
      }),
    )

    const response = await app.request('http://press-zone.test/api/plugin/translate', {
      method: 'POST',
    })

    expect(response.status).toBe(403)
    await expect(response.json()).resolves.toEqual({
      error: {
        code: 'FORBIDDEN',
        message: 'Forbidden',
      },
    })
    expect(seenAccounts).toEqual(['acct_123'])
    expect(execute).not.toHaveBeenCalled()
    expect(await readBalance(db, walletKey)).toBe(5n)
  })

  it('exposes the requireAuth -> bindAccount -> requirePluginAccess guard chain for mounting', () => {
    mockedRequireAuth.mockReturnValue(authGuard)
    mockedRequirePluginAccess.mockReturnValue(accessGuard)

    const guards = createPluginGuards({
      auth: {
        accessCookieName: 'access_token',
        authEngine: {} as never,
        db: {} as never,
        tenancy: {
          resolveCapabilities: vi.fn(),
        },
      },
      entitlement: 'plugin:translate',
      permission: 'translate.write',
    })

    expect(guards).toHaveLength(3)
    expect(guards[0]).toBe(authGuard)
    expect(guards[2]).toBe(accessGuard)
    expect(mockedRequirePluginAccess).toHaveBeenCalledWith('plugin:translate', 'translate.write')
  })
})
