import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { QueueRateLimiter } from '../src/rate-limiter';
import type { RateLimitRule } from '../src/types';

describe('QueueRateLimiter', () => {
  const mockRedisClient = {
    host: 'localhost',
    port: 6379,
  };

  const mockLogger = {
    info: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
    debug: mock(() => {}),
  };

  beforeEach(() => {
    mockLogger.info = mock(() => {});
    mockLogger.error = mock(() => {});
    mockLogger.warn = mock(() => {});
    mockLogger.debug = mock(() => {});
  });

  describe('constructor', () => {
    it('should create rate limiter', () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);
      expect(limiter).toBeDefined();
    });
  });

  describe('addRule', () => {
    it('should add a rate limit rule', () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const rule: RateLimitRule = {
        level: 'queue',
        queueName: 'test-queue',
        config: {
          points: 100,
          duration: 60,
        },
      };

      limiter.addRule(rule);

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Rate limit rule added',
        expect.objectContaining({
          level: 'queue',
          queueName: 'test-queue',
        })
      );
    });

    it('should add operation-level rule', () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const rule: RateLimitRule = {
        level: 'operation',
        queueName: 'test-queue',
        handler: 'user-service',
        operation: 'process-user',
        config: {
          points: 10,
          duration: 60,
          blockDuration: 300,
        },
      };

      limiter.addRule(rule);
      const rules = limiter.getRules();
      expect(rules).toHaveLength(1);
      expect(rules[0]).toEqual(rule);
    });
  });

  describe('checkLimit', () => {
    it('should allow when no rules apply', async () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const result = await limiter.checkLimit('test-queue', 'handler', 'operation');
      expect(result.allowed).toBe(true);
      expect(result.appliedRule).toBeUndefined();
    });

    it('should check against global rule', async () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const globalRule: RateLimitRule = {
        level: 'global',
        config: { points: 1000, duration: 60 },
      };

      limiter.addRule(globalRule);

      const result = await limiter.checkLimit('any-queue', 'any-handler', 'any-op');
      // In test environment without real Redis, it returns allowed: true on error
      expect(result.allowed).toBe(true);
      // Check that error was logged
      expect(mockLogger.error).toHaveBeenCalledWith(
        'Rate limit check failed',
        expect.objectContaining({
          queueName: 'any-queue',
          handler: 'any-handler',
          operation: 'any-op',
        })
      );
    });

    it('should prefer more specific rules', async () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      // Add rules from least to most specific
      const globalRule: RateLimitRule = {
        level: 'global',
        config: { points: 1000, duration: 60 },
      };

      const queueRule: RateLimitRule = {
        level: 'queue',
        queueName: 'test-queue',
        config: { points: 100, duration: 60 },
      };

      const handlerRule: RateLimitRule = {
        level: 'handler',
        queueName: 'test-queue',
        handler: 'test-handler',
        config: { points: 50, duration: 60 },
      };

      const operationRule: RateLimitRule = {
        level: 'operation',
        queueName: 'test-queue',
        handler: 'test-handler',
        operation: 'test-op',
        config: { points: 10, duration: 60 },
      };

      limiter.addRule(globalRule);
      limiter.addRule(queueRule);
      limiter.addRule(handlerRule);
      limiter.addRule(operationRule);

      // Operation level should take precedence
      const result = await limiter.checkLimit('test-queue', 'test-handler', 'test-op');
      expect(result.allowed).toBe(true);
      // Check that the most specific rule was attempted (operation level)
      expect(mockLogger.error).toHaveBeenCalledWith(
        'Rate limit check failed',
        expect.objectContaining({
          queueName: 'test-queue',
          handler: 'test-handler',
          operation: 'test-op',
        })
      );
    });
  });

  describe('getStatus', () => {
    it('should get rate limit status', async () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const rule: RateLimitRule = {
        level: 'queue',
        queueName: 'test-queue',
        config: { points: 100, duration: 60 },
      };

      limiter.addRule(rule);

      const status = await limiter.getStatus('test-queue', 'handler', 'operation');

      expect(status.queueName).toBe('test-queue');
      expect(status.handler).toBe('handler');
      expect(status.operation).toBe('operation');
      expect(status.appliedRule).toEqual(rule);
    });

    it('should return status without rule when none apply', async () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const status = await limiter.getStatus('test-queue', 'handler', 'operation');

      expect(status.queueName).toBe('test-queue');
      expect(status.appliedRule).toBeUndefined();
      expect(status.limit).toBeUndefined();
    });
  });

  describe('reset', () => {
    it('should reset rate limits for specific operation', async () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const rule: RateLimitRule = {
        level: 'operation',
        queueName: 'test-queue',
        handler: 'test-handler',
        operation: 'test-op',
        config: { points: 10, duration: 60 },
      };

      limiter.addRule(rule);

      try {
        await limiter.reset('test-queue', 'test-handler', 'test-op');
      } catch (error) {
        // In test environment, limiter.delete will fail due to no Redis connection
        // That's expected, just ensure the method can be called
      }

      // The method should at least attempt to reset
      expect(limiter.getRules()).toContain(rule);
    });

    it('should warn about broad reset', async () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      await limiter.reset('test-queue');

      expect(mockLogger.warn).toHaveBeenCalledWith(
        'Broad reset not implemented yet',
        expect.objectContaining({ queueName: 'test-queue' })
      );
    });
  });

  describe('removeRule', () => {
    it('should remove a rule', () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const rule: RateLimitRule = {
        level: 'queue',
        queueName: 'test-queue',
        config: { points: 100, duration: 60 },
      };

      limiter.addRule(rule);
      expect(limiter.getRules()).toHaveLength(1);

      const removed = limiter.removeRule('queue', 'test-queue');
      expect(removed).toBe(true);
      expect(limiter.getRules()).toHaveLength(0);
    });

    it('should return false when rule not found', () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const removed = limiter.removeRule('queue', 'non-existent');
      expect(removed).toBe(false);
    });
  });

  describe('getRules', () => {
    it('should return all configured rules', () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      const rule1: RateLimitRule = {
        level: 'global',
        config: { points: 1000, duration: 60 },
      };

      const rule2: RateLimitRule = {
        level: 'queue',
        queueName: 'test-queue',
        config: { points: 100, duration: 60 },
      };

      limiter.addRule(rule1);
      limiter.addRule(rule2);

      const rules = limiter.getRules();
      expect(rules).toHaveLength(2);
      expect(rules).toContain(rule1);
      expect(rules).toContain(rule2);
    });
  });

  describe('error handling', () => {
    it('should allow on rate limiter error', async () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      // Add a rule but don't set up the actual limiter to cause an error
      const rule: RateLimitRule = {
        level: 'queue',
        queueName: 'test-queue',
        config: { points: 100, duration: 60 },
      };

      limiter.addRule(rule);

      // Force the limiter map to be empty to simulate error
      (limiter as any).limiters.clear();

      const result = await limiter.checkLimit('test-queue', 'handler', 'operation');

      expect(result.allowed).toBe(true); // Should allow on error
      expect(mockLogger.warn).toHaveBeenCalledWith(
        'Rate limiter not found for rule',
        expect.any(Object)
      );
    });
  });

  describe('hierarchical rule precedence', () => {
    it('should correctly apply rule hierarchy', () => {
      const limiter = new QueueRateLimiter(mockRedisClient, mockLogger);

      // Add multiple rules at different levels
      const rules: RateLimitRule[] = [
        {
          level: 'global',
          config: { points: 10000, duration: 60 },
        },
        {
          level: 'queue',
          queueName: 'email-queue',
          config: { points: 1000, duration: 60 },
        },
        {
          level: 'handler',
          queueName: 'email-queue',
          handler: 'email-service',
          config: { points: 100, duration: 60 },
        },
        {
          level: 'operation',
          queueName: 'email-queue',
          handler: 'email-service',
          operation: 'send-bulk',
          config: { points: 10, duration: 60 },
        },
      ];

      rules.forEach(rule => limiter.addRule(rule));

      // Test that getMostSpecificRule works correctly
      const specificRule = (limiter as any).getMostSpecificRule(
        'email-queue',
        'email-service',
        'send-bulk'
      );

      expect(specificRule?.level).toBe('operation');
      expect(specificRule?.config.points).toBe(10);
    });
  });
});
