import { describe, it, expect, beforeAll, beforeEach, vi } from '@jest/globals';
import { SMSService } from '@shieldai/shared-notifications';
import type { SMSNotification } from '@shieldai/shared-notifications';
import twilio from 'twilio';

// Mock twilio
vi.mock('twilio', () => {
  const mockMessages = {
    create: vi.fn().mockResolvedValue({
      sid: 'SM1234567890abcdef1234567890abcdef',
      from: '+14155552671',
      to: '+14155552672',
      body: 'Test message',
      status: 'sent',
    }),
  };

  return vi.fn().mockReturnValue({
    messages: mockMessages,
  });
});

describe('SMSService Integration Tests', () => {
  let smsService: SMSService;
  let mockTwilio: any;

  beforeAll(() => {
    smsService = SMSService.getInstance();
    mockTwilio = (require('twilio') as any).mock.results[0].value;
  });

  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('send', () => {
    it('should successfully send SMS notification', async () => {
      mockTwilio.messages.create.mockResolvedValueOnce({
        sid: 'SM1234567890abcdef',
        from: '+14155552671',
        to: '+14155552672',
        body: 'Test SMS',
        status: 'sent',
      });

      const notification: SMSNotification = {
        channel: 'sms',
        to: '+14155552672',
        body: 'Test SMS',
      };

      const result = await smsService.send(notification);

      expect(result.status).toBe('sent');
      expect(result.channel).toBe('sms');
      expect(result.externalId).toBe('SM1234567890abcdef');
      expect(result.notificationId).toContain('sms-');
      expect(result.deliveredAt).toBeInstanceOf(Date);
    });

    it('should use default from number when not provided', async () => {
      mockTwilio.messages.create.mockResolvedValueOnce({
        sid: 'SM-default-from',
        from: '+14155552671',
        to: '+14155552672',
        body: 'Test',
        status: 'sent',
      });

      const notification: SMSNotification = {
        channel: 'sms',
        to: '+14155552672',
        body: 'Test',
      };

      await smsService.send(notification);

      expect(mockTwilio.messages.create).toHaveBeenCalledWith(
        expect.objectContaining({
          from: expect.any(String),
        })
      );
    });

    it('should use custom from number when provided', async () => {
      mockTwilio.messages.create.mockResolvedValueOnce({
        sid: 'SM-custom-from',
        from: '+14155559999',
        to: '+14155552672',
        body: 'Test',
        status: 'sent',
      });

      const notification: SMSNotification = {
        channel: 'sms',
        to: '+14155552672',
        from: '+14155559999',
        body: 'Test',
      };

      await smsService.send(notification);

      expect(mockTwilio.messages.create).toHaveBeenCalledWith(
        expect.objectContaining({
          from: '+14155559999',
        })
      );
    });

    it('should include metadata in SMS', async () => {
      mockTwilio.messages.create.mockResolvedValueOnce({
        sid: 'SM-metadata-test',
        from: '+14155552671',
        to: '+14155552672',
        body: 'Test',
        status: 'sent',
      });

      const notification: SMSNotification = {
        channel: 'sms',
        to: '+14155552672',
        body: 'Test',
        metadata: { userId: 'user-123', campaign: 'promo' },
      };

      await smsService.send(notification);

      expect(mockTwilio.messages.create).toHaveBeenCalledWith(
        expect.objectContaining({
          metadata: { userId: 'user-123', campaign: 'promo' },
        })
      );
    });

    it('should handle Twilio API error', async () => {
      mockTwilio.messages.create.mockRejectedValueOnce(
        new Error('Invalid phone number')
      );

      const notification: SMSNotification = {
        channel: 'sms',
        to: 'invalid-number',
        body: 'Test',
      };

      const result = await smsService.send(notification);

      expect(result.status).toBe('failed');
      expect(result.error).toBe('Invalid phone number');
    });

    it('should handle rate limiting', async () => {
      process.env.SMS_RATE_LIMIT = '2';
      vi.clearAllMocks();
      smsService = SMSService.getInstance();

      mockTwilio.messages.create
        .mockResolvedValueOnce({
          sid: 'SM-rate-1',
          from: '+14155552671',
          to: '+14155552672',
          body: 'Test',
          status: 'sent',
        })
        .mockResolvedValueOnce({
          sid: 'SM-rate-2',
          from: '+14155552671',
          to: '+14155552672',
          body: 'Test',
          status: 'sent',
        });

      const notification: SMSNotification = {
        channel: 'sms',
        to: '+14155552672',
        body: 'Test',
      };

      // First two should succeed
      const result1 = await smsService.send(notification);
      const result2 = await smsService.send(notification);

      expect(result1.status).toBe('sent');
      expect(result2.status).toBe('sent');

      // Third should throw due to rate limit
      await expect(smsService.send(notification)).rejects.toThrow(
        'SMS rate limit exceeded'
      );
    });

    it('should handle network timeout', async () => {
      mockTwilio.messages.create.mockRejectedValueOnce(
        new Error('Network timeout')
      );

      const notification: SMSNotification = {
        channel: 'sms',
        to: '+14155552672',
        body: 'Test',
      };

      const result = await smsService.send(notification);

      expect(result.status).toBe('failed');
      expect(result.error).toBe('Network timeout');
    });

    it('should handle invalid phone number format', async () => {
      mockTwilio.messages.create.mockRejectedValueOnce(
        new Error('Number not in E.164 format')
      );

      const notification: SMSNotification = {
        channel: 'sms',
        to: '12345',
        body: 'Test',
      };

      const result = await smsService.send(notification);

      expect(result.status).toBe('failed');
      expect(result.error).toContain('Number not in E.164 format');
    });
  });

  describe('sendBatch', () => {
    beforeEach(() => {
      vi.clearAllMocks();
      smsService = SMSService.getInstance();
      mockTwilio.messages.create
        .mockResolvedValueOnce({
          sid: 'batch-sms-1',
          from: '+14155552671',
          to: '+14155552672',
          body: 'Test 1',
          status: 'sent',
        })
        .mockResolvedValueOnce({
          sid: 'batch-sms-2',
          from: '+14155552671',
          to: '+14155552673',
          body: 'Test 2',
          status: 'sent',
        })
        .mockResolvedValueOnce({
          sid: 'batch-sms-3',
          from: '+14155552671',
          to: '+14155552674',
          body: 'Test 3',
          status: 'sent',
        });
    });

    it('should send multiple SMS successfully', async () => {
      const notifications: SMSNotification[] = [
        {
          channel: 'sms',
          to: '+14155552672',
          body: 'Batch 1',
        },
        {
          channel: 'sms',
          to: '+14155552673',
          body: 'Batch 2',
        },
        {
          channel: 'sms',
          to: '+14155552674',
          body: 'Batch 3',
        },
      ];

      const results = await smsService.sendBatch(notifications);

      expect(results).toHaveLength(3);
      expect(results.every(r => r.status === 'sent')).toBe(true);
      expect(results.map(r => r.externalId)).toEqual(['batch-sms-1', 'batch-sms-2', 'batch-sms-3']);
    });

    it('should handle partial failures in batch', async () => {
      mockTwilio.messages.create
        .mockResolvedValueOnce({
          sid: 'partial-sms-1',
          from: '+14155552671',
          to: '+14155552672',
          body: 'Valid',
          status: 'sent',
        })
        .mockRejectedValueOnce(new Error('Invalid number'));

      const notifications: SMSNotification[] = [
        {
          channel: 'sms',
          to: '+14155552672',
          body: 'Valid',
        },
        {
          channel: 'sms',
          to: 'invalid',
          body: 'Invalid',
        },
      ];

      const results = await smsService.sendBatch(notifications);

      expect(results).toHaveLength(2);
      expect(results[0].status).toBe('sent');
      expect(results[1].status).toBe('failed');
    });
  });

  describe('getRateLimitStatus', () => {
    beforeEach(() => {
      vi.clearAllMocks();
      smsService = SMSService.getInstance();
    });

    it('should return rate limit status', () => {
      const status = smsService.getRateLimitStatus();

      expect(status).toHaveProperty('remaining');
      expect(status).toHaveProperty('limit');
      expect(status.limit).toBeGreaterThan(0);
      expect(status.remaining).toBeLessThanOrEqual(status.limit);
    });
  });

  describe('Twilio configuration', () => {
    beforeEach(() => {
      vi.clearAllMocks();
      smsService = SMSService.getInstance();
    });

    it('should use configured account SID and auth token', () => {
      expect(require('twilio')).toHaveBeenCalledWith(
        expect.any(String),
        expect.any(String)
      );
    });

    it('should use configured messaging service SID', async () => {
      mockTwilio.messages.create.mockResolvedValueOnce({
        sid: 'SM-config-test',
        from: '+14155552671',
        to: '+14155552672',
        body: 'Test',
        status: 'sent',
      });

      const notification: SMSNotification = {
        channel: 'sms',
        to: '+14155552672',
        body: 'Test',
      };

      await smsService.send(notification);

      expect(mockTwilio.messages.create).toHaveBeenCalledWith(
        expect.objectContaining({
          from: expect.any(String),
        })
      );
    });
  });
});
