const { describe, test, expect, beforeEach } = require('bun:test')
const {
  initTokenManager,
  generateDeviceFingerprint,
  generateAccessToken,
  verifyAccessToken,
  generateRefreshToken,
  verifyRefreshToken,
  extractToken,
  createSecureCookie,
  parseCookies,
  getTokenTTL,
  isTokenManagerInitialized
} = require('./tokenManager')

describe('Token Manager', () => {
  const testSecret = 'test-jwt-secret-key-for-testing-purposes-only'

  beforeEach(() => {
    initTokenManager(testSecret)
  })

  describe('initTokenManager', () => {
    test('initializes with valid secret', () => {
      initTokenManager(testSecret)
      expect(isTokenManagerInitialized()).toBe(true)
    })

    test('throws error without secret', () => {
      expect(() => initTokenManager()).toThrow('JWT secret is required')
    })

    test('throws error with non-string secret', () => {
      expect(() => initTokenManager(123)).toThrow('JWT secret is required')
    })

    test('throws error with null secret', () => {
      expect(() => initTokenManager(null)).toThrow('JWT secret is required')
    })

    test('throws error with empty string secret', () => {
      expect(() => initTokenManager('')).toThrow('JWT secret is required')
    })
  })

  describe('generateDeviceFingerprint', () => {
    test('generates fingerprint from request headers', () => {
      const req = {
        headers: {
          'user-agent': 'Mozilla/5.0',
          'accept-language': 'en-US'
        },
        ip: '127.0.0.1',
        connection: { remoteAddress: '127.0.0.1' }
      }
      
      const fingerprint = generateDeviceFingerprint(req)
      
      expect(fingerprint).toBeDefined()
      expect(fingerprint.length).toBe(32)
      expect(/^[0-9a-f]+$/.test(fingerprint)).toBe(true)
    })

    test('produces same fingerprint for same request', () => {
      const req = {
        headers: {
          'user-agent': 'Mozilla/5.0',
          'accept-language': 'en-US'
        },
        ip: '127.0.0.1'
      }
      
      const fp1 = generateDeviceFingerprint(req)
      const fp2 = generateDeviceFingerprint(req)
      
      expect(fp1).toBe(fp2)
    })

    test('produces different fingerprint for different user agent', () => {
      const req1 = {
        headers: { 'user-agent': 'Mozilla/5.0' },
        ip: '127.0.0.1'
      }
      const req2 = {
        headers: { 'user-agent': 'Chrome/1.0' },
        ip: '127.0.0.1'
      }
      
      const fp1 = generateDeviceFingerprint(req1)
      const fp2 = generateDeviceFingerprint(req2)
      
      expect(fp1).not.toBe(fp2)
    })

    test('handles missing headers gracefully', () => {
      const req = { headers: {} }
      
      const fingerprint = generateDeviceFingerprint(req)
      
      expect(fingerprint).toBeDefined()
      expect(fingerprint.length).toBe(32)
    })

    test('handles x-forwarded-for header', () => {
      const req = {
        headers: {
          'user-agent': 'Mozilla/5.0',
          'x-forwarded-for': '192.168.1.1'
        }
      }
      
      const fingerprint = generateDeviceFingerprint(req)
      
      expect(fingerprint).toBeDefined()
      expect(fingerprint.length).toBe(32)
    })

    test('handles null/undefined values', () => {
      const req = {
        headers: {
          'user-agent': null,
          'accept-language': undefined
        }
      }
      
      const fingerprint = generateDeviceFingerprint(req)
      
      expect(fingerprint).toBeDefined()
    })
  })

  describe('generateAccessToken', () => {
    test('generates valid JWT token', () => {
      const payload = {
        userId: 'user-123',
        email: 'test@example.com',
        role: 'user',
        plan: 'hobby'
      }
      
      const token = generateAccessToken(payload)
      
      expect(token).toBeDefined()
      expect(token.split('.').length).toBe(3)
    })

    test('includes all payload fields', () => {
      const payload = {
        userId: 'user-123',
        email: 'test@example.com',
        role: 'admin',
        plan: 'enterprise'
      }
      
      const token = generateAccessToken(payload)
      const decoded = verifyAccessToken(token)
      
      expect(decoded.userId).toBe(payload.userId)
      expect(decoded.email).toBe(payload.email)
      expect(decoded.role).toBe(payload.role)
      expect(decoded.plan).toBe(payload.plan)
    })

    test('includes jti (JWT ID)', () => {
      const token = generateAccessToken({ userId: '123', email: 'test@test.com' })
      const decoded = verifyAccessToken(token)
      
      expect(decoded.jti).toBeDefined()
      expect(decoded.jti).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/)
    })

    test('includes iat (issued at)', () => {
      const beforeTime = Math.floor(Date.now() / 1000)
      const token = generateAccessToken({ userId: '123', email: 'test@test.com' })
      const afterTime = Math.floor(Date.now() / 1000)
      const decoded = verifyAccessToken(token)
      
      expect(decoded.iat).toBeGreaterThanOrEqual(beforeTime)
      expect(decoded.iat).toBeLessThanOrEqual(afterTime)
    })

    test('includes exp (expiration)', () => {
      const token = generateAccessToken({ userId: '123', email: 'test@test.com' })
      const decoded = verifyAccessToken(token)
      const { accessTokenTTL } = getTokenTTL()
      
      expect(decoded.exp).toBeDefined()
      expect(decoded.exp - decoded.iat).toBe(accessTokenTTL)
    })

    test('uses default values for missing role/plan', () => {
      const token = generateAccessToken({ userId: '123', email: 'test@test.com' })
      const decoded = verifyAccessToken(token)
      
      expect(decoded.role).toBe('user')
      expect(decoded.plan).toBe('hobby')
    })

    test('accepts custom TTL option', () => {
      const customTTL = 3600
      const token = generateAccessToken(
        { userId: '123', email: 'test@test.com' },
        { ttl: customTTL }
      )
      const decoded = verifyAccessToken(token)
      
      expect(decoded.exp - decoded.iat).toBe(customTTL)
    })

    test('generates unique tokens', () => {
      const payload = { userId: '123', email: 'test@test.com' }
      const token1 = generateAccessToken(payload)
      const token2 = generateAccessToken(payload)
      
      expect(token1).not.toBe(token2)
    })
  })

  describe('verifyAccessToken', () => {
    test('verifies and decodes valid token', () => {
      const payload = {
        userId: 'user-123',
        email: 'test@example.com',
        role: 'admin'
      }
      const token = generateAccessToken(payload)
      const decoded = verifyAccessToken(token)
      
      expect(decoded.userId).toBe(payload.userId)
      expect(decoded.email).toBe(payload.email)
      expect(decoded.role).toBe(payload.role)
    })

    test('returns expired object for expired token', () => {
      const token = generateAccessToken(
        { userId: '123', email: 'test@test.com' },
        { ttl: -1 }
      )
      
      const result = verifyAccessToken(token)
      
      expect(result.expired).toBe(true)
      expect(result.error).toBe('Token expired')
    })

    test('returns invalid object for malformed token', () => {
      const result = verifyAccessToken('invalid.token.here')
      
      expect(result.invalid).toBe(true)
      expect(result.error).toBe('Invalid token')
    })

    test('returns null for completely invalid input', () => {
      const result = verifyAccessToken('not-a-jwt')
      
      expect(result.invalid).toBe(true)
    })

    test('rejects token signed with wrong secret', () => {
      const jwt = require('jsonwebtoken')
      const wrongSecretToken = jwt.sign(
        { userId: '123' },
        'wrong-secret',
        { algorithm: 'HS256' }
      )
      
      const result = verifyAccessToken(wrongSecretToken)
      
      expect(result.invalid).toBe(true)
    })

    test('rejects token with wrong algorithm', () => {
      const jwt = require('jsonwebtoken')
      const noneAlgToken = jwt.sign(
        { userId: '123' },
        '',
        { algorithm: 'none' }
      )
      
      const result = verifyAccessToken(noneAlgToken)
      
      expect(result.invalid).toBe(true)
    })
  })

  describe('generateRefreshToken', () => {
    test('generates refresh token', () => {
      const result = generateRefreshToken()
      
      expect(result.token).toBeDefined()
      expect(result.tokenHash).toBeDefined()
    })

    test('token is 128 character hex string', () => {
      const { token } = generateRefreshToken()
      
      expect(token.length).toBe(128)
      expect(/^[0-9a-f]+$/.test(token)).toBe(true)
    })

    test('tokenHash contains salt and hash', () => {
      const { tokenHash } = generateRefreshToken()
      const parts = tokenHash.split(':')
      
      expect(parts.length).toBe(2)
      expect(parts[0].length).toBe(64)
      expect(parts[1].length).toBe(64)
    })

    test('generates unique tokens', () => {
      const { token: token1 } = generateRefreshToken()
      const { token: token2 } = generateRefreshToken()
      
      expect(token1).not.toBe(token2)
    })
  })

  describe('verifyRefreshToken', () => {
    test('verifies correct refresh token', () => {
      const { token, tokenHash } = generateRefreshToken()
      
      expect(verifyRefreshToken(token, tokenHash)).toBe(true)
    })

    test('rejects incorrect refresh token', () => {
      const { tokenHash } = generateRefreshToken()
      
      expect(verifyRefreshToken('wrong-token', tokenHash)).toBe(false)
    })

    test('returns false for null token', () => {
      expect(verifyRefreshToken(null, 'hash')).toBe(false)
    })

    test('returns false for null hash', () => {
      expect(verifyRefreshToken('token', null)).toBe(false)
    })

    test('returns false for malformed hash', () => {
      expect(verifyRefreshToken('token', 'malformed')).toBe(false)
    })

    test('returns false for hash with wrong format', () => {
      expect(verifyRefreshToken('token', 'only-one-part')).toBe(false)
    })
  })

  describe('extractToken', () => {
    test('extracts from Authorization header (Bearer)', () => {
      const req = {
        headers: {
          authorization: 'Bearer my-token-123'
        }
      }
      
      expect(extractToken(req)).toBe('my-token-123')
    })

    test('extracts from cookie', () => {
      const req = {
        headers: {
          cookie: 'access_token=cookie-token-456; other=value'
        }
      }
      
      expect(extractToken(req)).toBe('cookie-token-456')
    })

    test('prefers Authorization header over cookie', () => {
      const req = {
        headers: {
          authorization: 'Bearer header-token',
          cookie: 'access_token=cookie-token'
        }
      }
      
      expect(extractToken(req)).toBe('header-token')
    })

    test('extracts from custom cookie name', () => {
      const req = {
        headers: {
          cookie: 'custom_token=custom-value'
        }
      }
      
      expect(extractToken(req, 'custom_token')).toBe('custom-value')
    })

    test('returns null when no token found', () => {
      const req = { headers: {} }
      
      expect(extractToken(req)).toBe(null)
    })

    test('handles empty Authorization header', () => {
      const req = {
        headers: {
          authorization: ''
        }
      }
      
      expect(extractToken(req)).toBe(null)
    })

    test('handles non-Bearer Authorization', () => {
      const req = {
        headers: {
          authorization: 'Basic dXNlcjpwYXNz'
        }
      }
      
      expect(extractToken(req)).toBe(null)
    })
  })

  describe('parseCookies', () => {
    test('parses single cookie', () => {
      const result = parseCookies('name=value')
      
      expect(result.name).toBe('value')
    })

    test('parses multiple cookies', () => {
      const result = parseCookies('name1=value1; name2=value2')
      
      expect(result.name1).toBe('value1')
      expect(result.name2).toBe('value2')
    })

    test('handles cookies with spaces', () => {
      const result = parseCookies('name1=value1 ; name2 = value2')
      
      expect(result.name1).toBe('value1')
      expect(result.name2).toBe('value2')
    })

    test('handles cookies with equals in value', () => {
      const result = parseCookies('name=value=with=equals')
      
      expect(result.name).toBe('value=with=equals')
    })

    test('returns empty object for null input', () => {
      expect(parseCookies(null)).toEqual({})
    })

    test('returns empty object for undefined input', () => {
      expect(parseCookies(undefined)).toEqual({})
    })

    test('returns empty object for empty string', () => {
      expect(parseCookies('')).toEqual({})
    })
  })

  describe('createSecureCookie', () => {
    test('creates basic cookie', () => {
      const result = createSecureCookie('name', 'value')
      
      expect(result).toContain('name=value')
      expect(result).toContain('Path=/')
      expect(result).toContain('HttpOnly')
      expect(result).toContain('SameSite=Strict')
    })

    test('includes Max-Age when provided', () => {
      const result = createSecureCookie('name', 'value', { maxAge: 3600 })
      
      expect(result).toContain('Max-Age=3600')
    })

    test('includes custom Path when provided', () => {
      const result = createSecureCookie('name', 'value', { path: '/api' })
      
      expect(result).toContain('Path=/api')
    })

    test('includes Secure when provided', () => {
      const result = createSecureCookie('name', 'value', { secure: true })
      
      expect(result).toContain('Secure')
    })

    test('includes custom SameSite when provided', () => {
      const result = createSecureCookie('name', 'value', { sameSite: 'Lax' })
      
      expect(result).toContain('SameSite=Lax')
    })

    test('omits HttpOnly when httpOnly is false', () => {
      const result = createSecureCookie('name', 'value', { httpOnly: false })
      
      expect(result).not.toContain('HttpOnly')
    })

    test('creates cookie with all options', () => {
      const result = createSecureCookie('name', 'value', {
        maxAge: 3600,
        path: '/api',
        httpOnly: true,
        secure: true,
        sameSite: 'None'
      })
      
      expect(result).toContain('name=value')
      expect(result).toContain('Max-Age=3600')
      expect(result).toContain('Path=/api')
      expect(result).toContain('HttpOnly')
      expect(result).toContain('Secure')
      expect(result).toContain('SameSite=None')
    })
  })

  describe('getTokenTTL', () => {
    test('returns TTL values', () => {
      const ttl = getTokenTTL()
      
      expect(ttl).toHaveProperty('accessTokenTTL')
      expect(ttl).toHaveProperty('refreshTokenTTL')
      expect(ttl.accessTokenTTL).toBe(15 * 60)
      expect(ttl.refreshTokenTTL).toBe(7 * 24 * 60 * 60)
    })
  })

  describe('isTokenManagerInitialized', () => {
    test('returns true after initialization', () => {
      initTokenManager(testSecret)
      
      expect(isTokenManagerInitialized()).toBe(true)
    })
  })
})
