import { describe, it, expect } from 'vitest'
import { buildApp } from '../src/app.js'
import { CpClientError } from '../src/cp-client.js'
import { HostIdSchema } from '../src/routes/api-routes.js'
import { makeConfig, fakeCpClient, fakeMinter, authCookieHeader } from './helpers.js'

const AUTH = { cookie: authCookieHeader() }

describe('session gating', () => {
  it('rejects every proxy route without a valid session cookie (401)', async () => {
    const app = await buildApp({ config: makeConfig(), cpClient: fakeCpClient(), minter: fakeMinter(), staticRoot: null })
    for (const r of [
      { method: 'GET' as const, url: '/api/hosts' },
      { method: 'POST' as const, url: '/api/pairing-codes' },
      { method: 'DELETE' as const, url: '/api/hosts/abc' },
    ]) {
      const res = await app.inject(r)
      expect(res.statusCode).toBe(401)
    }
    await app.close()
  })
})

describe('GET /api/hosts', () => {
  it('mints a token and returns the CP host list for the operator account', async () => {
    const cpClient = fakeCpClient({
      hosts: [{ hostId: 'h1', subdomain: 'alpha', status: 'online', lastSeen: 'x', createdAt: 'y', revokedAt: null, notAfter: undefined }],
    })
    const minter = fakeMinter('MINTED')
    const app = await buildApp({ config: makeConfig({ operatorAccountId: 'acct-XYZ' }), cpClient, minter, staticRoot: null })

    const res = await app.inject({ method: 'GET', url: '/api/hosts', headers: AUTH })
    expect(res.statusCode).toBe(200)
    expect(res.json()).toEqual({ hosts: [{ hostId: 'h1', subdomain: 'alpha', status: 'online', lastSeen: 'x', createdAt: 'y', revokedAt: null, notAfter: undefined }] })
    expect(minter.calls()).toBe(1)
    expect(cpClient.calls[0]).toMatchObject({ method: 'listHosts', accountIdOrHostId: 'acct-XYZ', token: 'MINTED' })
    await app.close()
  })

  it('maps an upstream CP failure to 502', async () => {
    const cpClient = fakeCpClient({ throwErr: new CpClientError(403, 'denied') })
    const app = await buildApp({ config: makeConfig(), cpClient, minter: fakeMinter(), staticRoot: null })
    const res = await app.inject({ method: 'GET', url: '/api/hosts', headers: AUTH })
    expect(res.statusCode).toBe(502)
    expect(res.json()).toEqual({ error: 'upstream_error' })
    await app.close()
  })
})

describe('POST /api/pairing-codes', () => {
  it('returns the code, pair command, and a QR data URL', async () => {
    const cpClient = fakeCpClient({ pairing: { code: 'ABCD-EFGH', expiresAt: '2026-03-01T00:00:00.000Z' } })
    const app = await buildApp({ config: makeConfig({ tunnelZone: 'z.test' }), cpClient, minter: fakeMinter(), staticRoot: null })

    const res = await app.inject({ method: 'POST', url: '/api/pairing-codes', headers: AUTH })
    expect(res.statusCode).toBe(201)
    const body = res.json()
    expect(body.code).toBe('ABCD-EFGH')
    expect(body.expiresAt).toBe('2026-03-01T00:00:00.000Z')
    expect(body.pairCommand).toBe('web-terminal-agent pair ABCD-EFGH --install --zone z.test')
    expect(String(body.qrDataUrl).startsWith('data:image/png;base64,')).toBe(true)
    await app.close()
  })
})

describe('DELETE /api/hosts/:hostId', () => {
  it('revokes the host and returns 204', async () => {
    const cpClient = fakeCpClient()
    const app = await buildApp({ config: makeConfig(), cpClient, minter: fakeMinter('T'), staticRoot: null })
    const res = await app.inject({ method: 'DELETE', url: '/api/hosts/host-42', headers: AUTH })
    expect(res.statusCode).toBe(204)
    expect(cpClient.calls[0]).toMatchObject({ method: 'deleteHost', accountIdOrHostId: 'host-42', token: 'T' })
    await app.close()
  })

  it('rejects an invalid host id with 400', async () => {
    const cpClient = fakeCpClient()
    const app = await buildApp({ config: makeConfig(), cpClient, minter: fakeMinter(), staticRoot: null })
    const res = await app.inject({ method: 'DELETE', url: '/api/hosts/' + encodeURIComponent('bad id!'), headers: AUTH })
    expect(res.statusCode).toBe(400)
    expect(cpClient.calls).toHaveLength(0)
    await app.close()
  })
})

describe('HostIdSchema dot-segment rejection', () => {
  // The HTTP router already normalizes literal `.`/`..` path segments, but the outbound CP admin URL is
  // built as `${cpUrl}/hosts/${hostId}` — so the schema itself must refuse dot-segments (defense-in-depth).
  it('rejects a bare "." and ".."', () => {
    expect(HostIdSchema.safeParse('.').success).toBe(false)
    expect(HostIdSchema.safeParse('..').success).toBe(false)
  })

  it('accepts a UUID host id, a hyphenated id, and "..." (not a dot-segment)', () => {
    expect(HostIdSchema.safeParse('550e8400-e29b-41d4-a716-446655440000').success).toBe(true)
    expect(HostIdSchema.safeParse('host-42').success).toBe(true)
    expect(HostIdSchema.safeParse('...').success).toBe(true)
  })
})
