const { describe, test, expect, beforeEach, afterEach } = require('bun:test')
const crypto = require('crypto')
const bcrypt = require('bcrypt')

const PASSWORD_SALT_ROUNDS = 12
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

describe('Account Management', () => {
  describe('User Registration', () => {
    function validateEmail(email) {
      return EMAIL_REGEX.test(email)
    }

    function validatePassword(password) {
      const errors = []
      if (!password || password.length < 12) errors.push('Minimum 12 characters')
      if (!/[A-Z]/.test(password)) errors.push('Uppercase letter required')
      if (!/[a-z]/.test(password)) errors.push('Lowercase letter required')
      if (!/[0-9]/.test(password)) errors.push('Number required')
      if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) errors.push('Special character required')
      return { valid: errors.length === 0, errors }
    }

    async function hashPassword(password) {
      return bcrypt.hash(password, PASSWORD_SALT_ROUNDS)
    }

    test('validates email format', () => {
      expect(validateEmail('test@example.com')).toBe(true)
      expect(validateEmail('user.name@domain.co.uk')).toBe(true)
      expect(validateEmail('invalid')).toBe(false)
      expect(validateEmail('@nodomain.com')).toBe(false)
    })

    test('validates strong password', () => {
      const result = validatePassword('StrongP@ss123')
      expect(result.valid).toBe(true)
    })

    test('rejects weak passwords', () => {
      expect(validatePassword('short').errors.length).toBeGreaterThan(0)
      expect(validatePassword('alllowercase123!').errors).toContain('Uppercase letter required')
      expect(validatePassword('ALLUPPERCASE123!').errors).toContain('Lowercase letter required')
      expect(validatePassword('NoNumbers!').errors).toContain('Number required')
      expect(validatePassword('NoSpecial123').errors).toContain('Special character required')
    })

    test('requires 12 character minimum', () => {
      const result = validatePassword('Short1!')
      expect(result.errors).toContain('Minimum 12 characters')
    })

    test('hashes password securely', async () => {
      const password = 'TestPassword123!'
      const hash = await hashPassword(password)

      expect(hash).toBeDefined()
      expect(hash).not.toBe(password)
      expect(await bcrypt.compare(password, hash)).toBe(true)
    })

    test('generates unique hashes', async () => {
      const password = 'TestPassword123!'
      const hash1 = await hashPassword(password)
      const hash2 = await hashPassword(password)

      expect(hash1).not.toBe(hash2)
    })
  })

  describe('Email Change', () => {
    function initiateEmailChange(user, newEmail) {
      if (!validateEmail(newEmail)) {
        return { error: 'Invalid email format' }
      }
      if (newEmail.toLowerCase() === user.email.toLowerCase()) {
        return { error: 'New email must be different' }
      }

      const token = crypto.randomBytes(32).toString('hex')
      return {
        success: true,
        pendingEmail: newEmail,
        verificationToken: token,
        expiresAt: Date.now() + 24 * 60 * 60 * 1000,
      }
    }

    function confirmEmailChange(user, token) {
      if (user.verificationToken !== token) {
        return { error: 'Invalid token' }
      }
      if (Date.now() > user.expiresAt) {
        return { error: 'Token expired' }
      }

      return {
        success: true,
        newEmail: user.pendingEmail,
      }
    }

    function validateEmail(email) {
      return EMAIL_REGEX.test(email)
    }

    test('initiates email change', () => {
      const user = { email: 'old@example.com' }
      const result = initiateEmailChange(user, 'new@example.com')

      expect(result.success).toBe(true)
      expect(result.pendingEmail).toBe('new@example.com')
      expect(result.verificationToken).toBeDefined()
    })

    test('rejects invalid email', () => {
      const user = { email: 'old@example.com' }
      const result = initiateEmailChange(user, 'invalid-email')

      expect(result.error).toBe('Invalid email format')
    })

    test('rejects same email', () => {
      const user = { email: 'same@example.com' }
      const result = initiateEmailChange(user, 'same@example.com')

      expect(result.error).toBe('New email must be different')
    })

    test('confirms email change with valid token', () => {
      const user = {
        email: 'old@example.com',
        pendingEmail: 'new@example.com',
        verificationToken: 'valid-token',
        expiresAt: Date.now() + 3600000,
      }
      const result = confirmEmailChange(user, 'valid-token')

      expect(result.success).toBe(true)
      expect(result.newEmail).toBe('new@example.com')
    })

    test('rejects invalid token', () => {
      const user = {
        email: 'old@example.com',
        verificationToken: 'valid-token',
        expiresAt: Date.now() + 3600000,
      }
      const result = confirmEmailChange(user, 'wrong-token')

      expect(result.error).toBe('Invalid token')
    })

    test('rejects expired token', () => {
      const user = {
        email: 'old@example.com',
        verificationToken: 'valid-token',
        expiresAt: Date.now() - 1000,
      }
      const result = confirmEmailChange(user, 'valid-token')

      expect(result.error).toBe('Token expired')
    })
  })

  describe('Password Change', () => {
    async function changePassword(user, currentPassword, newPassword) {
      const validCurrent = await bcrypt.compare(currentPassword, user.passwordHash)
      if (!validCurrent) {
        return { error: 'Current password is incorrect' }
      }

      const validation = validatePassword(newPassword)
      if (!validation.valid) {
        return { error: validation.errors.join(', ') }
      }

      const newHash = await bcrypt.hash(newPassword, PASSWORD_SALT_ROUNDS)
      return {
        success: true,
        newPasswordHash: newHash,
      }
    }

    function validatePassword(password) {
      const errors = []
      if (!password || password.length < 12) errors.push('Minimum 12 characters')
      if (!/[A-Z]/.test(password)) errors.push('Uppercase letter required')
      if (!/[a-z]/.test(password)) errors.push('Lowercase letter required')
      if (!/[0-9]/.test(password)) errors.push('Number required')
      if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) errors.push('Special character required')
      return { valid: errors.length === 0, errors }
    }

    test('changes password with valid current password', async () => {
      const passwordHash = await bcrypt.hash('OldPassword123!', PASSWORD_SALT_ROUNDS)
      const user = { passwordHash }

      const result = await changePassword(user, 'OldPassword123!', 'NewPassword456!')

      expect(result.success).toBe(true)
      expect(await bcrypt.compare('NewPassword456!', result.newPasswordHash)).toBe(true)
    })

    test('rejects wrong current password', async () => {
      const passwordHash = await bcrypt.hash('CorrectPassword123!', PASSWORD_SALT_ROUNDS)
      const user = { passwordHash }

      const result = await changePassword(user, 'WrongPassword123!', 'NewPassword456!')

      expect(result.error).toBe('Current password is incorrect')
    })

    test('validates new password strength', async () => {
      const passwordHash = await bcrypt.hash('OldPassword123!', PASSWORD_SALT_ROUNDS)
      const user = { passwordHash }

      const result = await changePassword(user, 'OldPassword123!', 'weak')

      expect(result.error).toBeDefined()
    })
  })

  describe('Two-Factor Authentication', () => {
    function generateSecret() {
      return crypto.randomBytes(20).toString('base64')
    }

    function generateBackupCodes(count = 10) {
      const codes = []
      for (let i = 0; i < count; i++) {
        codes.push(crypto.randomBytes(4).toString('hex').toUpperCase())
      }
      return codes
    }

    function verifyTotp(secret, code) {
      return code.length === 6 && /^\d+$/.test(code)
    }

    function enable2FA(user, secret, backupCodes) {
      return {
        ...user,
        twoFactorSecret: secret,
        twoFactorBackupCodes: backupCodes,
        twoFactorEnabled: true,
        twoFactorEnabledAt: new Date().toISOString(),
      }
    }

    function disable2FA(user) {
      const updated = { ...user }
      delete updated.twoFactorSecret
      delete updated.twoFactorBackupCodes
      updated.twoFactorEnabled = false
      return updated
    }

    function useBackupCode(user, code) {
      if (!user.twoFactorBackupCodes?.includes(code)) {
        return { success: false, error: 'Invalid backup code' }
      }
      const updated = { ...user }
      updated.twoFactorBackupCodes = updated.twoFactorBackupCodes.filter(c => c !== code)
      return { success: true, remainingCodes: updated.twoFactorBackupCodes.length }
    }

    test('generates 2FA secret', () => {
      const secret = generateSecret()
      expect(secret).toBeDefined()
      expect(secret.length).toBeGreaterThan(0)
    })

    test('generates backup codes', () => {
      const codes = generateBackupCodes(10)
      expect(codes.length).toBe(10)
      codes.forEach(code => {
        expect(code.length).toBe(8)
        expect(/^[A-F0-9]+$/.test(code)).toBe(true)
      })
    })

    test('enables 2FA', () => {
      const user = { id: 'user-1' }
      const secret = generateSecret()
      const backupCodes = generateBackupCodes()

      const updated = enable2FA(user, secret, backupCodes)

      expect(updated.twoFactorEnabled).toBe(true)
      expect(updated.twoFactorSecret).toBe(secret)
      expect(updated.twoFactorBackupCodes.length).toBe(10)
    })

    test('disables 2FA', () => {
      const user = {
        id: 'user-1',
        twoFactorEnabled: true,
        twoFactorSecret: 'secret',
        twoFactorBackupCodes: ['CODE1', 'CODE2'],
      }

      const updated = disable2FA(user)

      expect(updated.twoFactorEnabled).toBe(false)
      expect(updated.twoFactorSecret).toBeUndefined()
      expect(updated.twoFactorBackupCodes).toBeUndefined()
    })

    test('verifies TOTP code format', () => {
      expect(verifyTotp('secret', '123456')).toBe(true)
      expect(verifyTotp('secret', '12345')).toBe(false)
      expect(verifyTotp('secret', 'abcdef')).toBe(false)
    })

    test('uses backup code', () => {
      const user = {
        twoFactorBackupCodes: ['CODE1234', 'CODE5678'],
      }

      const result = useBackupCode(user, 'CODE1234')
      expect(result.success).toBe(true)
      expect(result.remainingCodes).toBe(1)
    })

    test('rejects invalid backup code', () => {
      const user = {
        twoFactorBackupCodes: ['CODE1234'],
      }

      const result = useBackupCode(user, 'INVALID')
      expect(result.success).toBe(false)
    })
  })

  describe('Session Management', () => {
    const sessions = new Map()

    function createSession(userId, deviceInfo) {
      const token = crypto.randomBytes(32).toString('hex')
      const session = {
        id: crypto.randomUUID(),
        userId,
        token,
        device: deviceInfo.device || 'Unknown',
        ip: deviceInfo.ip || '0.0.0.0',
        userAgent: deviceInfo.userAgent || '',
        createdAt: Date.now(),
        lastAccessedAt: Date.now(),
        expiresAt: Date.now() + 30 * 24 * 60 * 60 * 1000,
      }
      sessions.set(token, session)
      return session
    }

    function getSessions(userId) {
      return Array.from(sessions.values())
        .filter(s => s.userId === userId)
        .map(s => ({
          id: s.id,
          device: s.device,
          ip: s.ip,
          lastAccessedAt: s.lastAccessedAt,
          createdAt: s.createdAt,
        }))
    }

    function revokeSession(token) {
      return sessions.delete(token)
    }

    function revokeAllSessions(userId, exceptToken) {
      let revoked = 0
      for (const [token, session] of sessions.entries()) {
        if (session.userId === userId && token !== exceptToken) {
          sessions.delete(token)
          revoked++
        }
      }
      return revoked
    }

    beforeEach(() => {
      sessions.clear()
    })

    test('creates session', () => {
      const session = createSession('user-1', { device: 'Chrome on Windows', ip: '192.168.1.1' })

      expect(session.id).toBeDefined()
      expect(session.token).toBeDefined()
      expect(session.device).toBe('Chrome on Windows')
    })

    test('lists user sessions', () => {
      createSession('user-1', { device: 'Device 1' })
      createSession('user-1', { device: 'Device 2' })
      createSession('user-2', { device: 'Device 3' })

      const userSessions = getSessions('user-1')
      expect(userSessions.length).toBe(2)
    })

    test('excludes sensitive data from session list', () => {
      createSession('user-1', { device: 'Test' })
      const userSessions = getSessions('user-1')

      expect(userSessions[0].token).toBeUndefined()
    })

    test('revokes session', () => {
      const session = createSession('user-1', {})
      const result = revokeSession(session.token)

      expect(result).toBe(true)
      expect(sessions.size).toBe(0)
    })

    test('revokes all sessions except current', () => {
      const s1 = createSession('user-1', {})
      createSession('user-1', {})
      createSession('user-1', {})

      const revoked = revokeAllSessions('user-1', s1.token)
      expect(revoked).toBe(2)
      expect(sessions.size).toBe(1)
    })
  })

  describe('Account Deletion', () => {
    function validateDeletionRequest(user, password) {
      if (!user) return { error: 'User not found' }
      if (!user.passwordHash) return { error: 'Cannot delete account' }
      return { valid: true }
    }

    async function confirmDeletion(user, password) {
      const validPassword = await bcrypt.compare(password, user.passwordHash)
      if (!validPassword) return { error: 'Password is incorrect' }
      return { success: true, deletedAt: new Date().toISOString() }
    }

    function scheduleDeletion(userId, delayDays = 30) {
      return {
        scheduledDeletion: true,
        deletionDate: new Date(Date.now() + delayDays * 24 * 60 * 60 * 1000).toISOString(),
        gracePeriodDays: delayDays,
      }
    }

    test('validates deletion request', () => {
      const user = { id: 'user-1', passwordHash: 'hash' }
      const result = validateDeletionRequest(user, 'password')

      expect(result.valid).toBe(true)
    })

    test('confirms deletion with correct password', async () => {
      const passwordHash = await bcrypt.hash('Password123!', PASSWORD_SALT_ROUNDS)
      const user = { passwordHash }

      const result = await confirmDeletion(user, 'Password123!')
      expect(result.success).toBe(true)
    })

    test('rejects deletion with wrong password', async () => {
      const passwordHash = await bcrypt.hash('CorrectPassword!', PASSWORD_SALT_ROUNDS)
      const user = { passwordHash }

      const result = await confirmDeletion(user, 'WrongPassword!')
      expect(result.error).toBe('Password is incorrect')
    })

    test('schedules deletion with grace period', () => {
      const result = scheduleDeletion('user-1', 30)

      expect(result.scheduledDeletion).toBe(true)
      expect(result.gracePeriodDays).toBe(30)
    })
  })

  describe('Account Security Logging', () => {
    const securityLog = []

    function logSecurityEvent(userId, event, details) {
      securityLog.push({
        id: crypto.randomUUID(),
        userId,
        event,
        details,
        ip: details.ip || '0.0.0.0',
        userAgent: details.userAgent || '',
        timestamp: new Date().toISOString(),
      })
    }

    function getSecurityEvents(userId, limit = 50) {
      return securityLog
        .filter(e => e.userId === userId)
        .slice(-limit)
    }

    beforeEach(() => {
      securityLog.length = 0
    })

    test('logs security event', () => {
      logSecurityEvent('user-1', 'login', { ip: '192.168.1.1', success: true })

      expect(securityLog.length).toBe(1)
      expect(securityLog[0].event).toBe('login')
    })

    test('logs multiple event types', () => {
      logSecurityEvent('user-1', 'login', {})
      logSecurityEvent('user-1', 'password_change', {})
      logSecurityEvent('user-1', '2fa_enabled', {})

      const events = getSecurityEvents('user-1')
      expect(events.length).toBe(3)
    })

    test('filters by user', () => {
      logSecurityEvent('user-1', 'login', {})
      logSecurityEvent('user-2', 'login', {})

      const events = getSecurityEvents('user-1')
      expect(events.length).toBe(1)
    })

    test('limits returned events', () => {
      for (let i = 0; i < 100; i++) {
        logSecurityEvent('user-1', 'login', {})
      }

      const events = getSecurityEvents('user-1', 10)
      expect(events.length).toBe(10)
    })
  })
})
