import { loggerMock } from '@sim/testing'
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
import { RateLimiter } from './rate-limiter'
import type { ConsumeResult, RateLimitStorageAdapter, TokenStatus } from './storage'
import { MANUAL_EXECUTION_LIMIT, RATE_LIMITS, RateLimitError } from './types'

vi.mock('@sim/logger', () => loggerMock)
vi.mock('@/lib/core/config/feature-flags', () => ({ isBillingEnabled: true }))

interface MockAdapter {
  consumeTokens: Mock
  getTokenStatus: Mock
  resetBucket: Mock
}

const createMockAdapter = (): MockAdapter => ({
  consumeTokens: vi.fn(),
  getTokenStatus: vi.fn(),
  resetBucket: vi.fn(),
})

describe('RateLimiter', () => {
  const testUserId = 'test-user-123'
  const freeSubscription = { plan: 'free', referenceId: testUserId }
  let mockAdapter: MockAdapter
  let rateLimiter: RateLimiter

  beforeEach(() => {
    vi.clearAllMocks()
    mockAdapter = createMockAdapter()
    rateLimiter = new RateLimiter(mockAdapter as RateLimitStorageAdapter)
  })

  describe('checkRateLimitWithSubscription', () => {
    it('should allow unlimited requests for manual trigger type', async () => {
      const result = await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        freeSubscription,
        'manual',
        false
      )

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(MANUAL_EXECUTION_LIMIT)
      expect(result.resetAt).toBeInstanceOf(Date)
      expect(mockAdapter.consumeTokens).not.toHaveBeenCalled()
    })

    it('should consume tokens for API requests', async () => {
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: RATE_LIMITS.free.sync.maxTokens - 1,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      const result = await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        freeSubscription,
        'api',
        false
      )

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(mockResult.tokensRemaining)
      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${testUserId}:sync`,
        1,
        RATE_LIMITS.free.sync
      )
    })

    it('should use async bucket for async requests', async () => {
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: RATE_LIMITS.free.async.maxTokens - 1,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(testUserId, freeSubscription, 'api', true)

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${testUserId}:async`,
        1,
        RATE_LIMITS.free.async
      )
    })

    it('should use api-endpoint bucket for api-endpoint trigger', async () => {
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: RATE_LIMITS.free.apiEndpoint.maxTokens - 1,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        freeSubscription,
        'api-endpoint',
        false
      )

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${testUserId}:api-endpoint`,
        1,
        RATE_LIMITS.free.apiEndpoint
      )
    })

    it('should deny requests when rate limit exceeded', async () => {
      const mockResult: ConsumeResult = {
        allowed: false,
        tokensRemaining: 0,
        resetAt: new Date(Date.now() + 60000),
        retryAfterMs: 30000,
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      const result = await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        freeSubscription,
        'api',
        false
      )

      expect(result.allowed).toBe(false)
      expect(result.remaining).toBe(0)
      expect(result.retryAfterMs).toBe(30000)
    })

    it('should use organization key for team subscriptions', async () => {
      const orgId = 'org-123'
      const teamSubscription = { plan: 'team', referenceId: orgId }
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: RATE_LIMITS.team.sync.maxTokens - 1,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(testUserId, teamSubscription, 'api', false)

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${orgId}:sync`,
        1,
        RATE_LIMITS.team.sync
      )
    })

    it('should use user key when team subscription referenceId matches userId', async () => {
      const directTeamSubscription = { plan: 'team', referenceId: testUserId }
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: RATE_LIMITS.team.sync.maxTokens - 1,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        directTeamSubscription,
        'api',
        false
      )

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${testUserId}:sync`,
        1,
        RATE_LIMITS.team.sync
      )
    })

    it('should allow on storage error (fail open)', async () => {
      mockAdapter.consumeTokens.mockRejectedValue(new Error('Storage error'))

      const result = await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        freeSubscription,
        'api',
        false
      )

      expect(result.allowed).toBe(true)
      expect(result.remaining).toBe(1)
    })

    it('should work for all non-manual trigger types', async () => {
      const triggerTypes = ['api', 'webhook', 'schedule', 'chat'] as const
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: 10,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      for (const triggerType of triggerTypes) {
        await rateLimiter.checkRateLimitWithSubscription(
          testUserId,
          freeSubscription,
          triggerType,
          false
        )
        expect(mockAdapter.consumeTokens).toHaveBeenCalled()
        mockAdapter.consumeTokens.mockClear()
      }
    })
  })

  describe('getRateLimitStatusWithSubscription', () => {
    it('should return unlimited status for manual trigger type', async () => {
      const status = await rateLimiter.getRateLimitStatusWithSubscription(
        testUserId,
        freeSubscription,
        'manual',
        false
      )

      expect(status.requestsPerMinute).toBe(MANUAL_EXECUTION_LIMIT)
      expect(status.maxBurst).toBe(MANUAL_EXECUTION_LIMIT)
      expect(status.remaining).toBe(MANUAL_EXECUTION_LIMIT)
      expect(mockAdapter.getTokenStatus).not.toHaveBeenCalled()
    })

    it('should return status from storage for API requests', async () => {
      const mockStatus: TokenStatus = {
        tokensAvailable: 15,
        maxTokens: RATE_LIMITS.free.sync.maxTokens,
        lastRefillAt: new Date(),
        nextRefillAt: new Date(Date.now() + 60000),
      }
      mockAdapter.getTokenStatus.mockResolvedValue(mockStatus)

      const status = await rateLimiter.getRateLimitStatusWithSubscription(
        testUserId,
        freeSubscription,
        'api',
        false
      )

      expect(status.remaining).toBe(15)
      expect(status.requestsPerMinute).toBe(RATE_LIMITS.free.sync.refillRate)
      expect(status.maxBurst).toBe(RATE_LIMITS.free.sync.maxTokens)
      expect(mockAdapter.getTokenStatus).toHaveBeenCalledWith(
        `${testUserId}:sync`,
        RATE_LIMITS.free.sync
      )
    })
  })

  describe('resetRateLimit', () => {
    it('should reset all bucket types for a user', async () => {
      mockAdapter.resetBucket.mockResolvedValue(undefined)

      await rateLimiter.resetRateLimit(testUserId)

      expect(mockAdapter.resetBucket).toHaveBeenCalledTimes(3)
      expect(mockAdapter.resetBucket).toHaveBeenCalledWith(`${testUserId}:sync`)
      expect(mockAdapter.resetBucket).toHaveBeenCalledWith(`${testUserId}:async`)
      expect(mockAdapter.resetBucket).toHaveBeenCalledWith(`${testUserId}:api-endpoint`)
    })

    it('should throw error if reset fails', async () => {
      mockAdapter.resetBucket.mockRejectedValue(new Error('Reset failed'))

      await expect(rateLimiter.resetRateLimit(testUserId)).rejects.toThrow('Reset failed')
    })
  })

  describe('subscription plan handling', () => {
    it('should use pro plan limits', async () => {
      const proSubscription = { plan: 'pro', referenceId: testUserId }
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: RATE_LIMITS.pro.sync.maxTokens - 1,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(testUserId, proSubscription, 'api', false)

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${testUserId}:sync`,
        1,
        RATE_LIMITS.pro.sync
      )
    })

    it('should use enterprise plan limits', async () => {
      const enterpriseSubscription = { plan: 'enterprise', referenceId: 'org-enterprise' }
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: RATE_LIMITS.enterprise.sync.maxTokens - 1,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        enterpriseSubscription,
        'api',
        false
      )

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `org-enterprise:sync`,
        1,
        RATE_LIMITS.enterprise.sync
      )
    })

    it('should fall back to free plan when subscription is null', async () => {
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: RATE_LIMITS.free.sync.maxTokens - 1,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(testUserId, null, 'api', false)

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${testUserId}:sync`,
        1,
        RATE_LIMITS.free.sync
      )
    })
  })

  describe('schedule trigger type', () => {
    it('should use sync bucket for schedule trigger', async () => {
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: 10,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        freeSubscription,
        'schedule',
        false
      )

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${testUserId}:sync`,
        1,
        RATE_LIMITS.free.sync
      )
    })

    it('should use async bucket for schedule trigger with isAsync true', async () => {
      const mockResult: ConsumeResult = {
        allowed: true,
        tokensRemaining: 10,
        resetAt: new Date(Date.now() + 60000),
      }
      mockAdapter.consumeTokens.mockResolvedValue(mockResult)

      await rateLimiter.checkRateLimitWithSubscription(
        testUserId,
        freeSubscription,
        'schedule',
        true
      )

      expect(mockAdapter.consumeTokens).toHaveBeenCalledWith(
        `${testUserId}:async`,
        1,
        RATE_LIMITS.free.async
      )
    })
  })

  describe('getRateLimitStatusWithSubscription error handling', () => {
    it('should return default config on storage error', async () => {
      mockAdapter.getTokenStatus.mockRejectedValue(new Error('Storage error'))

      const status = await rateLimiter.getRateLimitStatusWithSubscription(
        testUserId,
        freeSubscription,
        'api',
        false
      )

      expect(status.remaining).toBe(0)
      expect(status.requestsPerMinute).toBe(RATE_LIMITS.free.sync.refillRate)
      expect(status.maxBurst).toBe(RATE_LIMITS.free.sync.maxTokens)
    })
  })
})

describe('RateLimitError', () => {
  it('should create error with default status code 429', () => {
    const error = new RateLimitError('Rate limit exceeded')

    expect(error.message).toBe('Rate limit exceeded')
    expect(error.statusCode).toBe(429)
    expect(error.name).toBe('RateLimitError')
  })

  it('should create error with custom status code', () => {
    const error = new RateLimitError('Custom error', 503)

    expect(error.message).toBe('Custom error')
    expect(error.statusCode).toBe(503)
  })

  it('should be instanceof Error', () => {
    const error = new RateLimitError('Test')

    expect(error instanceof Error).toBe(true)
    expect(error instanceof RateLimitError).toBe(true)
  })

  it('should have proper stack trace', () => {
    const error = new RateLimitError('Test error')

    expect(error.stack).toBeDefined()
    expect(error.stack).toContain('RateLimitError')
  })
})
