import { describe, it, expect, beforeEach, vi } from 'vitest'
import { prismaMock, resetPrismaMock } from '../../../mocks/prisma'

// Mock the email service
vi.mock('@/lib/services/email-service', () => ({
  emailService: {
    isConfigured: vi.fn().mockResolvedValue(false),
    sendEmail: vi.fn().mockResolvedValue({ success: true }),
  },
}))

import { securityVerificationService } from '@/lib/services/security-verification-service'

describe('SecurityVerificationService', () => {
  beforeEach(() => {
    resetPrismaMock()
    vi.clearAllMocks()
    // Suppress console.log from email fallback logging
    vi.spyOn(console, 'log').mockImplementation(() => {})
  })

  describe('requestVerificationCode', () => {
    const mockClient = {
      id: 'client-123',
      name: 'Test Client',
      contactEmail: 'admin@testclient.com',
    }

    const mockServer = {
      id: 'server-456',
      nickname: 'Production Server',
      netcupServerId: 'ns-789',
    }

    beforeEach(() => {
      prismaMock.enterpriseClient.findUnique.mockResolvedValue(mockClient as any)
      prismaMock.enterpriseServer.findFirst.mockResolvedValue(mockServer as any)
      prismaMock.securityVerificationCode.updateMany.mockResolvedValue({ count: 0 } as any)
      prismaMock.securityVerificationCode.create.mockResolvedValue({} as any)
    })

    it('should throw when client not found', async () => {
      prismaMock.enterpriseClient.findUnique.mockResolvedValue(null)

      await expect(
        securityVerificationService.requestVerificationCode('bad-id', 'server-456', 'WIPE')
      ).rejects.toThrow('Client not found')
    })

    it('should throw when server not found', async () => {
      prismaMock.enterpriseServer.findFirst.mockResolvedValue(null)

      await expect(
        securityVerificationService.requestVerificationCode('client-123', 'bad-id', 'WIPE')
      ).rejects.toThrow('Server not found or does not belong to this client')
    })

    it('should invalidate existing unused codes before creating new one', async () => {
      const result = await securityVerificationService.requestVerificationCode(
        'client-123',
        'server-456',
        'WIPE'
      )

      expect(prismaMock.securityVerificationCode.updateMany).toHaveBeenCalledWith({
        where: {
          clientId: 'client-123',
          targetServerId: 'server-456',
          action: 'WIPE',
          usedAt: null,
        },
        data: {
          usedAt: expect.any(Date),
        },
      })
    })

    it('should create a new verification code', async () => {
      await securityVerificationService.requestVerificationCode(
        'client-123',
        'server-456',
        'REINSTALL'
      )

      expect(prismaMock.securityVerificationCode.create).toHaveBeenCalledWith({
        data: expect.objectContaining({
          clientId: 'client-123',
          action: 'REINSTALL',
          targetServerId: 'server-456',
          code: expect.any(String),
          expiresAt: expect.any(Date),
        }),
      })
    })

    it('should generate 8-digit code', async () => {
      await securityVerificationService.requestVerificationCode(
        'client-123',
        'server-456',
        'WIPE'
      )

      const createCall = prismaMock.securityVerificationCode.create.mock.calls[0][0]
      const code = createCall.data.code as string

      expect(code).toHaveLength(8)
      expect(code).toMatch(/^\d{8}$/)
    })

    it('should set expiry 15 minutes in the future', async () => {
      const before = Date.now()

      await securityVerificationService.requestVerificationCode(
        'client-123',
        'server-456',
        'WIPE'
      )

      const createCall = prismaMock.securityVerificationCode.create.mock.calls[0][0]
      const expiresAt = createCall.data.expiresAt as Date

      const after = Date.now()
      const fifteenMinMs = 15 * 60 * 1000

      // expiresAt should be approximately 15 minutes from now
      expect(expiresAt.getTime()).toBeGreaterThanOrEqual(before + fifteenMinMs - 1000)
      expect(expiresAt.getTime()).toBeLessThanOrEqual(after + fifteenMinMs + 1000)
    })

    it('should return masked email and expiry', async () => {
      const result = await securityVerificationService.requestVerificationCode(
        'client-123',
        'server-456',
        'WIPE'
      )

      expect(result.expiresAt).toBeInstanceOf(Date)
      // Email should be masked (a***n@testclient.com pattern)
      expect(result.email).toContain('@testclient.com')
      expect(result.email).toContain('*')
    })
  })

  describe('verifyCode', () => {
    it('should return invalid when no active code exists', async () => {
      prismaMock.securityVerificationCode.findFirst.mockResolvedValue(null)

      const result = await securityVerificationService.verifyCode('client-123', '12345678')

      expect(result.valid).toBe(false)
      expect(result.errorMessage).toBe('Invalid or expired verification code')
    })

    it('should return invalid when max attempts exceeded', async () => {
      prismaMock.securityVerificationCode.findFirst.mockResolvedValue({
        id: 'code-123',
        code: '12345678',
        attempts: 5,
        clientId: 'client-123',
        action: 'WIPE',
        targetServerId: 'server-456',
      } as any)
      prismaMock.securityVerificationCode.update.mockResolvedValue({} as any)

      const result = await securityVerificationService.verifyCode('client-123', '12345678')

      expect(result.valid).toBe(false)
      expect(result.errorMessage).toContain('Too many failed attempts')

      // Should invalidate the code
      expect(prismaMock.securityVerificationCode.update).toHaveBeenCalledWith({
        where: { id: 'code-123' },
        data: { usedAt: expect.any(Date) },
      })
    })

    it('should increment attempt counter on wrong code', async () => {
      prismaMock.securityVerificationCode.findFirst.mockResolvedValue({
        id: 'code-123',
        code: '12345678',
        attempts: 2,
        clientId: 'client-123',
        action: 'WIPE',
        targetServerId: 'server-456',
      } as any)
      prismaMock.securityVerificationCode.update.mockResolvedValue({} as any)

      const result = await securityVerificationService.verifyCode('client-123', '00000000')

      expect(result.valid).toBe(false)
      expect(result.errorMessage).toContain('attempt(s) remaining')

      expect(prismaMock.securityVerificationCode.update).toHaveBeenCalledWith({
        where: { id: 'code-123' },
        data: { attempts: { increment: 1 } },
      })
    })

    it('should show remaining attempts in error message', async () => {
      prismaMock.securityVerificationCode.findFirst.mockResolvedValue({
        id: 'code-123',
        code: '12345678',
        attempts: 3,
        clientId: 'client-123',
        action: 'WIPE',
        targetServerId: 'server-456',
      } as any)
      prismaMock.securityVerificationCode.update.mockResolvedValue({} as any)

      const result = await securityVerificationService.verifyCode('client-123', '00000000')

      // 5 max - 3 current - 1 this attempt = 1 remaining
      expect(result.errorMessage).toContain('1 attempt(s) remaining')
    })

    it('should show too many attempts when at last attempt', async () => {
      prismaMock.securityVerificationCode.findFirst.mockResolvedValue({
        id: 'code-123',
        code: '12345678',
        attempts: 4,
        clientId: 'client-123',
        action: 'WIPE',
        targetServerId: 'server-456',
      } as any)
      prismaMock.securityVerificationCode.update.mockResolvedValue({} as any)

      const result = await securityVerificationService.verifyCode('client-123', '00000000')

      expect(result.errorMessage).toContain('Too many failed attempts')
    })

    it('should return valid with action and serverId on correct code', async () => {
      prismaMock.securityVerificationCode.findFirst.mockResolvedValue({
        id: 'code-123',
        code: '12345678',
        attempts: 0,
        clientId: 'client-123',
        action: 'WIPE',
        targetServerId: 'server-456',
      } as any)
      prismaMock.securityVerificationCode.update.mockResolvedValue({} as any)

      const result = await securityVerificationService.verifyCode('client-123', '12345678')

      expect(result.valid).toBe(true)
      expect(result.action).toBe('WIPE')
      expect(result.serverId).toBe('server-456')
    })

    it('should mark code as used after successful verification', async () => {
      prismaMock.securityVerificationCode.findFirst.mockResolvedValue({
        id: 'code-123',
        code: '12345678',
        attempts: 0,
        clientId: 'client-123',
        action: 'REINSTALL',
        targetServerId: 'server-456',
      } as any)
      prismaMock.securityVerificationCode.update.mockResolvedValue({} as any)

      await securityVerificationService.verifyCode('client-123', '12345678')

      expect(prismaMock.securityVerificationCode.update).toHaveBeenCalledWith({
        where: { id: 'code-123' },
        data: { usedAt: expect.any(Date) },
      })
    })
  })

  describe('cleanupExpiredCodes', () => {
    it('should delete expired and used codes older than 24h', async () => {
      prismaMock.securityVerificationCode.deleteMany.mockResolvedValue({ count: 5 } as any)

      const result = await securityVerificationService.cleanupExpiredCodes()

      expect(result).toBe(5)
      expect(prismaMock.securityVerificationCode.deleteMany).toHaveBeenCalledWith({
        where: {
          OR: [
            { expiresAt: { lt: expect.any(Date) } },
            { usedAt: { not: null } },
          ],
          createdAt: { lt: expect.any(Date) },
        },
      })
    })

    it('should return 0 when no codes to clean up', async () => {
      prismaMock.securityVerificationCode.deleteMany.mockResolvedValue({ count: 0 } as any)

      const result = await securityVerificationService.cleanupExpiredCodes()

      expect(result).toBe(0)
    })
  })
})
