import { describe, it, expect } from 'vitest'
import { buildApp } from '../src/app.js'
import { SESSION_COOKIE_NAME } from '../src/security/cookies.js'
import { createSlidingWindowLimiter, type RateLimiter } from '../src/security/rate-limit.js'
import { makeConfig, fakeCpClient, fakeMinter, authCookieHeader, TEST_SESSION_SECRET } from './helpers.js'

async function makeApp(overrides: Parameters<typeof buildApp>[0] extends infer T ? Partial<T> : never = {}) {
  return buildApp({
    config: makeConfig(),
    cpClient: fakeCpClient(),
    minter: fakeMinter(),
    staticRoot: null,
    ...overrides,
  })
}

describe('POST /login', () => {
  it('returns 503 when PANEL_PASSWORD is unset (fail-closed)', async () => {
    const app = await makeApp({ config: makeConfig({ panelPassword: undefined }) })
    const res = await app.inject({ method: 'POST', url: '/login', payload: { password: 'anything' } })
    expect(res.statusCode).toBe(503)
    await app.close()
  })

  it('returns 401 on the wrong password (no cookie set)', async () => {
    const app = await makeApp()
    const res = await app.inject({ method: 'POST', url: '/login', payload: { password: 'wrong' } })
    expect(res.statusCode).toBe(401)
    expect(res.headers['set-cookie']).toBeUndefined()
    await app.close()
  })

  it('returns 200 and sets an HttpOnly SameSite=Strict session cookie on success', async () => {
    const app = await makeApp({ config: makeConfig({ panelPassword: 'secret-pw' }) })
    const res = await app.inject({ method: 'POST', url: '/login', payload: { password: 'secret-pw' } })
    expect(res.statusCode).toBe(200)
    expect(res.json()).toEqual({ authenticated: true })
    const cookie = String(res.headers['set-cookie'])
    expect(cookie).toContain(`${SESSION_COOKIE_NAME}=`)
    expect(cookie).toContain('HttpOnly')
    expect(cookie).toContain('SameSite=Strict')
    await app.close()
  })

  it('returns 429 when rate-limited (before credential work)', async () => {
    const denyAll: RateLimiter = { allow: () => false }
    const app = await makeApp({ rateLimiter: denyAll })
    const res = await app.inject({ method: 'POST', url: '/login', payload: { password: 'whatever' } })
    expect(res.statusCode).toBe(429)
    await app.close()
  })

  it('returns 400 on a malformed body', async () => {
    const app = await makeApp()
    const res = await app.inject({ method: 'POST', url: '/login', payload: { notpassword: 1 } })
    expect(res.statusCode).toBe(400)
    await app.close()
  })

  // trustProxy: true (app.ts) makes req.ip read X-Forwarded-For, so the limiter buckets by REAL client
  // IP. Without it every request would share the single 127.0.0.1 bucket (a global lockout DoS).
  it('rate-limits per forwarded client IP: same X-Forwarded-For shares a budget, a different one is independent', async () => {
    const limiter = createSlidingWindowLimiter(1, 60_000, () => 0) // one attempt per window per key
    const app = await makeApp({ rateLimiter: limiter })
    const login = (xff: string) =>
      app.inject({ method: 'POST', url: '/login', headers: { 'x-forwarded-for': xff }, payload: { password: 'wrong' } })

    // IP A, 1st attempt: budget available → reaches credential check → 401 (not throttled).
    expect((await login('203.0.113.10')).statusCode).toBe(401)
    // IP A, 2nd attempt: same bucket exhausted → 429.
    expect((await login('203.0.113.10')).statusCode).toBe(429)
    // IP B: its own bucket → 401, proving req.ip reflects the forwarded address (else it would be 429).
    expect((await login('198.51.100.20')).statusCode).toBe(401)
    await app.close()
  })
})

describe('POST /logout', () => {
  it('clears the session cookie', async () => {
    const app = await makeApp()
    const res = await app.inject({ method: 'POST', url: '/logout' })
    expect(res.statusCode).toBe(200)
    expect(String(res.headers['set-cookie'])).toContain('Max-Age=0')
    await app.close()
  })
})

describe('GET /api/session', () => {
  it('reports authenticated:false without a cookie', async () => {
    const app = await makeApp()
    const res = await app.inject({ method: 'GET', url: '/api/session' })
    expect(res.json()).toEqual({ authenticated: false })
    await app.close()
  })

  it('reports authenticated:true with a valid session cookie', async () => {
    const app = await makeApp()
    const res = await app.inject({ method: 'GET', url: '/api/session', headers: { cookie: authCookieHeader(TEST_SESSION_SECRET) } })
    expect(res.json()).toEqual({ authenticated: true })
    await app.close()
  })
})
