import { describe, it, expect, vi, beforeEach } from 'vitest'

const { cookieJar, mockGetIronSession } = vi.hoisted(() => ({
  cookieJar: { set: vi.fn(), get: vi.fn(), delete: vi.fn() },
  mockGetIronSession: vi.fn().mockResolvedValue({ isDemo: false }),
}))

vi.mock('iron-session', () => ({
  getIronSession: mockGetIronSession,
}))

vi.mock('@/lib/prisma', () => ({
  prisma: {
    loginPairing: {
      create: vi.fn(),
    },
  },
}))

vi.mock('next/headers', () => ({
  cookies: vi.fn().mockResolvedValue(cookieJar),
}))

import { prisma } from '@/lib/prisma'
import { POST } from '@/app/api/auth/pair/start/route'

const mockPrisma = prisma as unknown as {
  loginPairing: { create: ReturnType<typeof vi.fn> }
}

function makePost(opts: { ip?: string; ua?: string } = {}): Request {
  const headers = new Headers()
  if (opts.ip !== undefined) headers.set('x-forwarded-for', opts.ip)
  if (opts.ua !== undefined) headers.set('user-agent', opts.ua)
  return new Request('http://localhost:3000/api/auth/pair/start', {
    method: 'POST',
    headers,
  })
}

beforeEach(() => {
  vi.clearAllMocks()
  mockPrisma.loginPairing.create.mockResolvedValue({
    id: 'pair-1',
    expires_at: new Date('2026-04-27T20:30:00Z'),
  })
})

describe('POST /api/auth/pair/start', () => {
  it('200 met body { pairingId, mobileSecret, expiresAt, qrUrl met fragment }', async () => {
    const res = await POST(makePost({ ip: '198.51.100.7', ua: 'TestUA/1.0' }))
    expect(res.status).toBe(200)
    const body = await res.json()
    expect(body.pairingId).toBe('pair-1')
    expect(body.mobileSecret).toMatch(/^[A-Za-z0-9_-]{43}$/)
    expect(body.qrUrl).toBe(
      `http://localhost:3000/m/pair#id=pair-1&s=${body.mobileSecret}`,
    )
    expect(body.expiresAt).toBe('2026-04-27T20:30:00.000Z')
  })

  it('slaat alleen sha256-hashes op — geen plaintext mobileSecret of desktopToken', async () => {
    const res = await POST(makePost({ ip: '198.51.100.8' }))
    const body = await res.json()
    const arg = mockPrisma.loginPairing.create.mock.calls[0][0].data
    expect(arg.secret_hash).toMatch(/^[a-f0-9]{64}$/)
    expect(arg.desktop_token_hash).toMatch(/^[a-f0-9]{64}$/)
    expect(arg.secret_hash).not.toBe(body.mobileSecret)
    expect(arg.status).toBe('pending')
    // expires_at ~5 min in toekomst
    const dt = new Date(arg.expires_at).getTime() - Date.now()
    expect(dt).toBeGreaterThan(295_000)
    expect(dt).toBeLessThan(305_000)
  })

  it('zet HttpOnly Path-scoped s4m_pair cookie met Max-Age 120', async () => {
    await POST(makePost({ ip: '198.51.100.9' }))
    expect(cookieJar.set).toHaveBeenCalledTimes(1)
    const [name, value, opts] = cookieJar.set.mock.calls[0]
    expect(name).toBe('s4m_pair')
    expect(value).toMatch(/^[A-Za-z0-9_-]{43}$/) // desktopToken
    expect(opts).toMatchObject({
      httpOnly: true,
      sameSite: 'lax',
      path: '/api/auth/pair',
      maxAge: 300,
    })
  })

  it('slaat user-agent en IP op (afgekapt)', async () => {
    const longUa = 'A'.repeat(500)
    await POST(makePost({ ip: '198.51.100.10', ua: longUa }))
    const arg = mockPrisma.loginPairing.create.mock.calls[0][0].data
    expect(arg.desktop_ua).toBe('A'.repeat(255))
    expect(arg.desktop_ip).toBe('198.51.100.10')
  })

  it('desktop_ip = null als x-forwarded-for ontbreekt', async () => {
    await POST(makePost({}))
    const arg = mockPrisma.loginPairing.create.mock.calls[0][0].data
    expect(arg.desktop_ip).toBeNull()
  })

  it('11e POST binnen window levert 429', async () => {
    const ip = '198.51.100.99' // unieke IP zodat andere tests niet in de weg zitten
    for (let i = 0; i < 10; i++) {
      const ok = await POST(makePost({ ip }))
      expect(ok.status).toBe(200)
    }
    const blocked = await POST(makePost({ ip }))
    expect(blocked.status).toBe(429)
    const body = await blocked.json()
    expect(body.error).toMatch(/Te veel pogingen/i)
  })
})
