import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { NextFunction, Request, Response } from 'express'
import { generateCompanyApiKey } from '../security/apiKeys'

vi.mock('../lib/prisma', () => ({
  prisma: {
    companyApiKey: {
      findUnique: vi.fn(),
      update: vi.fn(),
    },
  },
}))

import { prisma } from '../lib/prisma'
import { requireApiKey } from './requireApiKey'

function createResponseStub() {
  const res = {
    status: vi.fn(),
    json: vi.fn(),
  }

  res.status.mockReturnValue(res)
  res.json.mockReturnValue(res)

  return res as unknown as Response & typeof res
}

describe('requireApiKey middleware', () => {
  beforeEach(() => {
    vi.clearAllMocks()
  })

  it('rejects requests with no x-api-key header', async () => {
    const req = { headers: {} } as Request
    const res = createResponseStub()
    const next = vi.fn() as NextFunction

    await requireApiKey(req, res, next)

    expect(res.status).toHaveBeenCalledWith(401)
    expect(res.json).toHaveBeenCalledWith({
      error: 'missing_api_key',
      message: 'API key required in x-api-key header',
      statusCode: 401,
    })
    expect((prisma as any).companyApiKey.findUnique).not.toHaveBeenCalled()
    expect(next).not.toHaveBeenCalled()
  })

  it('rejects malformed API keys before database lookup', async () => {
    const req = { headers: { 'x-api-key': 'bad-key' } } as unknown as Request
    const res = createResponseStub()
    const next = vi.fn() as NextFunction

    await requireApiKey(req, res, next)

    expect((prisma as any).companyApiKey.findUnique).not.toHaveBeenCalled()
    expect(res.status).toHaveBeenCalledWith(401)
    expect(res.json).toHaveBeenCalledWith({
      error: 'invalid_api_key',
      message: 'Invalid API key',
      statusCode: 401,
    })
    expect(next).not.toHaveBeenCalled()
  })

  it('rejects unknown API key prefixes', async () => {
    const generated = generateCompanyApiKey()
    vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue(null)
    const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request
    const res = createResponseStub()
    const next = vi.fn() as NextFunction

    await requireApiKey(req, res, next)

    expect((prisma as any).companyApiKey.findUnique).toHaveBeenCalledWith({
      where: { prefix: generated.prefix },
      include: { company: true },
    })
    expect(res.status).toHaveBeenCalledWith(401)
    expect(res.json).toHaveBeenCalledWith({
      error: 'invalid_api_key',
      message: 'Invalid API key',
      statusCode: 401,
    })
    expect(next).not.toHaveBeenCalled()
  })

  it('rejects revoked API keys', async () => {
    const generated = generateCompanyApiKey()
    vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({
      id: 'key_1',
      companyId: 'company_1',
      prefix: generated.prefix,
      keyHash: generated.keyHash,
      revokedAt: new Date('2026-06-01T00:00:00.000Z'),
      company: { id: 'company_1', name: 'Atlas Cars' },
    })
    const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request
    const res = createResponseStub()
    const next = vi.fn() as NextFunction

    await requireApiKey(req, res, next)

    expect(res.status).toHaveBeenCalledWith(401)
    expect(next).not.toHaveBeenCalled()
  })

  it('rejects API keys whose secret does not match the stored hash', async () => {
    const generated = generateCompanyApiKey()
    const other = generateCompanyApiKey()
    vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({
      id: 'key_1',
      companyId: 'company_1',
      prefix: generated.prefix,
      keyHash: other.keyHash,
      revokedAt: null,
      company: { id: 'company_1', name: 'Atlas Cars' },
    })
    const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request
    const res = createResponseStub()
    const next = vi.fn() as NextFunction

    await requireApiKey(req, res, next)

    expect(res.status).toHaveBeenCalledWith(401)
    expect(next).not.toHaveBeenCalled()
  })

  it('attaches company context, updates lastUsedAt, and calls next for valid API keys', async () => {
    const generated = generateCompanyApiKey()
    const company = { id: 'company_1', name: 'Atlas Cars' }
    vi.mocked((prisma as any).companyApiKey.findUnique).mockResolvedValue({
      id: 'key_1',
      companyId: company.id,
      prefix: generated.prefix,
      keyHash: generated.keyHash,
      revokedAt: null,
      company,
    })
    vi.mocked((prisma as any).companyApiKey.update).mockResolvedValue({})
    const req = { headers: { 'x-api-key': generated.rawKey } } as unknown as Request
    const res = createResponseStub()
    const next = vi.fn() as NextFunction

    await requireApiKey(req, res, next)

    expect((prisma as any).companyApiKey.update).toHaveBeenCalledWith({
      where: { id: 'key_1' },
      data: { lastUsedAt: expect.any(Date) },
    })
    expect(req.company).toEqual(company)
    expect(req.companyId).toBe('company_1')
    expect(next).toHaveBeenCalledTimes(1)
    expect(res.status).not.toHaveBeenCalled()
    expect(res.json).not.toHaveBeenCalled()
  })
})
