import { Test, TestingModule } from '@nestjs/testing';
import {
  BadRequestException,
  InternalServerErrorException,
} from '@nestjs/common';
import { PluggyController } from './pluggy.controller';
import { PluggyService } from './pluggy.service';
import {
  PluggyRequestDto,
  ConnectTokenDto,
  HttpMethod,
} from './dto/pluggy-request.dto';
import { ServiceCredentialGuard } from '../common/guards/service-credential/service-credential.guard';

describe('PluggyController', () => {
  let controller: PluggyController;
  let service: PluggyService;

  beforeEach(async () => {
    const mockService = {
      makeRequest: jest.fn(),
      getConnectToken: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      controllers: [PluggyController],
      providers: [
        {
          provide: PluggyService,
          useValue: mockService,
        },
      ],
    })
      .overrideGuard(ServiceCredentialGuard)
      .useValue({ canActivate: () => true })
      .compile();

    controller = module.get<PluggyController>(PluggyController);
    service = module.get<PluggyService>(PluggyService);
  });

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

  it('skips the global per-IP throttler at class level (S2S endpoint with its own service-key auth)', () => {
    // @SkipThrottle() sets THROTTLER:SKIP<name> metadata; default throttler name is "default".
    expect(Reflect.getMetadata('THROTTLER:SKIPdefault', PluggyController)).toBe(
      true,
    );
  });

  describe('index', () => {
    it('should make request successfully', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'test-endpoint',
        method: HttpMethod.GET,
        itemId: 'test-item',
      };

      const mockResponse = {
        success: true,
        data: { message: 'Success' },
      };

      jest.spyOn(service, 'makeRequest').mockResolvedValue(mockResponse);

      const result = await controller.index(request);

      expect(service.makeRequest).toHaveBeenCalledWith(
        'test-endpoint',
        'GET',
        request,
      );
      expect(result).toEqual(mockResponse);
    });

    it('should handle POST request', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'items',
        method: HttpMethod.POST,
        name: 'Test Item',
        type: 'bank',
      };

      const mockResponse = {
        success: true,
        data: { id: 'new-item-123' },
      };

      jest.spyOn(service, 'makeRequest').mockResolvedValue(mockResponse);

      const result = await controller.index(request);

      expect(service.makeRequest).toHaveBeenCalledWith(
        'items',
        'POST',
        request,
      );
      expect(result).toEqual(mockResponse);
    });

    it('should handle PUT request', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'items/123',
        method: HttpMethod.PUT,
        itemId: '123',
        status: 'active',
      };

      const mockResponse = {
        success: true,
        data: { id: '123', status: 'active' },
      };

      jest.spyOn(service, 'makeRequest').mockResolvedValue(mockResponse);

      const result = await controller.index(request);

      expect(service.makeRequest).toHaveBeenCalledWith(
        'items/123',
        'PUT',
        request,
      );
      expect(result).toEqual(mockResponse);
    });

    it('should handle DELETE request', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'items/123',
        method: HttpMethod.DELETE,
        itemId: '123',
      };

      const mockResponse = {
        success: true,
        data: { deleted: true },
      };

      jest.spyOn(service, 'makeRequest').mockResolvedValue(mockResponse);

      const result = await controller.index(request);

      expect(service.makeRequest).toHaveBeenCalledWith(
        'items/123',
        'DELETE',
        request,
      );
      expect(result).toEqual(mockResponse);
    });

    it('should rethrow BadRequestException', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'test-endpoint',
        method: HttpMethod.GET,
      };

      const badRequestError = new BadRequestException('Invalid request');
      jest.spyOn(service, 'makeRequest').mockRejectedValue(badRequestError);

      await expect(controller.index(request)).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should convert validation errors to BadRequestException', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'test-endpoint',
        method: HttpMethod.GET,
      };

      const validationError = new Error('status code 400');
      jest.spyOn(service, 'makeRequest').mockRejectedValue(validationError);

      await expect(controller.index(request)).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should convert required field errors to BadRequestException', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'test-endpoint',
        method: HttpMethod.GET,
      };

      const requiredError = new Error('field is required');
      jest.spyOn(service, 'makeRequest').mockRejectedValue(requiredError);

      await expect(controller.index(request)).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should convert other errors to InternalServerErrorException', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'test-endpoint',
        method: HttpMethod.GET,
      };

      const unknownError = new Error('Database connection failed');
      jest.spyOn(service, 'makeRequest').mockRejectedValue(unknownError);

      await expect(controller.index(request)).rejects.toThrow(
        InternalServerErrorException,
      );
    });

    it('should handle non-Error objects', async () => {
      const request: PluggyRequestDto = {
        endpoint: 'test-endpoint',
        method: HttpMethod.GET,
      };

      jest.spyOn(service, 'makeRequest').mockRejectedValue('String error');

      await expect(controller.index(request)).rejects.toThrow(
        InternalServerErrorException,
      );
    });
  });

  describe('getConnectToken', () => {
    it('should get connect token successfully', async () => {
      const request: ConnectTokenDto = {
        itemId: 'test-item-123',
      };

      const mockResponse = {
        accessToken: 'connect_token_123',
        itemId: 'test-item-123',
      };

      jest.spyOn(service, 'getConnectToken').mockResolvedValue(mockResponse);

      const result = await controller.getConnectToken(request);

      expect(service.getConnectToken).toHaveBeenCalledWith('test-item-123');
      expect(result).toEqual(mockResponse);
    });

    it('should rethrow BadRequestException', async () => {
      const request: ConnectTokenDto = {
        itemId: 'test-item-123',
      };

      const badRequestError = new BadRequestException('Invalid itemId');
      jest.spyOn(service, 'getConnectToken').mockRejectedValue(badRequestError);

      await expect(controller.getConnectToken(request)).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should convert validation errors to BadRequestException', async () => {
      const request: ConnectTokenDto = {
        itemId: 'test-item-123',
      };

      const validationError = new Error('status code 422');
      jest.spyOn(service, 'getConnectToken').mockRejectedValue(validationError);

      await expect(controller.getConnectToken(request)).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should convert required field errors to BadRequestException', async () => {
      const request: ConnectTokenDto = {
        itemId: 'test-item-123',
      };

      const requiredError = new Error('itemId is required');
      jest.spyOn(service, 'getConnectToken').mockRejectedValue(requiredError);

      await expect(controller.getConnectToken(request)).rejects.toThrow(
        BadRequestException,
      );
    });

    it('should convert other errors to InternalServerErrorException', async () => {
      const request: ConnectTokenDto = {
        itemId: 'test-item-123',
      };

      const unknownError = new Error('Network timeout');
      jest.spyOn(service, 'getConnectToken').mockRejectedValue(unknownError);

      await expect(controller.getConnectToken(request)).rejects.toThrow(
        InternalServerErrorException,
      );
    });

    it('should handle non-Error objects', async () => {
      const request: ConnectTokenDto = {
        itemId: 'test-item-123',
      };

      jest.spyOn(service, 'getConnectToken').mockRejectedValue({ code: 500 });

      await expect(controller.getConnectToken(request)).rejects.toThrow(
        InternalServerErrorException,
      );
    });
  });
});
