/**
 * FileRedisService单元测试
 * 
 * 功能描述：
 * - 测试文件模拟Redis服务的所有公共方法
 * - 验证文件系统操作和数据持久化
 * - 测试过期时间机制和自动清理功能
 * - 测试正常情况、异常情况和边界情况
 * 
 * 职责分离：
 * - 单元测试：隔离测试每个方法的功能
 * - Mock测试：使用模拟文件系统避免真实文件操作
 * - 过期测试：验证TTL机制和自动清理
 * - 边界测试：测试参数边界和特殊情况
 * 
 * 最近修改：
 * - 2025-01-07: 功能新增 - 创建FileRedisService完整单元测试，覆盖所有公共方法
 * 
 * @author moyin
 * @version 1.0.0
 * @since 2025-01-07
 * @lastModified 2025-01-07
 */
import { Test, TestingModule } from '@nestjs/testing';
import { FileRedisService } from './file_redis.service';
import { promises as fs } from 'fs';
import * as path from 'path';

// Mock fs promises
jest.mock('fs', () => ({
  promises: {
    mkdir: jest.fn(),
    readFile: jest.fn(),
    writeFile: jest.fn(),
  },
}));

// Mock path
jest.mock('path');

// Mock global timers
const mockSetInterval = jest.fn();
const mockClearInterval = jest.fn();
global.setInterval = mockSetInterval;
global.clearInterval = mockClearInterval;

