import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { NotificationService } from '../services/notification.service';
import { RedisService } from '../services/redis.service';

vi.mock('../services/email.service', () => ({
  EmailService: {
    getInstance: vi.fn(() => ({
      send: vi.fn(async () => ({
        notificationId: 'email-123',
        channel: 'email',
        status: 'sent',
        externalId: 'ext-123',
      })),
      getRateLimitStatus: vi.fn(() => ({ remaining: 50, limit: 60 })),
    })),
  },
}));

vi.mock('../services/sms.service', () => ({
  SMSService: {
    getInstance: vi.fn(() => ({
      send: vi.fn(async () => ({
        notificationId: 'sms-123',
        channel: 'sms',
        status: 'sent',
        externalId: 'ext-456',
      })),
      getRateLimitStatus: vi.fn(() => ({ remaining: 25, limit: 30 })),
    })),
  },
}));

vi.mock('../services/push.service', () => ({
  PushService: {
    getInstance: vi.fn(() => ({
      send: vi.fn(async () => ({
        notificationId: 'push-123',
        channel: 'push',
        status: 'sent',
        externalId: 'ext-789',
      })),
      getRateLimitStatus: vi.fn(() => ({ remaining: 90, limit: 100 })),
    })),
  },
}));

vi.mock('../config/notification.config', () => ({
  loadNotificationConfig: vi.fn(() => ({
    resend: { apiKey: 'test', baseUrl: 'https://api.resend.com' },
    fcm: { privateKey: 'test', projectId: 'test', clientEmail: 'test@test.com' },
    apns: { key: 'test', keyId: 'test', teamId: 'test', bundleId: 'test' },
    twilio: { accountSid: 'test', authToken: 'test', messagingServiceSid: 'test' },
    rateLimits: {
      emailPerMinute: 60,
      smsPerMinute: 30,
      pushPerMinute: 100,
      windowSeconds: 60,
    },
    redis: {
      url: 'redis://localhost:6379',
      dedupWindowSeconds: 300,
    },
  })),
}));

describe('NotificationService - Rate Limiting', () => {
  let notificationService: NotificationService;
  let redis: RedisService;

  beforeEach(async () => {
    vi.clearAllMocks();
    process.env.EMAIL_RATE_LIMIT = '60';
    process.env.SMS_RATE_LIMIT = '30';
    process.env.PUSH_RATE_LIMIT = '100';
    process.env.RATE_LIMIT_WINDOW_SECONDS = '60';
    process.env.DEDUP_WINDOW_SECONDS = '300';

    redis = RedisService.getInstance({ url: 'redis://localhost:6379' });
    await redis.getClient().flushdb();

    notificationService = NotificationService.getInstance();
  });

  afterEach(async () => {
    await redis.getClient().flushdb();
  });

  describe('checkRateLimit', () => {
    it('should allow request within limit', async () => {
      const result = await notificationService.checkRateLimit('user-1', 'email');

      expect(result.allowed).toBe(true);
      expect(result.currentCount).toBe(1);
      expect(result.limit).toBe(60);
      expect(result.remaining).toBe(59);
      expect(result.resetInSeconds).toBeGreaterThan(0);
    });

    it('should allow multiple requests within limit', async () => {
      for (let i = 0; i < 5; i++) {
        const result = await notificationService.checkRateLimit('user-2', 'email');
        expect(result.allowed).toBe(true);
        expect(result.currentCount).toBe(i + 1);
        expect(result.remaining).toBe(60 - (i + 1));
      }
    });

    it('should deny request when limit is exceeded', async () => {
      for (let i = 0; i < 60; i++) {
        await notificationService.checkRateLimit('user-3', 'email');
      }

      const result = await notificationService.checkRateLimit('user-3', 'email');
      expect(result.allowed).toBe(false);
      expect(result.currentCount).toBe(61);
      expect(result.remaining).toBe(0);
    });

    it('should use custom limit when provided', async () => {
      const result = await notificationService.checkRateLimit('user-4', 'email', 5);
      expect(result.limit).toBe(5);
      expect(result.currentCount).toBe(1);
      expect(result.remaining).toBe(4);
    });

    it('should use custom window when provided', async () => {
      const result = await notificationService.checkRateLimit('user-5', 'email', 10, 120);
      expect(result.resetInSeconds).toBeLessThanOrEqual(120);
      expect(result.resetInSeconds).toBeGreaterThan(0);
    });

    it('should use SMS default limit', async () => {
      const result = await notificationService.checkRateLimit('user-6', 'sms');
      expect(result.limit).toBe(30);
      expect(result.allowed).toBe(true);
    });

    it('should use push default limit', async () => {
      const result = await notificationService.checkRateLimit('user-7', 'push');
      expect(result.limit).toBe(100);
      expect(result.allowed).toBe(true);
    });

    it('should track different identifiers independently', async () => {
      await notificationService.checkRateLimit('user-a', 'email');
      await notificationService.checkRateLimit('user-a', 'email');

      const resultA = await notificationService.checkRateLimit('user-a', 'email');
      expect(resultA.currentCount).toBe(3);

      const resultB = await notificationService.checkRateLimit('user-b', 'email');
      expect(resultB.currentCount).toBe(1);
    });

    it('should track different channels independently', async () => {
      await notificationService.checkRateLimit('user-8', 'email');
      await notificationService.checkRateLimit('user-8', 'email');

      const emailResult = await notificationService.checkRateLimit('user-8', 'email');
      expect(emailResult.currentCount).toBe(3);

      const smsResult = await notificationService.checkRateLimit('user-8', 'sms');
      expect(smsResult.currentCount).toBe(1);
    });
  });
});

