/**
 * 验证码服务测试
 * 
 * 功能测试：
 * - 验证码生成功能
 * - 验证码验证功能
 * - Redis连接和操作
 * - 频率限制机制
 * - 错误处理
 * - 验证码统计信息
 * 
 * 职责分离：
 * - 单元测试覆盖所有公共方法
 * - Mock依赖服务进行隔离测试
 * - 边界条件和异常情况测试
 * 
 * 最近修改：
 * - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录规范
 * 
 * @author moyin
 * @version 1.0.1
 * @since 2025-12-17
 * @lastModified 2026-01-07
 */

import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { BadRequestException, HttpException, HttpStatus } from '@nestjs/common';
import { VerificationService, VerificationCodeType } from './verification.service';
import { IRedisService } from '../../redis/redis.interface';

describe('VerificationService', () => {
  let service: VerificationService;
  let configService: jest.Mocked<ConfigService>;
  let mockRedis: jest.Mocked<IRedisService>;

  beforeEach(async () => {
    // 创建 mock Redis 服务
    mockRedis = {
      set: jest.fn(),
      get: jest.fn(),
      del: jest.fn(),
      exists: jest.fn(),
      expire: jest.fn(),
      ttl: jest.fn(),
      flushall: jest.fn(),
    } as any;

    // Mock ConfigService
    const mockConfigService = {
      get: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        VerificationService,
        {
          provide: ConfigService,
          useValue: mockConfigService,
        },
        {
          provide: 'REDIS_SERVICE',
          useValue: mockRedis,
        },
      ],
    }).compile();

    service = module.get<VerificationService>(VerificationService);
    configService = module.get(ConfigService);
  });

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

  describe('初始化测试', () => {
    it('应该正确初始化验证码服务', () => {
      expect(service).toBeDefined();
      expect(mockRedis).toBeDefined();
    });

    it('应该使用默认Redis配置', () => {
      // 创建 mock Redis 服务
      const mockRedisService = {
        set: jest.fn(),
        get: jest.fn(),
        del: jest.fn(),
        exists: jest.fn(),
        expire: jest.fn(),
        ttl: jest.fn(),
        flushall: jest.fn(),
      };

      new VerificationService(mockRedisService as any);

      // 由于现在使用注入的Redis服务，不再直接创建Redis实例
      expect(true).toBe(true);
    });

    it('应该使用自定义Redis配置', () => {
      // 创建 mock Redis 服务
      const mockRedisService = {
        set: jest.fn(),
        get: jest.fn(),
        del: jest.fn(),
        exists: jest.fn(),
        expire: jest.fn(),
        ttl: jest.fn(),
        flushall: jest.fn(),
      };

      new VerificationService(mockRedisService as any);

      // 由于现在使用注入的Redis服务，不再直接创建Redis实例
      expect(true).toBe(true);
    });

    it('应该正确注入Redis服务', () => {
      expect(mockRedis).toBeDefined();
      expect(typeof mockRedis.set).toBe('function');
      expect(typeof mockRedis.get).toBe('function');
    });
  });

  describe('generateCode', () => {
    beforeEach(() => {
      // Mock 频率限制检查通过
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.get.mockResolvedValue(null);
      mockRedis.set.mockResolvedValue(undefined);
    });

    it('应该成功生成邮箱验证码', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      const code = await service.generateCode(email, type);

      expect(code).toMatch(/^\d{6}$/); // 6位数字
      expect(mockRedis.set).toHaveBeenCalledWith(
        `verification_code:${type}:${email}`,
        expect.stringContaining(code),
        300 // 5分钟
      );
    });

    it('应该成功生成密码重置验证码', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.PASSWORD_RESET;

      const code = await service.generateCode(email, type);

      expect(code).toMatch(/^\d{6}$/);
      expect(mockRedis.set).toHaveBeenCalledWith(
        `verification_code:${type}:${email}`,
        expect.stringContaining(code),
        300
      );
    });

    it('应该在冷却时间内抛出频率限制错误', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      // Mock 冷却时间存在
      mockRedis.exists.mockResolvedValueOnce(true);
      mockRedis.ttl.mockResolvedValue(30);

      await expect(service.generateCode(email, type)).rejects.toThrow(
        new HttpException('请等待 30 秒后再试', HttpStatus.TOO_MANY_REQUESTS)
      );
    });

    it('应该在每小时发送次数达到上限时抛出错误', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      // Mock 冷却时间不存在，但每小时次数达到上限
      mockRedis.exists.mockResolvedValueOnce(false);
      mockRedis.get.mockResolvedValueOnce('5'); // 已达到上限

      await expect(service.generateCode(email, type)).rejects.toThrow(
        new HttpException('每小时发送次数已达上限，请稍后再试', HttpStatus.TOO_MANY_REQUESTS)
      );
    });

    it('应该记录发送尝试', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      await service.generateCode(email, type);

      // 验证冷却时间设置
      expect(mockRedis.set).toHaveBeenCalledWith(
        `verification_cooldown:${type}:${email}`,
        '1',
        60
      );

      // 验证每小时计数
      expect(mockRedis.set).toHaveBeenCalledWith(
        expect.stringMatching(/verification_hourly:/),
        '1',
        3600
      );
    });
  });

  describe('verifyCode', () => {
    const email = 'test@example.com';
    const type = VerificationCodeType.EMAIL_VERIFICATION;
    const code = '123456';

    it('应该成功验证正确的验证码', async () => {
      const codeInfo = {
        code: '123456',
        createdAt: Date.now(),
        attempts: 0,
        maxAttempts: 3,
      };

      mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));
      mockRedis.del.mockResolvedValue(true);

      const result = await service.verifyCode(email, type, code);

      expect(result).toBe(true);
      expect(mockRedis.del).toHaveBeenCalledWith(`verification_code:${type}:${email}`);
    });

    it('应该在验证码不存在时抛出错误', async () => {
      mockRedis.get.mockResolvedValue(null);

      await expect(service.verifyCode(email, type, code)).rejects.toThrow(
        new BadRequestException('验证码不存在或已过期')
      );
    });

    it('应该在尝试次数过多时抛出错误', async () => {
      const codeInfo = {
        code: '123456',
        createdAt: Date.now(),
        attempts: 3,
        maxAttempts: 3,
      };

      mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));
      mockRedis.del.mockResolvedValue(true);

      await expect(service.verifyCode(email, type, code)).rejects.toThrow(
        new BadRequestException('验证码尝试次数过多，请重新获取')
      );

      expect(mockRedis.del).toHaveBeenCalledWith(`verification_code:${type}:${email}`);
    });

    it('应该在验证码错误时增加尝试次数并抛出错误', async () => {
      const codeInfo = {
        code: '123456',
        createdAt: Date.now(),
        attempts: 1,
        maxAttempts: 3,
      };

      mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));
      mockRedis.ttl.mockResolvedValue(240); // Mock TTL 返回 240 秒

      await expect(service.verifyCode(email, type, '654321')).rejects.toThrow(
        new BadRequestException('验证码错误，剩余尝试次数: 1')
      );

      // 验证尝试次数增加
      const updatedCodeInfo = {
        ...codeInfo,
        attempts: 2,
      };
      expect(mockRedis.set).toHaveBeenCalledWith(
        `verification_code:${type}:${email}`,
        JSON.stringify(updatedCodeInfo),
        240
      );
    });

    it('应该在最后一次尝试失败时显示正确的剩余次数', async () => {
      const codeInfo = {
        code: '123456',
        createdAt: Date.now(),
        attempts: 2,
        maxAttempts: 3,
      };

      mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));
      mockRedis.ttl.mockResolvedValue(240); // Mock TTL 返回 240 秒

      await expect(service.verifyCode(email, type, '654321')).rejects.toThrow(
        new BadRequestException('验证码错误，剩余尝试次数: 0')
      );
    });
  });

  describe('codeExists', () => {
    it('应该在验证码存在时返回true', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.exists.mockResolvedValue(true);

      const result = await service.codeExists(email, type);

      expect(result).toBe(true);
      expect(mockRedis.exists).toHaveBeenCalledWith(`verification_code:${type}:${email}`);
    });

    it('应该在验证码不存在时返回false', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.exists.mockResolvedValue(false);

      const result = await service.codeExists(email, type);

      expect(result).toBe(false);
    });
  });

  describe('deleteCode', () => {
    it('应该成功删除验证码', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.del.mockResolvedValue(true);

      await service.deleteCode(email, type);

      expect(mockRedis.del).toHaveBeenCalledWith(`verification_code:${type}:${email}`);
    });
  });

  describe('getCodeTTL', () => {
    it('应该返回验证码剩余时间', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.ttl.mockResolvedValue(180); // 3分钟

      const result = await service.getCodeTTL(email, type);

      expect(result).toBe(180);
      expect(mockRedis.ttl).toHaveBeenCalledWith(`verification_code:${type}:${email}`);
    });

    it('应该在验证码不存在时返回-1', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.ttl.mockResolvedValue(-2); // Redis返回-2表示键不存在

      const result = await service.getCodeTTL(email, type);

      expect(result).toBe(-2);
    });
  });

  describe('getCodeStats', () => {
    const email = 'test@example.com';
    const type = VerificationCodeType.EMAIL_VERIFICATION;

    it('应该返回存在的验证码统计信息', async () => {
      const codeInfo = {
        code: '123456',
        createdAt: 1766035834340,
        attempts: 1,
        maxAttempts: 3,
      };

      mockRedis.exists.mockResolvedValue(true);
      mockRedis.ttl.mockResolvedValue(240);
      mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));

      const result = await service.getCodeStats(email, type);

      expect(result).toEqual({
        exists: true,
        ttl: 240,
        attempts: 1,
        maxAttempts: 3,
        code: '123456',
        createdAt: expect.any(Number),
      });
    });

    it('应该在验证码不存在时返回基本信息', async () => {
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.ttl.mockResolvedValue(-2); // -2 表示键不存在

      const result = await service.getCodeStats(email, type);

      expect(result).toEqual({
        exists: false,
        ttl: -2, // 修改为 -2
      });
    });

    it('应该处理无效的验证码信息', async () => {
      mockRedis.exists.mockResolvedValue(true);
      mockRedis.ttl.mockResolvedValue(240);
      mockRedis.get.mockResolvedValue('invalid json');

      const result = await service.getCodeStats(email, type);

      expect(result).toEqual({
        exists: true,
        ttl: 240,
        attempts: undefined,
        maxAttempts: undefined,
      });
    });
  });

  describe('cleanupExpiredCodes', () => {
    it('应该成功执行清理任务', async () => {
      await service.cleanupExpiredCodes();
      // 由于这个方法主要是日志记录，我们只需要确保它不抛出错误
      expect(true).toBe(true);
    });
  });

  describe('私有方法测试', () => {
    it('应该生成正确格式的Redis键', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      // Mock 频率限制检查通过
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.get.mockResolvedValue(null);
      mockRedis.set.mockResolvedValue(undefined);

      await service.generateCode(email, type);

      expect(mockRedis.set).toHaveBeenCalledWith(
        `verification_code:${type}:${email}`,
        expect.any(String),
        expect.any(Number)
      );
    });

    it('应该生成正确格式的冷却时间键', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      // Mock 频率限制检查通过
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.get.mockResolvedValue(null);
      mockRedis.set.mockResolvedValue(undefined);

      await service.generateCode(email, type);

      expect(mockRedis.set).toHaveBeenCalledWith(
        `verification_cooldown:${type}:${email}`,
        '1',
        60
      );
    });

    it('应该生成正确格式的每小时限制键', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      // Mock 频率限制检查通过
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.get.mockResolvedValue(null);
      mockRedis.set.mockResolvedValue(undefined);

      await service.generateCode(email, type);

      const hour = new Date().getHours();
      const date = new Date().toDateString();
      const expectedKey = `verification_hourly:${type}:${email}:${date}:${hour}`;

      expect(mockRedis.set).toHaveBeenCalledWith(
        expectedKey,
        '1',
        3600
      );
    });
  });

  describe('错误处理测试', () => {
    it('应该处理Redis连接错误', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.exists.mockRejectedValue(new Error('Redis connection failed'));

      await expect(service.generateCode(email, type)).rejects.toThrow('Redis connection failed');
    });

    it('应该处理Redis操作错误', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;
      const code = '123456';

      mockRedis.get.mockRejectedValue(new Error('Redis operation failed'));

      await expect(service.verifyCode(email, type, code)).rejects.toThrow('Redis operation failed');
    });

    it('应该处理JSON解析错误', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;
      const code = '123456';

      mockRedis.get.mockResolvedValue('invalid json string');

      await expect(service.verifyCode(email, type, code)).rejects.toThrow();
    });
  });

  describe('验证码类型测试', () => {
    it('应该支持所有验证码类型', async () => {
      const email = 'test@example.com';
      const types = [
        VerificationCodeType.EMAIL_VERIFICATION,
        VerificationCodeType.PASSWORD_RESET,
        VerificationCodeType.SMS_VERIFICATION,
      ];

      // Mock 频率限制检查通过
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.get.mockResolvedValue(null);
      mockRedis.set.mockResolvedValue(undefined);

      for (const type of types) {
        const code = await service.generateCode(email, type);
        expect(code).toMatch(/^\d{6}$/);
      }

      expect(mockRedis.set).toHaveBeenCalledTimes(types.length * 3); // 每个类型调用3次set
    });
  });

  describe('边界条件测试', () => {
    it('应该处理空字符串标识符', async () => {
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      // Mock 频率限制检查通过
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.get.mockResolvedValue(null);
      mockRedis.set.mockResolvedValue(undefined);

      const code = await service.generateCode('', type);
      expect(code).toMatch(/^\d{6}$/);
    });

    it('应该处理特殊字符标识符', async () => {
      const specialEmail = 'test+special@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      // Mock 频率限制检查通过
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.get.mockResolvedValue(null);
      mockRedis.set.mockResolvedValue(undefined);

      const code = await service.generateCode(specialEmail, type);
      expect(code).toMatch(/^\d{6}$/);
    });

    it('应该处理长标识符', async () => {
      const longEmail = 'a'.repeat(100) + '@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      // Mock 频率限制检查通过
      mockRedis.exists.mockResolvedValue(false);
      mockRedis.get.mockResolvedValue(null);
      mockRedis.set.mockResolvedValue(undefined);

      const code = await service.generateCode(longEmail, type);
      expect(code).toMatch(/^\d{6}$/);
    });
  });

  describe('clearCooldown', () => {
    it('应该成功清除验证码冷却时间', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.del.mockResolvedValue(true);

      await service.clearCooldown(email, type);

      expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`);
    });

    it('应该处理清除不存在的冷却时间', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.del.mockResolvedValue(false);

      await service.clearCooldown(email, type);

      expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`);
    });

    it('应该处理Redis删除操作错误', async () => {
      const email = 'test@example.com';
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.del.mockRejectedValue(new Error('Redis delete failed'));

      await expect(service.clearCooldown(email, type)).rejects.toThrow('Redis delete failed');
    });

    it('应该为不同类型的验证码清除对应的冷却时间', async () => {
      const email = 'test@example.com';
      const types = [
        VerificationCodeType.EMAIL_VERIFICATION,
        VerificationCodeType.PASSWORD_RESET,
        VerificationCodeType.SMS_VERIFICATION,
      ];

      mockRedis.del.mockResolvedValue(true);

      for (const type of types) {
        await service.clearCooldown(email, type);
        expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${email}`);
      }

      expect(mockRedis.del).toHaveBeenCalledTimes(types.length);
    });

    it('应该为不同标识符清除对应的冷却时间', async () => {
      const identifiers = ['test1@example.com', 'test2@example.com', '+8613800138000'];
      const type = VerificationCodeType.EMAIL_VERIFICATION;

      mockRedis.del.mockResolvedValue(true);

      for (const identifier of identifiers) {
        await service.clearCooldown(identifier, type);
        expect(mockRedis.del).toHaveBeenCalledWith(`verification_cooldown:${type}:${identifier}`);
      }

      expect(mockRedis.del).toHaveBeenCalledTimes(identifiers.length);
    });
  });
});