/**
 * Auth utility functions tests
 * 
 * Property 3: Token expiration detection
 * For any token expiration time, the isTokenExpiringSoon() function SHALL return true 
 * if and only if the remaining time is less than or equal to 5 minutes.
 * 
 * Validates: Requirements 13.3
 */

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
import * as fc from 'fast-check'
import {
  getAccessToken,
  setAccessToken,
  removeAccessToken,
  getRefreshToken,
  setRefreshToken,
  removeRefreshToken,
  getTokenExpires,
  setTokenExpires,
  removeTokenExpires,
  isTokenExpiringSoon,
  isTokenExpired,
  clearTokens,
  setTokens,
  getToken,
  setToken,
  removeToken
} from './auth'

// Mock localStorage
const localStorageMock = (() => {
  let store: Record<string, string> = {}
  return {
    getItem: (key: string) => store[key] || null,
    setItem: (key: string, value: string) => { store[key] = value },
    removeItem: (key: string) => { delete store[key] },
    clear: () => { store = {} }
  }
})()

Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock })

describe('Auth Utilities', () => {
  beforeEach(() => {
    localStorageMock.clear()
    vi.useFakeTimers()
  })

  afterEach(() => {
    vi.useRealTimers()
  })

  describe('Access Token', () => {
    it('should store and retrieve access token', () => {
      setAccessToken('test-access-token')
      expect(getAccessToken()).toBe('test-access-token')
    })

    it('should return null when no access token exists', () => {
      expect(getAccessToken()).toBeNull()
    })

    it('should remove access token', () => {
      setAccessToken('test-token')
      removeAccessToken()
      expect(getAccessToken()).toBeNull()
    })
  })

  describe('Refresh Token', () => {
    it('should store and retrieve refresh token', () => {
      setRefreshToken('test-refresh-token')
      expect(getRefreshToken()).toBe('test-refresh-token')
    })

    it('should return null when no refresh token exists', () => {
      expect(getRefreshToken()).toBeNull()
    })

    it('should remove refresh token', () => {
      setRefreshToken('test-token')
      removeRefreshToken()
      expect(getRefreshToken()).toBeNull()
    })
  })

  describe('Token Expiration', () => {
    it('should store and retrieve token expiration', () => {
      const now = Date.now()
      vi.setSystemTime(now)
      
      setTokenExpires(3600) // 1 hour
      const expires = getTokenExpires()
      
      expect(expires).toBe(now + 3600 * 1000)
    })

    it('should return null when no expiration exists', () => {
      expect(getTokenExpires()).toBeNull()
    })

    it('should remove token expiration', () => {
      setTokenExpires(3600)
      removeTokenExpires()
      expect(getTokenExpires()).toBeNull()
    })
  })

  /**
   * Property 3: Token expiration detection
   * Feature: admin-system, Property 3: Token expiration detection
   * Validates: Requirements 13.3
   */
  describe('isTokenExpiringSoon - Property Based Tests', () => {
    const FIVE_MINUTES_MS = 5 * 60 * 1000

    it('should return false when no expiration is set', () => {
      expect(isTokenExpiringSoon()).toBe(false)
    })

    it('should return true when token expires within 5 minutes', () => {
      const now = Date.now()
      vi.setSystemTime(now)
      
      // Token expires in 4 minutes (within threshold)
      setTokenExpires(4 * 60)
      expect(isTokenExpiringSoon()).toBe(true)
    })

    it('should return true when token expires exactly at 5 minutes', () => {
      const now = Date.now()
      vi.setSystemTime(now)
      
      // Token expires in exactly 5 minutes (at threshold)
      setTokenExpires(5 * 60)
      expect(isTokenExpiringSoon()).toBe(true)
    })

    it('should return false when token expires after 5 minutes', () => {
      const now = Date.now()
      vi.setSystemTime(now)
      
      // Token expires in 6 minutes (beyond threshold)
      setTokenExpires(6 * 60)
      expect(isTokenExpiringSoon()).toBe(false)
    })

    it('should return false when token has already expired', () => {
      const now = Date.now()
      vi.setSystemTime(now)
      
      // Set expiration in the past
      localStorageMock.setItem('admin_token_expires', (now - 1000).toString())
      expect(isTokenExpiringSoon()).toBe(false)
    })

    /**
     * Property-based test: For any remaining time > 0 and <= 5 minutes, 
     * isTokenExpiringSoon should return true
     */
    it('PROPERTY: returns true for any remaining time in (0, 5 minutes]', () => {
      fc.assert(
        fc.property(
          // Generate remaining time between 1ms and 5 minutes (inclusive)
          fc.integer({ min: 1, max: FIVE_MINUTES_MS }),
          (remainingTimeMs) => {
            const now = Date.now()
            vi.setSystemTime(now)
            
            // Set expiration to now + remainingTime
            localStorageMock.setItem('admin_token_expires', (now + remainingTimeMs).toString())
            
            return isTokenExpiringSoon() === true
          }
        ),
        { numRuns: 100 }
      )
    })

    /**
     * Property-based test: For any remaining time > 5 minutes, 
     * isTokenExpiringSoon should return false
     */
    it('PROPERTY: returns false for any remaining time > 5 minutes', () => {
      fc.assert(
        fc.property(
          // Generate remaining time between 5 minutes + 1ms and 1 hour
          fc.integer({ min: FIVE_MINUTES_MS + 1, max: 60 * 60 * 1000 }),
          (remainingTimeMs) => {
            const now = Date.now()
            vi.setSystemTime(now)
            
            // Set expiration to now + remainingTime
            localStorageMock.setItem('admin_token_expires', (now + remainingTimeMs).toString())
            
            return isTokenExpiringSoon() === false
          }
        ),
        { numRuns: 100 }
      )
    })

    /**
     * Property-based test: For any expired token (remaining time <= 0),
     * isTokenExpiringSoon should return false
     */
    it('PROPERTY: returns false for any expired token', () => {
      fc.assert(
        fc.property(
          // Generate negative remaining time (already expired)
          fc.integer({ min: -60 * 60 * 1000, max: 0 }),
          (remainingTimeMs) => {
            const now = Date.now()
            vi.setSystemTime(now)
            
            // Set expiration to now + remainingTime (which is in the past)
            localStorageMock.setItem('admin_token_expires', (now + remainingTimeMs).toString())
            
            return isTokenExpiringSoon() === false
          }
        ),
        { numRuns: 100 }
      )
    })
  })

  describe('isTokenExpired', () => {
    it('should return false when no expiration is set', () => {
      expect(isTokenExpired()).toBe(false)
    })

    it('should return false when token has not expired', () => {
      const now = Date.now()
      vi.setSystemTime(now)
      
      setTokenExpires(3600) // 1 hour
      expect(isTokenExpired()).toBe(false)
    })

    it('should return true when token has expired', () => {
      const now = Date.now()
      vi.setSystemTime(now)
      
      // Set expiration in the past
      localStorageMock.setItem('admin_token_expires', (now - 1000).toString())
      expect(isTokenExpired()).toBe(true)
    })
  })

  describe('Combined Operations', () => {
    it('should clear all tokens', () => {
      setAccessToken('access')
      setRefreshToken('refresh')
      setTokenExpires(3600)
      
      clearTokens()
      
      expect(getAccessToken()).toBeNull()
      expect(getRefreshToken()).toBeNull()
      expect(getTokenExpires()).toBeNull()
    })

    it('should set all tokens at once', () => {
      const now = Date.now()
      vi.setSystemTime(now)
      
      setTokens('access', 'refresh', 3600)
      
      expect(getAccessToken()).toBe('access')
      expect(getRefreshToken()).toBe('refresh')
      expect(getTokenExpires()).toBe(now + 3600 * 1000)
    })
  })

  describe('Legacy Compatibility', () => {
    it('getToken should return access token', () => {
      setAccessToken('test-token')
      expect(getToken()).toBe('test-token')
    })

    it('setToken should set access token', () => {
      setToken('test-token')
      expect(getAccessToken()).toBe('test-token')
    })

    it('removeToken should clear all tokens', () => {
      setTokens('access', 'refresh', 3600)
      removeToken()
      
      expect(getAccessToken()).toBeNull()
      expect(getRefreshToken()).toBeNull()
      expect(getTokenExpires()).toBeNull()
    })
  })
})
