import {
  SMS_RATE_LIMIT_BUCKETS,
  SmsRateLimiterService,
} from '../services/sms-rate-limiter.service';

describe('SmsRateLimiterService', () => {
  let mockRedis: { getClient: ReturnType<typeof vi.fn> };
  let mockClient: { eval: ReturnType<typeof vi.fn> };
  let mockLogger: {
    log: ReturnType<typeof vi.fn>;
    warn: ReturnType<typeof vi.fn>;
    error: ReturnType<typeof vi.fn>;
  };
  let service: SmsRateLimiterService;

  beforeEach(() => {
    mockClient = { eval: vi.fn() };
    mockRedis = { getClient: vi.fn().mockReturnValue(mockClient) };
    mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };
    service = new SmsRateLimiterService(mockRedis as any, mockLogger as any);
  });

  it('allows the request when Lua script reports under limit', async () => {
    mockClient.eval.mockResolvedValue([1, 0]);

    const decision = await service.check('+84901234567', 'otp');

    expect(decision.allowed).toBe(true);
    expect(decision.current).toBe(1);
    expect(decision.limit).toBe(SMS_RATE_LIMIT_BUCKETS.otp.limit);
    expect(decision.retryAfterSeconds).toBe(0);
    expect(decision.bucket).toBe('otp');
  });

  it('blocks the request and returns retryAfter when limit reached', async () => {
    mockClient.eval.mockResolvedValue([SMS_RATE_LIMIT_BUCKETS.otp.limit, 12_345]);

    const decision = await service.check('+84901234567', 'otp');

    expect(decision.allowed).toBe(false);
    expect(decision.retryAfterSeconds).toBeGreaterThanOrEqual(1);
    expect(mockLogger.warn).toHaveBeenCalledWith(
      expect.stringContaining('SMS rate limit hit'),
      'SmsRateLimiterService',
    );
  });

  it('namespaces the key per phone and bucket', async () => {
    mockClient.eval.mockResolvedValue([1, 0]);

    await service.check('+84901234567', 'transactional');

    expect(mockClient.eval).toHaveBeenCalledWith(
      expect.any(String),
      1,
      'sms_rate_limit:transactional:+84901234567',
      expect.any(Number),
      SMS_RATE_LIMIT_BUCKETS.transactional.windowSeconds * 1000,
      SMS_RATE_LIMIT_BUCKETS.transactional.limit,
      expect.any(String),
      SMS_RATE_LIMIT_BUCKETS.transactional.windowSeconds,
    );
  });

  it('fails open when Redis throws (allows the send, logs warning)', async () => {
    mockClient.eval.mockRejectedValue(new Error('redis down'));

    const decision = await service.check('+84901234567', 'otpHourly');

    expect(decision.allowed).toBe(true);
    expect(decision.current).toBe(0);
    expect(mockLogger.warn).toHaveBeenCalledWith(
      expect.stringContaining('Redis error'),
      'SmsRateLimiterService',
    );
  });
});
