import { ZaloOaService } from '../services/zalo-oa.service';

describe('ZaloOaService', () => {
  let service: ZaloOaService;
  let mockLogger: {
    log: ReturnType<typeof vi.fn>;
    warn: ReturnType<typeof vi.fn>;
    error: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };
    service = new ZaloOaService(mockLogger as any);
    vi.restoreAllMocks();
  });

  afterEach(() => {
    delete process.env['ZALO_OA_ID'];
    delete process.env['ZALO_OA_ACCESS_TOKEN'];
  });

  describe('onModuleInit', () => {
    it('initializes when ZALO_OA_ID and ZALO_OA_ACCESS_TOKEN are set', () => {
      process.env['ZALO_OA_ID'] = 'test-oa-id';
      process.env['ZALO_OA_ACCESS_TOKEN'] = 'test-access-token';

      service.onModuleInit();

      expect(service.isAvailable).toBe(true);
      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('test-oa-id'),
        'ZaloOaService',
      );
    });

    it('disables when ZALO_OA_ID is not set', () => {
      process.env['ZALO_OA_ACCESS_TOKEN'] = 'test-access-token';

      service.onModuleInit();

      expect(service.isAvailable).toBe(false);
      expect(mockLogger.warn).toHaveBeenCalledWith(
        expect.stringContaining('ZALO_OA_ID or ZALO_OA_ACCESS_TOKEN not set'),
        'ZaloOaService',
      );
    });

    it('disables when ZALO_OA_ACCESS_TOKEN is not set', () => {
      process.env['ZALO_OA_ID'] = 'test-oa-id';

      service.onModuleInit();

      expect(service.isAvailable).toBe(false);
      expect(mockLogger.warn).toHaveBeenCalledWith(
        expect.stringContaining('ZALO_OA_ID or ZALO_OA_ACCESS_TOKEN not set'),
        'ZaloOaService',
      );
    });

    it('disables when neither var is set', () => {
      service.onModuleInit();

      expect(service.isAvailable).toBe(false);
      expect(mockLogger.warn).toHaveBeenCalled();
    });
  });

  describe('sendMessage', () => {
    beforeEach(() => {
      process.env['ZALO_OA_ID'] = 'test-oa-id';
      process.env['ZALO_OA_ACCESS_TOKEN'] = 'test-access-token';
      service.onModuleInit();
    });

    it('sends a template message successfully', async () => {
      const mockResponse = {
        ok: true,
        json: vi.fn().mockResolvedValue({
          error: 0,
          message: 'Success',
          data: { msg_id: 'zalo-msg-123' },
        }),
        text: vi.fn(),
      };
      vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);

      const result = await service.sendMessage({
        toUid: '1234567890',
        templateId: 'tpl-inquiry-001',
        templateData: { buyer_name: 'Nguyễn Văn A', listing_title: 'Căn hộ Q7' },
      });

      expect(result).toEqual({ messageId: 'zalo-msg-123' });
      expect(globalThis.fetch).toHaveBeenCalledWith(
        'https://business.openapi.zalo.me/message/template',
        expect.objectContaining({
          method: 'POST',
          headers: expect.objectContaining({
            access_token: 'test-access-token',
          }),
        }),
      );
    });

    it('sends correct request body shape', async () => {
      const mockResponse = {
        ok: true,
        json: vi.fn().mockResolvedValue({
          error: 0,
          data: { msg_id: 'zalo-msg-456' },
        }),
        text: vi.fn(),
      };
      vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);

      await service.sendMessage({
        toUid: '9876543210',
        templateId: 'tpl-payment-001',
        templateData: { amount: '50000000', payment_id: 'PAY-001' },
      });

      const callBody = JSON.parse(
        (globalThis.fetch as any).mock.calls[0][1].body,
      );
      expect(callBody).toEqual({
        phone: '9876543210',
        template_id: 'tpl-payment-001',
        template_data: { amount: '50000000', payment_id: 'PAY-001' },
      });
    });

    it('retries on failure with exponential backoff', async () => {
      const mockFailResponse = {
        ok: false,
        status: 500,
        text: vi.fn().mockResolvedValue('Server error'),
      };
      const mockSuccessResponse = {
        ok: true,
        json: vi.fn().mockResolvedValue({
          error: 0,
          data: { msg_id: 'zalo-msg-retry' },
        }),
        text: vi.fn(),
      };

      vi.spyOn(globalThis, 'fetch')
        .mockResolvedValueOnce(mockFailResponse as any)
        .mockResolvedValueOnce(mockSuccessResponse as any);

      const result = await service.sendMessage({
        toUid: '1234567890',
        templateId: 'tpl-001',
        templateData: { key: 'value' },
      });

      expect(result).toEqual({ messageId: 'zalo-msg-retry' });
      expect(globalThis.fetch).toHaveBeenCalledTimes(2);
      expect(mockLogger.warn).toHaveBeenCalledWith(
        expect.stringContaining('attempt 1/3 failed'),
        'ZaloOaService',
      );
    });

    it('throws after 3 failed attempts', async () => {
      const mockFailResponse = {
        ok: false,
        status: 500,
        text: vi.fn().mockResolvedValue('Server error'),
      };

      vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockFailResponse as any);

      await expect(
        service.sendMessage({
          toUid: '1234567890',
          templateId: 'tpl-001',
          templateData: { key: 'value' },
        }),
      ).rejects.toThrow('Zalo OA API error (500)');

      expect(globalThis.fetch).toHaveBeenCalledTimes(3);
      expect(mockLogger.error).toHaveBeenCalledWith(
        expect.stringContaining('failed after 3 attempts'),
        'ZaloOaService',
      );
    });

    it('throws when Zalo returns non-zero error code', async () => {
      const mockResponse = {
        ok: true,
        json: vi.fn().mockResolvedValue({
          error: -201,
          message: 'Invalid template',
        }),
        text: vi.fn(),
      };

      vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);

      await expect(
        service.sendMessage({
          toUid: '1234567890',
          templateId: 'invalid-tpl',
          templateData: {},
        }),
      ).rejects.toThrow('Zalo OA message rejected');
    });

    it('throws when not initialized', async () => {
      const uninitService = new ZaloOaService(mockLogger as any);

      await expect(
        uninitService.sendMessage({
          toUid: '1234567890',
          templateId: 'tpl-001',
          templateData: {},
        }),
      ).rejects.toThrow('Zalo OA not initialized');
    });

    it('generates a fallback message ID when API does not return one', async () => {
      const mockResponse = {
        ok: true,
        json: vi.fn().mockResolvedValue({ error: 0, data: {} }),
        text: vi.fn(),
      };
      vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);

      const result = await service.sendMessage({
        toUid: '1234567890',
        templateId: 'tpl-001',
        templateData: {},
      });

      expect(result.messageId).toMatch(/^zalo-oa-\d+$/);
    });

    it('masks recipient UID in log output', async () => {
      const mockResponse = {
        ok: true,
        json: vi.fn().mockResolvedValue({
          error: 0,
          data: { msg_id: 'zalo-msg-mask' },
        }),
        text: vi.fn(),
      };
      vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any);

      await service.sendMessage({
        toUid: '1234567890',
        templateId: 'tpl-001',
        templateData: {},
      });

      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('123456***'),
        'ZaloOaService',
      );
    });
  });
});
