import { beforeEach, describe, expect, it, vi } from 'vitest'
import { Hono } from 'hono'
import { generate, generateSecret } from 'otplib'
import type { AppEnv } from '../src/types'

const mockFindAdminById = vi.fn()
const mockSetAdminTotpSecret = vi.fn()
const mockRecordFailure = vi.fn()
const mockClearFailures = vi.fn()
const mockVerifySignedToken = vi.fn()
const mockSignAdminSession = vi.fn()

vi.mock('@zync/db/queries', () => ({
  createDb: vi.fn(() => ({})),
  findAdminById: (...args: unknown[]) => mockFindAdminById(...args),
  setAdminTotpSecret: (...args: unknown[]) => mockSetAdminTotpSecret(...args),
}))

vi.mock('@zync/auth', async (importOriginal) => {
  const actual = await importOriginal<typeof import('@zync/auth')>()
  return {
    ...actual,
    decryptSecret: vi.fn(async (secret: string) => secret),
    encryptSecret: vi.fn(async (secret: string) => secret),
    signAdminSession: (...args: unknown[]) => mockSignAdminSession(...args),
    verifySignedToken: (...args: unknown[]) => mockVerifySignedToken(...args),
  }
})

vi.mock('../src/lib/login-throttle', () => ({
  clearFailures: (...args: unknown[]) => mockClearFailures(...args),
  isLockedOut: vi.fn(async () => false),
  recordFailure: (...args: unknown[]) => mockRecordFailure(...args),
}))

import { adminTotpRoute } from '../src/routes/admin/auth/totp'

const admin = { id: 'admin-1', email: 'admin@example.test', totpSecret: null }
const kv = {
  delete: vi.fn(async () => undefined),
  get: vi.fn(),
  put: vi.fn(async () => undefined),
}

const env = {
  ADMIN_ENCRYPTION_KEY: 'test-encryption-key',
  JWT_SECRET: 'test-jwt-secret',
  KV: kv,
  RATE_LIMITER_AUTH: { limit: vi.fn(async () => ({ success: true })) },
} as unknown as AppEnv['Bindings']

function app() {
  const instance = new Hono<AppEnv>()
  instance.route('/api/admin/auth', adminTotpRoute)
  return instance
}

function request(path: string, token: string, code: string) {
  return app().request(path, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      Origin: 'https://admin.zync.is',
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ code }),
  }, env)
}

describe('admin TOTP verification', () => {
  beforeEach(() => {
    vi.clearAllMocks()
    mockFindAdminById.mockResolvedValue(admin)
    mockSignAdminSession.mockResolvedValue('admin-session')
    mockVerifySignedToken.mockImplementation(async (token: string) => ({
      purpose: token === 'setup-token' ? 'admin_totp_setup' : 'admin_totp_verify',
      sub: admin.id,
    }))
  })

  it('enrolls a valid current code and persists the secret', async () => {
    const secret = generateSecret()
    const code = await generate({ secret })
    kv.get.mockResolvedValue(secret)

    const response = await request('/api/admin/auth/totp/setup/verify', 'setup-token', code)

    expect(response.status).toBe(200)
    expect(mockSetAdminTotpSecret).toHaveBeenCalledWith({}, admin.id, secret)
    expect(kv.delete).toHaveBeenCalledWith(`admin_totp_setup:${admin.id}`)
    expect(response.headers.get('set-cookie')).toContain('zync_admin=admin-session')
  })

  it('rejects an invalid six-digit code without issuing a session or changing failure state', async () => {
    const secret = generateSecret()
    kv.get.mockResolvedValue(secret)
    mockFindAdminById.mockResolvedValue({ ...admin, totpSecret: secret })

    const response = await request('/api/admin/auth/totp/verify', 'verify-token', '000000')

    expect(response.status).toBe(401)
    expect(mockSetAdminTotpSecret).not.toHaveBeenCalled()
    expect(mockClearFailures).not.toHaveBeenCalled()
    expect(mockRecordFailure).toHaveBeenCalledWith(env, `admin_totp:${admin.id}`)
    expect(response.headers.get('set-cookie')).toBeNull()
  })
})
