import { Hono } from 'hono'
import { describe, expect, it } from 'vitest'

import { loadConfig } from './config.js'
import { AppError } from './errors.js'
import { ok, fail, onError } from './http.js'
import { createApp } from './index.js'
import { createRateLimit, type RateLimitNamespace } from './mw/rate-limit.js'
import { healthRoute } from './routes/health.js'

class MemoryRateLimitNamespace implements RateLimitNamespace {
  readonly store = new Map<string, string>()

  async get(key: string): Promise<string | null> {
    return this.store.get(key) ?? null
  }

  async put(key: string, value: string): Promise<void> {
    this.store.set(key, value)
  }
}

async function readJson(response: Response): Promise<unknown> {
  return response.json()
}

describe('http envelopes', () => {
  it('ok returns the expected envelope shape', async () => {
    const response = ok({ ok: true })

    expect(response.status).toBe(200)
    await expect(readJson(response)).resolves.toEqual({ data: { ok: true } })
  })

  it('fail returns the expected envelope shape and status', async () => {
    const response = fail('BROKEN', 'broken request', 400)

    expect(response.status).toBe(400)
    await expect(readJson(response)).resolves.toEqual({
      error: { code: 'BROKEN', message: 'broken request' },
    })
  })

  it('serializes bigint values in ok(data) as decimal strings', async () => {
    const response = ok({ amountMinor: 123n })

    await expect(readJson(response)).resolves.toEqual({
      data: { amountMinor: '123' },
    })
  })
})

describe('health route', () => {
  it('returns 200 with {data:{ok:true}}', async () => {
    const app = createApp()
    const response = await app.request('http://press-zone.test/health')

    expect(response.status).toBe(200)
    await expect(readJson(response)).resolves.toEqual({ data: { ok: true } })
  })
})

describe('rate limit middleware', () => {
  it('allows N requests and blocks the N+1th request with a 429 envelope', async () => {
    const namespace = new MemoryRateLimitNamespace()
    const app = new Hono<{ Bindings: { RATE_LIMIT_KV?: RateLimitNamespace } }>()

    app.onError(onError)
    app.use(
      '*',
      createRateLimit<{ RATE_LIMIT_KV?: RateLimitNamespace }>({
        namespace: (env) => env.RATE_LIMIT_KV,
        limit: 2,
        windowMs: 60_000,
        key: () => 'tenant:1',
      }),
    )
    app.route('/', healthRoute)

    const first = await app.request('http://press-zone.test/health', undefined, {
      RATE_LIMIT_KV: namespace,
    })
    const second = await app.request('http://press-zone.test/health', undefined, {
      RATE_LIMIT_KV: namespace,
    })
    const third = await app.request('http://press-zone.test/health', undefined, {
      RATE_LIMIT_KV: namespace,
    })

    expect(first.status).toBe(200)
    expect(second.status).toBe(200)
    expect(third.status).toBe(429)
    await expect(readJson(third)).resolves.toEqual({
      error: { code: 'RATE_LIMITED', message: 'Rate limit exceeded' },
    })
  })
})

describe('error handler', () => {
  it('maps AppError to the public error envelope', async () => {
    const app = new Hono()

    app.onError(onError)
    app.get('/boom', () => {
      throw new AppError('NOPE', 'Nope', 418)
    })

    const response = await app.request('http://press-zone.test/boom')

    expect(response.status).toBe(418)
    await expect(readJson(response)).resolves.toEqual({
      error: { code: 'NOPE', message: 'Nope' },
    })
  })
})

describe('config', () => {
  it('loads required bindings for the app request path', () => {
    expect(
      loadConfig({
        AUTH_PEPPER: 'pepper',
        AUTH_SESSION_SECRET: 'session',
        DATABASE_URL: 'postgres://postgres:postgres@127.0.0.1:5432/press_zone',
      }),
    ).toEqual({
      authPepper: 'pepper',
      authSessionSecret: 'session',
      databaseUrl: 'postgres://postgres:postgres@127.0.0.1:5432/press_zone',
    })
  })
})