describe('FileRedisService', () => {
  let service: FileRedisService;
  let mockFs: jest.Mocked<typeof fs>;
  let mockPath: jest.Mocked<typeof path>;
  let mockSetInterval: jest.Mock;
  let mockClearInterval: jest.Mock;

  beforeEach(async () => {
    mockFs = fs as jest.Mocked<typeof fs>;
    mockPath = path as jest.Mocked<typeof path>;
    mockSetInterval = global.setInterval as jest.Mock;
    mockClearInterval = global.clearInterval as jest.Mock;

    // Mock path.join to return predictable paths
    mockPath.join.mockImplementation((...args) => args.join('/'));

    // Mock process.cwd()
    jest.spyOn(process, 'cwd').mockReturnValue('/test/project');

    const module: TestingModule = await Test.createTestingModule({
      providers: [FileRedisService],
    }).compile();

    service = module.get<FileRedisService>(FileRedisService);

    // 等待构造函数中的异步初始化完成
    await new Promise(resolve => setTimeout(resolve, 10));
  });

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

  describe('构造函数和初始化', () => {
    it('should create service successfully', () => {
      expect(service).toBeDefined();
    });

    it('should create data directory during initialization', () => {
      expect(mockFs.mkdir).toHaveBeenCalledWith('/test/project/redis-data', { recursive: true });
    });

    it('should attempt to load existing data', () => {
      expect(mockFs.readFile).toHaveBeenCalledWith('/test/project/redis-data/redis.json', 'utf-8');
    });
  });

  describe('set', () => {
    beforeEach(() => {
      mockFs.writeFile.mockResolvedValue(undefined);
    });

    it('should set key-value without TTL', async () => {
      await service.set('testKey', 'testValue');

      expect(mockFs.writeFile).toHaveBeenCalledWith(
        '/test/project/redis-data/redis.json',
        expect.stringContaining('"testKey"')
      );
    });

    it('should set key-value with TTL', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      await service.set('testKey', 'testValue', 3600);

      const expectedExpireAt = now + 3600 * 1000;
      expect(mockFs.writeFile).toHaveBeenCalledWith(
        '/test/project/redis-data/redis.json',
        expect.stringContaining(expectedExpireAt.toString())
      );

      jest.useRealTimers();
    });

    it('should not set TTL when TTL is 0', async () => {
      await service.set('testKey', 'testValue', 0);

      expect(mockFs.writeFile).toHaveBeenCalledWith(
        '/test/project/redis-data/redis.json',
        expect.not.stringContaining('expireAt')
      );
    });

    it('should handle file write errors gracefully', async () => {
      const error = new Error('File write failed');
      mockFs.writeFile.mockRejectedValue(error);

      // 应该不抛出异常，而是在内部处理
      await expect(service.set('testKey', 'testValue')).resolves.not.toThrow();
    });
  });

  describe('get', () => {
    it('should return value when key exists and not expired', async () => {
      // 模拟内存中有数据
      const testData = new Map([
        ['testKey', { value: 'testValue' }]
      ]);
      (service as any).data = testData;

      const result = await service.get('testKey');

      expect(result).toBe('testValue');
    });

    it('should return null when key does not exist', async () => {
      // 确保内存中没有数据
      (service as any).data = new Map();

      const result = await service.get('nonExistentKey');

      expect(result).toBeNull();
    });

    it('should return null and remove expired key', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      // 模拟过期的数据
      const testData = new Map([
        ['expiredKey', { value: 'expiredValue', expireAt: now - 1000 }]
      ]);
      (service as any).data = testData;
      mockFs.writeFile.mockResolvedValue(undefined);

      const result = await service.get('expiredKey');

      expect(result).toBeNull();
      expect(testData.has('expiredKey')).toBe(false);
      expect(mockFs.writeFile).toHaveBeenCalled();

      jest.useRealTimers();
    });
  });

  describe('del', () => {
    beforeEach(() => {
      mockFs.writeFile.mockResolvedValue(undefined);
    });

    it('should return true when key is deleted', async () => {
      const testData = new Map([
        ['testKey', { value: 'testValue' }]
      ]);
      (service as any).data = testData;

      const result = await service.del('testKey');

      expect(result).toBe(true);
      expect(testData.has('testKey')).toBe(false);
      expect(mockFs.writeFile).toHaveBeenCalled();
    });

    it('should return false when key does not exist', async () => {
      (service as any).data = new Map();

      const result = await service.del('nonExistentKey');

      expect(result).toBe(false);
      expect(mockFs.writeFile).not.toHaveBeenCalled();
    });
  });

  describe('exists', () => {
    it('should return true when key exists and not expired', async () => {
      const testData = new Map([
        ['testKey', { value: 'testValue' }]
      ]);
      (service as any).data = testData;

      const result = await service.exists('testKey');

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

    it('should return false when key does not exist', async () => {
      (service as any).data = new Map();

      const result = await service.exists('nonExistentKey');

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

    it('should return false and remove expired key', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      const testData = new Map([
        ['expiredKey', { value: 'expiredValue', expireAt: now - 1000 }]
      ]);
      (service as any).data = testData;
      mockFs.writeFile.mockResolvedValue(undefined);

      const result = await service.exists('expiredKey');

      expect(result).toBe(false);
      expect(testData.has('expiredKey')).toBe(false);

      jest.useRealTimers();
    });
  });

  describe('expire', () => {
    beforeEach(() => {
      mockFs.writeFile.mockResolvedValue(undefined);
    });

    it('should set expiration time for existing key', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      const testData = new Map([
        ['testKey', { value: 'testValue' }]
      ]);
      (service as any).data = testData;

      await service.expire('testKey', 3600);

      const item = testData.get('testKey');
      expect((item as any)?.expireAt).toBe(now + 3600 * 1000);
      expect(mockFs.writeFile).toHaveBeenCalled();

      jest.useRealTimers();
    });

    it('should do nothing for non-existent key', async () => {
      (service as any).data = new Map();

      await service.expire('nonExistentKey', 3600);

      expect(mockFs.writeFile).not.toHaveBeenCalled();
    });
  });

  describe('ttl', () => {
    it('should return remaining TTL for key with expiration', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      const testData = new Map([
        ['testKey', { value: 'testValue', expireAt: now + 3600 * 1000 }]
      ]);
      (service as any).data = testData;

      const result = await service.ttl('testKey');

      expect(result).toBe(3600);

      jest.useRealTimers();
    });

    it('should return -1 for key without expiration', async () => {
      const testData = new Map([
        ['testKey', { value: 'testValue' }]
      ]);
      (service as any).data = testData;

      const result = await service.ttl('testKey');

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

    it('should return -2 for non-existent key', async () => {
      (service as any).data = new Map();

      const result = await service.ttl('nonExistentKey');

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

    it('should return -2 and remove expired key', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      const testData = new Map([
        ['expiredKey', { value: 'expiredValue', expireAt: now - 1000 }]
      ]);
      (service as any).data = testData;
      mockFs.writeFile.mockResolvedValue(undefined);

      const result = await service.ttl('expiredKey');

      expect(result).toBe(-2);
      expect(testData.has('expiredKey')).toBe(false);

      jest.useRealTimers();
    });
  });

  describe('flushall', () => {
    it('should clear all data', async () => {
      const testData = new Map([
        ['key1', { value: 'value1' }],
        ['key2', { value: 'value2' }]
      ]);
      (service as any).data = testData;
      mockFs.writeFile.mockResolvedValue(undefined);

      await service.flushall();

      expect(testData.size).toBe(0);
      expect(mockFs.writeFile).toHaveBeenCalled();
    });
  });

  describe('setex', () => {
    it('should set key with expiration time', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      mockFs.writeFile.mockResolvedValue(undefined);

      await service.setex('testKey', 1800, 'testValue');

      const testData = (service as any).data;
      const item = testData.get('testKey');
      expect(item.value).toBe('testValue');
      expect(item.expireAt).toBe(now + 1800 * 1000);

      jest.useRealTimers();
    });
  });

  describe('incr', () => {
    beforeEach(() => {
      mockFs.writeFile.mockResolvedValue(undefined);
    });

    it('should increment existing numeric value', async () => {
      const testData = new Map([
        ['counter', { value: '5' }]
      ]);
      (service as any).data = testData;

      const result = await service.incr('counter');

      expect(result).toBe(6);
      expect(testData.get('counter').value).toBe('6');
    });

    it('should initialize non-existent key to 1', async () => {
      (service as any).data = new Map();

      const result = await service.incr('newCounter');

      expect(result).toBe(1);
      const testData = (service as any).data;
      expect(testData.get('newCounter').value).toBe('1');
    });
  });

  describe('sadd', () => {
    beforeEach(() => {
      mockFs.writeFile.mockResolvedValue(undefined);
    });

    it('should add member to new set', async () => {
      (service as any).data = new Map();

      await service.sadd('users', 'user123');

      const testData = (service as any).data;
      const setData = JSON.parse(testData.get('users').value);
      expect(setData).toContain('user123');
    });

    it('should add member to existing set', async () => {
      const testData = new Map([
        ['users', { value: JSON.stringify(['user1', 'user2']) }]
      ]);
      (service as any).data = testData;

      await service.sadd('users', 'user3');

      const setData = JSON.parse(testData.get('users').value);
      expect(setData).toContain('user1');
      expect(setData).toContain('user2');
      expect(setData).toContain('user3');
    });

    it('should preserve expiration time when adding to existing set', async () => {
      const expireAt = Date.now() + 3600 * 1000;
      const testData = new Map([
        ['users', { value: JSON.stringify(['user1']), expireAt }]
      ]);
      (service as any).data = testData;

      await service.sadd('users', 'user2');

      expect(testData.get('users').expireAt).toBe(expireAt);
    });
  });

  describe('srem', () => {
    beforeEach(() => {
      mockFs.writeFile.mockResolvedValue(undefined);
    });

    it('should remove member from set', async () => {
      const testData = new Map([
        ['users', { value: JSON.stringify(['user1', 'user2', 'user3']) }]
      ]);
      (service as any).data = testData;

      await service.srem('users', 'user2');

      const setData = JSON.parse(testData.get('users').value);
      expect(setData).not.toContain('user2');
      expect(setData).toContain('user1');
      expect(setData).toContain('user3');
    });

    it('should delete key when set becomes empty', async () => {
      const testData = new Map([
        ['users', { value: JSON.stringify(['user1']) }]
      ]);
      (service as any).data = testData;

      await service.srem('users', 'user1');

      expect(testData.has('users')).toBe(false);
    });

    it('should do nothing for non-existent key', async () => {
      (service as any).data = new Map();

      await service.srem('nonExistentSet', 'member');

      expect(mockFs.writeFile).not.toHaveBeenCalled();
    });
  });

  describe('smembers', () => {
    it('should return all set members', async () => {
      const members = ['user1', 'user2', 'user3'];
      const testData = new Map([
        ['users', { value: JSON.stringify(members) }]
      ]);
      (service as any).data = testData;

      const result = await service.smembers('users');

      expect(result).toEqual(members);
    });

    it('should return empty array for non-existent set', async () => {
      (service as any).data = new Map();

      const result = await service.smembers('nonExistentSet');

      expect(result).toEqual([]);
    });

    it('should return empty array and remove expired set', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      const testData = new Map([
        ['expiredSet', { value: JSON.stringify(['user1']), expireAt: now - 1000 }]
      ]);
      (service as any).data = testData;
      mockFs.writeFile.mockResolvedValue(undefined);

      const result = await service.smembers('expiredSet');

      expect(result).toEqual([]);
      expect(testData.has('expiredSet')).toBe(false);

      jest.useRealTimers();
    });
  });

  describe('过期清理机制', () => {
    it('should start expiration cleanup on initialization', () => {
      jest.useFakeTimers();
      
      // 创建新的服务实例来测试定时器
      const newService = new FileRedisService();
      
      // 验证定时器被设置 - 检查是否有setInterval调用
      expect(mockSetInterval).toHaveBeenCalled();
      
      jest.useRealTimers();
    });

    it('should clean expired keys during cleanup', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      const testData = new Map([
        ['validKey', { value: 'validValue', expireAt: now + 3600 * 1000 }],
        ['expiredKey1', { value: 'expiredValue1', expireAt: now - 1000 }],
        ['expiredKey2', { value: 'expiredValue2', expireAt: now - 2000 }],
        ['permanentKey', { value: 'permanentValue' }]
      ]);
      (service as any).data = testData;
      mockFs.writeFile.mockResolvedValue(undefined);

      // 手动调用清理方法
      (service as any).cleanExpiredKeys();

      expect(testData.has('validKey')).toBe(true);
      expect(testData.has('permanentKey')).toBe(true);
      expect(testData.has('expiredKey1')).toBe(false);
      expect(testData.has('expiredKey2')).toBe(false);

      jest.useRealTimers();
    });
  });

  describe('边界情况测试', () => {
    beforeEach(() => {
      mockFs.writeFile.mockResolvedValue(undefined);
    });

    it('should handle empty string key', async () => {
      await service.set('', 'value');

      const testData = (service as any).data;
      expect(testData.has('')).toBe(true);
    });

    it('should handle empty string value', async () => {
      await service.set('key', '');

      const testData = (service as any).data;
      expect(testData.get('key').value).toBe('');
    });

    it('should handle very large TTL values', async () => {
      jest.useFakeTimers();
      const now = Date.now();
      jest.setSystemTime(now);

      await service.set('key', 'value', 2147483647); // Max 32-bit integer

      const testData = (service as any).data;
      expect(testData.get('key').expireAt).toBe(now + 2147483647 * 1000);

      jest.useRealTimers();
    });

    it('should handle negative TTL values', async () => {
      await service.set('key', 'value', -1);

      const testData = (service as any).data;
      expect(testData.get('key').expireAt).toBeUndefined();
    });

    it('should handle JSON parsing errors during data loading', async () => {
      mockFs.readFile.mockResolvedValue('invalid json');

      // 创建新的服务实例来测试数据加载
      const newService = new FileRedisService();
      await new Promise(resolve => setTimeout(resolve, 10));

      // 应该初始化为空数据而不是抛出异常
      expect((newService as any).data.size).toBe(0);
    });

    it('should handle file read errors during data loading', async () => {
      mockFs.readFile.mockRejectedValue(new Error('File not found'));

      // 创建新的服务实例来测试数据加载
      const newService = new FileRedisService();
      await new Promise(resolve => setTimeout(resolve, 10));

      // 应该初始化为空数据而不是抛出异常
      expect((newService as any).data.size).toBe(0);
    });
  });
});