describe('NotificationService - Deduplication', () => {
  let notificationService: NotificationService;
  let redis: RedisService;

  beforeEach(async () => {
    vi.clearAllMocks();
    process.env.EMAIL_RATE_LIMIT = '60';
    process.env.SMS_RATE_LIMIT = '30';
    process.env.PUSH_RATE_LIMIT = '100';
    process.env.RATE_LIMIT_WINDOW_SECONDS = '60';
    process.env.DEDUP_WINDOW_SECONDS = '300';

    redis = RedisService.getInstance({ url: 'redis://localhost:6379' });
    await redis.getClient().flushdb();

    notificationService = NotificationService.getInstance();
  });

  afterEach(async () => {
    await redis.getClient().flushdb();
  });

  describe('deduplicateNotification', () => {
    it('should return true for first notification', async () => {
      const result = await notificationService.deduplicateNotification({
        userId: 'user-1',
        templateId: 'welcome-email',
        key: 'initial',
      });

      expect(result).toBe(true);
    });

    it('should return false for duplicate notification', async () => {
      await notificationService.deduplicateNotification({
        userId: 'user-2',
        templateId: 'welcome-email',
        key: 'initial',
      });

      const result = await notificationService.deduplicateNotification({
        userId: 'user-2',
        templateId: 'welcome-email',
        key: 'initial',
      });

      expect(result).toBe(false);
    });

    it('should allow different keys for same user', async () => {
      await notificationService.deduplicateNotification({
        userId: 'user-3',
        templateId: 'welcome-email',
        key: 'initial',
      });

      const result = await notificationService.deduplicateNotification({
        userId: 'user-3',
        templateId: 'welcome-email',
        key: 'followup',
      });

      expect(result).toBe(true);
    });

    it('should allow different users with same template', async () => {
      await notificationService.deduplicateNotification({
        userId: 'user-4',
        templateId: 'welcome-email',
        key: 'initial',
      });

      const result = await notificationService.deduplicateNotification({
        userId: 'user-5',
        templateId: 'welcome-email',
        key: 'initial',
      });

      expect(result).toBe(true);
    });

    it('should use custom window when provided', async () => {
      const result = await notificationService.deduplicateNotification(
        {
          userId: 'user-6',
          templateId: 'test',
          key: 'custom-window',
        },
        60
      );

      expect(result).toBe(true);

      const ttl = await redis.getTTL('dedup:user-6:test:custom-window');
      expect(ttl).toBeLessThanOrEqual(60);
      expect(ttl).toBeGreaterThan(0);
    });

    it('should use windowSeconds from dedupKey', async () => {
      const result = await notificationService.deduplicateNotification({
        userId: 'user-7',
        templateId: 'test',
        key: 'key-window',
        windowSeconds: 120,
      });

      expect(result).toBe(true);

      const ttl = await redis.getTTL('dedup:user-7:test:key-window');
      expect(ttl).toBeLessThanOrEqual(120);
      expect(ttl).toBeGreaterThan(0);
    });

    it('should use default dedup window from config', async () => {
      const result = await notificationService.deduplicateNotification({
        userId: 'user-8',
        templateId: 'test',
        key: 'default-window',
      });

      expect(result).toBe(true);

      const ttl = await redis.getTTL('dedup:user-8:test:default-window');
      expect(ttl).toBeLessThanOrEqual(300);
      expect(ttl).toBeGreaterThan(0);
    });
  });
});

describe('NotificationService - Rate Limit Config', () => {
  let notificationService: NotificationService;

  beforeEach(() => {
    vi.clearAllMocks();
    process.env.EMAIL_RATE_LIMIT = '60';
    process.env.SMS_RATE_LIMIT = '30';
    process.env.PUSH_RATE_LIMIT = '100';
    process.env.RATE_LIMIT_WINDOW_SECONDS = '60';
    process.env.DEDUP_WINDOW_SECONDS = '300';

    notificationService = NotificationService.getInstance();
  });

  describe('getRateLimitConfig', () => {
    it('should return configured rate limits', () => {
      const config = notificationService.getRateLimitConfig();

      expect(config.emailPerMinute).toBe(60);
      expect(config.smsPerMinute).toBe(30);
      expect(config.pushPerMinute).toBe(100);
      expect(config.windowSeconds).toBe(60);
    });
  });
});
