import { ZaloOaWebhookController } from '../controllers/zalo-oa-webhook.controller';

describe('ZaloOaWebhookController', () => {
  let controller: ZaloOaWebhookController;
  let mockPrisma: {
    oAuthAccount: { findFirst: ReturnType<typeof vi.fn> };
    zaloAccountLink: { findFirst: ReturnType<typeof vi.fn> };
  };
  let mockLogger: {
    log: ReturnType<typeof vi.fn>;
    warn: ReturnType<typeof vi.fn>;
    error: ReturnType<typeof vi.fn>;
  };
  let mockZaloOaService: {
    isAvailable: boolean;
    isOAuthEnabled: boolean;
    recordInteraction: ReturnType<typeof vi.fn>;
  };

  beforeEach(() => {
    mockPrisma = {
      oAuthAccount: { findFirst: vi.fn() },
      zaloAccountLink: { findFirst: vi.fn() },
    };
    mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn() };
    mockZaloOaService = {
      isAvailable: true,
      isOAuthEnabled: true,
      recordInteraction: vi.fn().mockResolvedValue(undefined),
    };

    controller = new ZaloOaWebhookController(
      mockPrisma as any,
      mockLogger as any,
      mockZaloOaService as any,
    );
  });

  describe('verify', () => {
    it('returns the challenge token', () => {
      const result = controller.verify('test-challenge-123');
      expect(result).toBe('test-challenge-123');
    });

    it('returns empty string when no challenge provided', () => {
      const result = controller.verify(undefined as any);
      expect(result).toBe('');
    });
  });

  describe('handleEvent', () => {
    const mockReq = {} as any;

    it('returns received:true for all events', async () => {
      mockPrisma.zaloAccountLink.findFirst.mockResolvedValue(null);
      mockPrisma.oAuthAccount.findFirst.mockResolvedValue(null);

      const result = await controller.handleEvent(
        { app_id: 'app-1', event_name: 'follow', timestamp: '123', sender: { id: 'zalo-1' }, recipient: { id: 'oa-1' } },
        mockReq,
      );
      expect(result).toEqual({ received: true });
    });

    it('skips processing when neither legacy nor OAuth mode is configured', async () => {
      mockZaloOaService.isAvailable = false;
      mockZaloOaService.isOAuthEnabled = false;

      await controller.handleEvent(
        { app_id: 'app-1', event_name: 'follow', timestamp: '123', sender: { id: 'zalo-1' }, recipient: { id: 'oa-1' } },
        mockReq,
      );

      expect(mockLogger.warn).toHaveBeenCalledWith(
        expect.stringContaining('not configured'),
        'ZaloOaWebhookController',
      );
      expect(mockZaloOaService.recordInteraction).not.toHaveBeenCalled();
    });

    describe('follow event', () => {
      it('records interaction on follow', async () => {
        mockPrisma.zaloAccountLink.findFirst.mockResolvedValue(null);
        mockPrisma.oAuthAccount.findFirst.mockResolvedValue(null);

        await controller.handleEvent(
          { app_id: 'app-1', event_name: 'follow', timestamp: '123', sender: { id: 'zalo-user-123' }, recipient: { id: 'oa-1' } },
          mockReq,
        );

        expect(mockZaloOaService.recordInteraction).toHaveBeenCalledWith('zalo-user-123');
      });

      it('checks OA account link first on follow', async () => {
        mockPrisma.zaloAccountLink.findFirst.mockResolvedValue(null);
        mockPrisma.oAuthAccount.findFirst.mockResolvedValue(null);

        await controller.handleEvent(
          { app_id: 'app-1', event_name: 'follow', timestamp: '123', sender: { id: 'zalo-user-123' }, recipient: { id: 'oa-1' } },
          mockReq,
        );

        expect(mockPrisma.zaloAccountLink.findFirst).toHaveBeenCalledWith({
          where: { zaloUserId: 'zalo-user-123' },
        });
      });

      it('logs when user is already OA-linked', async () => {
        mockPrisma.zaloAccountLink.findFirst.mockResolvedValue({
          userId: 'user-abc',
          zaloUserId: 'zalo-user-123',
        });

        await controller.handleEvent(
          { app_id: 'app-1', event_name: 'follow', timestamp: '123', sender: { id: 'zalo-user-123' }, recipient: { id: 'oa-1' } },
          mockReq,
        );

        expect(mockLogger.log).toHaveBeenCalledWith(
          expect.stringContaining('already OA-linked'),
          'ZaloOaWebhookController',
        );
      });

      it('falls back to OAuthAccount check when no OA link exists', async () => {
        mockPrisma.zaloAccountLink.findFirst.mockResolvedValue(null);
        mockPrisma.oAuthAccount.findFirst.mockResolvedValue({ userId: 'user-oauth' });

        await controller.handleEvent(
          { app_id: 'app-1', event_name: 'follow', timestamp: '123', sender: { id: 'zalo-user-123' }, recipient: { id: 'oa-1' } },
          mockReq,
        );

        expect(mockPrisma.oAuthAccount.findFirst).toHaveBeenCalledWith({
          where: { provider: 'ZALO', providerUserId: 'zalo-user-123' },
        });
        expect(mockLogger.log).toHaveBeenCalledWith(
          expect.stringContaining('linked via social OAuth'),
          'ZaloOaWebhookController',
        );
      });

      it('logs when no link found (user should complete OA linking)', async () => {
        mockPrisma.zaloAccountLink.findFirst.mockResolvedValue(null);
        mockPrisma.oAuthAccount.findFirst.mockResolvedValue(null);

        await controller.handleEvent(
          { app_id: 'app-1', event_name: 'follow', timestamp: '123', sender: { id: 'zalo-user-456' }, recipient: { id: 'oa-1' } },
          mockReq,
        );

        expect(mockLogger.log).toHaveBeenCalledWith(
          expect.stringContaining('no existing link'),
          'ZaloOaWebhookController',
        );
      });
    });

    describe('unfollow event', () => {
      it('logs unfollow event', async () => {
        await controller.handleEvent(
          { app_id: 'app-1', event_name: 'unfollow', timestamp: '123', sender: { id: 'zalo-user-789' }, recipient: { id: 'oa-1' } },
          mockReq,
        );

        expect(mockLogger.log).toHaveBeenCalledWith(
          expect.stringContaining('unfollowed'),
          'ZaloOaWebhookController',
        );
      });
    });

    describe('user_send_text event', () => {
      it('records interaction and checks for OA-linked user', async () => {
        mockPrisma.zaloAccountLink.findFirst.mockResolvedValue({ userId: 'user-linked' });

        await controller.handleEvent(
          {
            app_id: 'app-1',
            event_name: 'user_send_text',
            timestamp: '123',
            sender: { id: 'zalo-user-100' },
            recipient: { id: 'oa-1' },
            message: { text: 'Xin chào', msg_id: 'msg-001' },
          },
          mockReq,
        );

        expect(mockZaloOaService.recordInteraction).toHaveBeenCalledWith('zalo-user-100');
        expect(mockPrisma.zaloAccountLink.findFirst).toHaveBeenCalledWith({
          where: { zaloUserId: 'zalo-user-100' },
          select: { userId: true },
        });
        expect(mockLogger.log).toHaveBeenCalledWith(
          expect.stringContaining('OA-linked user user-linked'),
          'ZaloOaWebhookController',
        );
      });

      it('handles message from unlinked user', async () => {
        mockPrisma.zaloAccountLink.findFirst.mockResolvedValue(null);

        await controller.handleEvent(
          {
            app_id: 'app-1',
            event_name: 'user_send_text',
            timestamp: '123',
            sender: { id: 'zalo-user-200' },
            recipient: { id: 'oa-1' },
            message: { text: 'Hello', msg_id: 'msg-002' },
          },
          mockReq,
        );

        expect(mockLogger.log).toHaveBeenCalledWith(
          expect.stringContaining('Message from Zalo UID'),
          'ZaloOaWebhookController',
        );
      });

      it('ignores messages without text', async () => {
        await controller.handleEvent(
          {
            app_id: 'app-1',
            event_name: 'user_send_text',
            timestamp: '123',
            sender: { id: 'zalo-user-300' },
            recipient: { id: 'oa-1' },
            message: { msg_id: 'msg-003' },
          },
          mockReq,
        );

        expect(mockZaloOaService.recordInteraction).not.toHaveBeenCalled();
      });
    });

    describe('unknown events', () => {
      it('logs unhandled event types', async () => {
        await controller.handleEvent(
          { app_id: 'app-1', event_name: 'user_send_image', timestamp: '123', sender: { id: 'zalo-1' }, recipient: { id: 'oa-1' } },
          mockReq,
        );

        expect(mockLogger.log).toHaveBeenCalledWith(
          expect.stringContaining('Unhandled event type'),
          'ZaloOaWebhookController',
        );
      });
    });

    describe('error handling', () => {
      it('catches and logs errors without throwing', async () => {
        mockZaloOaService.recordInteraction.mockRejectedValue(new Error('DB connection lost'));

        const result = await controller.handleEvent(
          { app_id: 'app-1', event_name: 'follow', timestamp: '123', sender: { id: 'zalo-1' }, recipient: { id: 'oa-1' } },
          mockReq,
        );

        expect(result).toEqual({ received: true });
        expect(mockLogger.error).toHaveBeenCalledWith(
          expect.stringContaining('DB connection lost'),
          expect.any(String),
          'ZaloOaWebhookController',
        );
      });
    });
  });
});
