/**
 * ThrottleGuard 单元测试
 * 
 * @author moyin
 * @version 1.0.0
 * @since 2026-01-07
 */

import { Test, TestingModule } from '@nestjs/testing';
import { ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ThrottleGuard } from './throttle.guard';
import { ThrottleConfig } from './throttle.decorator';

describe('ThrottleGuard', () => {
  let guard: ThrottleGuard;
  let reflector: jest.Mocked<Reflector>;

  beforeEach(async () => {
    const mockReflector = {
      get: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        ThrottleGuard,
        { provide: Reflector, useValue: mockReflector },
      ],
    }).compile();

    guard = module.get<ThrottleGuard>(ThrottleGuard);
    reflector = module.get(Reflector);
  });

  afterEach(() => {
    jest.clearAllMocks();
    guard.clearAllRecords();
    // 确保清理定时器
    guard.onModuleDestroy();
  });

  describe('canActivate', () => {
    it('should allow request when no throttle config is found', async () => {
      // Arrange
      reflector.get.mockReturnValue(null);
      const mockContext = createMockContext();

      // Act
      const result = await guard.canActivate(mockContext);

      // Assert
      expect(result).toBe(true);
    });

    it('should allow first request within limit', async () => {
      // Arrange
      const config: ThrottleConfig = { limit: 5, ttl: 60 };
      reflector.get.mockReturnValueOnce(config).mockReturnValueOnce(null);
      const mockContext = createMockContext();

      // Act
      const result = await guard.canActivate(mockContext);

      // Assert
      expect(result).toBe(true);
    });

    it('should throw HttpException when limit exceeded', async () => {
      // Arrange
      const config: ThrottleConfig = { limit: 1, ttl: 60 };
      reflector.get.mockReturnValue(config);
      const mockContext = createMockContext();

      // Act - first request should pass
      await guard.canActivate(mockContext);

      // Assert - second request should throw
      await expect(guard.canActivate(mockContext)).rejects.toThrow(HttpException);
    });
  });

  describe('getStats', () => {
    it('should return empty stats initially', () => {
      const stats = guard.getStats();
      expect(stats.totalRecords).toBe(0);
    });
  });

  describe('clearAllRecords', () => {
    it('should clear all records', () => {
      guard.clearAllRecords();
      const stats = guard.getStats();
      expect(stats.totalRecords).toBe(0);
    });
  });

  describe('onModuleDestroy', () => {
    it('should cleanup resources', () => {
      expect(() => guard.onModuleDestroy()).not.toThrow();
    });
  });

  function createMockContext(): ExecutionContext {
    const mockRequest = {
      ip: '127.0.0.1',
      method: 'POST',
      url: '/api/test',
      route: { path: '/api/test' },
      get: jest.fn(),
    };

    return {
      switchToHttp: jest.fn().mockReturnValue({
        getRequest: jest.fn().mockReturnValue(mockRequest),
      }),
      getHandler: jest.fn(),
      getClass: jest.fn(),
    } as any;
  }
});