import { Test, TestingModule } from '@nestjs/testing';
import {
  HttpException,
  HttpStatus,
  UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PluggyWebhookController } from './pluggy-webhook.controller';
import { PluggyWebhookService } from './pluggy-webhook.service';

describe('PluggyWebhookController', () => {
  let controller: PluggyWebhookController;
  let service: PluggyWebhookService;

  const mockPluggyWebhookService = {
    processWebhook: jest.fn(),
  };

  const mockConfigService = {
    get: jest.fn((key: string) => {
      if (key === 'PLUGGY_WEBHOOK_VERIFY_ENABLED') return 'false';
      return undefined;
    }),
  };

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [PluggyWebhookController],
      providers: [
        {
          provide: PluggyWebhookService,
          useValue: mockPluggyWebhookService,
        },
        {
          provide: ConfigService,
          useValue: mockConfigService,
        },
      ],
    }).compile();

    controller = module.get<PluggyWebhookController>(PluggyWebhookController);
    service = module.get<PluggyWebhookService>(PluggyWebhookService);
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  const makeReq = (
    headers: Record<string, string> = {},
    ip = '177.71.238.212',
  ) => ({ headers, ip }) as any;

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  describe('handleWebhook', () => {
    it('should handle webhook successfully', async () => {
      const webhookBody = {
        event: 'item/created',
        item: { id: 'item_123' },
      };

      mockPluggyWebhookService.processWebhook.mockResolvedValue(undefined);

      const result = await controller.handleWebhook(makeReq(), webhookBody);

      expect(service.processWebhook).toHaveBeenCalledWith(webhookBody);
      expect(result).toEqual({ received: true });
    });

    it('should throw HttpException when service throws error', async () => {
      const webhookBody = {
        event: 'INVALID_EVENT',
      };

      const serviceError = new Error(
        "Invalid webhook payload: unknown event type 'INVALID_EVENT'",
      );
      mockPluggyWebhookService.processWebhook.mockRejectedValue(serviceError);

      try {
        await controller.handleWebhook(makeReq(), webhookBody);
        fail('Expected HttpException to be thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(HttpException);
        expect(error.getStatus()).toBe(HttpStatus.BAD_REQUEST);
        expect(error.getResponse()).toEqual({
          error: "Invalid webhook payload: unknown event type 'INVALID_EVENT'",
        });
      }
    });

    it('should handle service error with unknown error type', async () => {
      const webhookBody = {
        event: 'item/created',
      };

      const unknownError = 'Unknown error type';
      mockPluggyWebhookService.processWebhook.mockRejectedValue(unknownError);

      try {
        await controller.handleWebhook(makeReq(), webhookBody);
        fail('Expected HttpException to be thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(HttpException);
        expect(error.getStatus()).toBe(HttpStatus.BAD_REQUEST);
        expect(error.getResponse()).toEqual({
          error: undefined,
        });
      }
    });

    it('should handle complex webhook payload', async () => {
      const webhookBody = {
        event: 'transactions/created',
        transaction: { id: 'transaction_123' },
        account: { id: 'account_123' },
        user: { id: 'user_123' },
        amount: 100.5,
        description: 'Test transaction',
      };

      mockPluggyWebhookService.processWebhook.mockResolvedValue(undefined);

      const result = await controller.handleWebhook(makeReq(), webhookBody);

      expect(service.processWebhook).toHaveBeenCalledWith(webhookBody);
      expect(result).toEqual({ received: true });
    });

    it('should handle empty webhook payload', async () => {
      const webhookBody = {};

      const serviceError = new Error(
        'Invalid webhook payload: event field is required and must be a string',
      );
      mockPluggyWebhookService.processWebhook.mockRejectedValue(serviceError);

      try {
        await controller.handleWebhook(makeReq(), webhookBody);
        fail('Expected HttpException to be thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(HttpException);
        expect(error.getStatus()).toBe(HttpStatus.BAD_REQUEST);
        expect(error.getResponse()).toEqual({
          error:
            'Invalid webhook payload: event field is required and must be a string',
        });
      }
    });

    it('should handle null webhook payload', async () => {
      const serviceError = new Error(
        'Invalid webhook payload: body must be an object',
      );
      mockPluggyWebhookService.processWebhook.mockRejectedValue(serviceError);

      try {
        await controller.handleWebhook(makeReq(), null);
        fail('Expected HttpException to be thrown');
      } catch (error) {
        expect(error).toBeInstanceOf(HttpException);
        expect(error.getStatus()).toBe(HttpStatus.BAD_REQUEST);
        expect(error.getResponse()).toEqual({
          error: 'Invalid webhook payload: body must be an object',
        });
      }
    });
  });

  describe('authentication (flag on — IP allowlist + secret header)', () => {
    const enableAuth = () =>
      (mockConfigService.get as jest.Mock).mockImplementation((key: string) => {
        if (key === 'PLUGGY_WEBHOOK_VERIFY_ENABLED') return 'true';
        if (key === 'PLUGGY_WEBHOOK_SECRET') return 'whsec_test';
        if (key === 'PLUGGY_WEBHOOK_AUTH_HEADER') return 'X-Webhook-Token';
        if (key === 'PLUGGY_ALLOWED_IPS') return '177.71.238.212';
        return undefined;
      });

    it('throws 401 when the source IP is not allowlisted', async () => {
      enableAuth();
      const body = { event: 'item/created' };
      const req = makeReq({ 'x-webhook-token': 'whsec_test' }, '203.0.113.9');
      await expect(controller.handleWebhook(req, body)).rejects.toBeInstanceOf(
        UnauthorizedException,
      );
      expect(service.processWebhook).not.toHaveBeenCalled();
    });

    it('throws 401 when the secret header is missing or mismatched', async () => {
      enableAuth();
      const body = { event: 'item/created' };
      // missing header
      await expect(
        controller.handleWebhook(makeReq({}, '177.71.238.212'), body),
      ).rejects.toBeInstanceOf(UnauthorizedException);
      // mismatched header
      await expect(
        controller.handleWebhook(
          makeReq({ 'x-webhook-token': 'wrong' }, '177.71.238.212'),
          body,
        ),
      ).rejects.toBeInstanceOf(UnauthorizedException);
      expect(service.processWebhook).not.toHaveBeenCalled();
    });

    it('processes when the IP is allowlisted and the secret header matches', async () => {
      enableAuth();
      mockPluggyWebhookService.processWebhook.mockResolvedValue(undefined);
      const body = { event: 'item/created' };
      const req = makeReq(
        { 'x-webhook-token': 'whsec_test' },
        '177.71.238.212',
      );

      const result = await controller.handleWebhook(req, body);

      expect(result).toEqual({ received: true });
      expect(service.processWebhook).toHaveBeenCalledWith(body);
    });

    it('ignores client-supplied X-Forwarded-For and uses req.ip (trust proxy resolves it)', async () => {
      enableAuth();
      const body = { event: 'item/created' };
      // Spoofed X-Forwarded-For header with Pluggy's allowlisted IP, but req.ip is untrusted client
      const req = {
        ip: '10.9.9.9', // what express resolves via trust proxy (the REAL client)
        headers: {
          'x-forwarded-for': '177.71.238.212, 10.0.0.1', // spoofed first hop
          'x-webhook-token': 'whsec_test',
        },
      } as any;

      // With the fix, the spoofed header must NOT grant access: assertAuthentic should throw
      // because req.ip (10.9.9.9) is not in the allowlist.
      await expect(controller.handleWebhook(req, body)).rejects.toBeInstanceOf(
        UnauthorizedException,
      );
      expect(service.processWebhook).not.toHaveBeenCalled();
    });
  });
});
