/**
 * 会话管理服务测试
 * 
 * 功能描述：
 * - 测试SessionManagerService的核心功能
 * - 包含属性测试验证会话状态一致性
 * 
 * @author angjustinl
 * @version 1.0.0
 * @since 2025-12-25
 */

import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
import { SessionManagerService, GameSession, Position } from './session_manager.service';
import { IZulipConfigService } from '../../../core/zulip_core/zulip_core.interfaces';
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
import { IRedisService } from '../../../core/redis/redis.interface';

describe('SessionManagerService', () => {
  let service: SessionManagerService;
  let mockLogger: jest.Mocked<AppLoggerService>;
  let mockRedisService: jest.Mocked<IRedisService>;
  let mockConfigManager: jest.Mocked<IZulipConfigService>;

  // 内存存储模拟Redis
  let memoryStore: Map<string, { value: string; expireAt?: number }>;
  let memorySets: Map<string, Set<string>>;

  beforeEach(async () => {
    jest.clearAllMocks();
    
    // 初始化内存存储
    memoryStore = new Map();
    memorySets = new Map();

    mockLogger = {
      info: jest.fn(),
      warn: jest.fn(),
      error: jest.fn(),
      debug: jest.fn(),
    } as any;

    mockConfigManager = {
      getStreamByMap: jest.fn().mockImplementation((mapId: string) => {
        const streamMap: Record<string, string> = {
          'whale_port': 'Whale Port',
          'pumpkin_valley': 'Pumpkin Valley',
          'offer_city': 'Offer City',
          'model_factory': 'Model Factory',
          'kernel_island': 'Kernel Island',
          'moyu_beach': 'Moyu Beach',
          'ladder_peak': 'Ladder Peak',
          'galaxy_bay': 'Galaxy Bay',
          'data_ruins': 'Data Ruins',
          'novice_village': 'Novice Village',
        };
        return streamMap[mapId] || 'General';
      }),
      getMapIdByStream: jest.fn(),
      getTopicByObject: jest.fn().mockReturnValue('General'),
      findNearbyObject: jest.fn().mockReturnValue(null),
      getZulipConfig: jest.fn(),
      hasMap: jest.fn(),
      hasStream: jest.fn(),
      getAllMapIds: jest.fn(),
      getAllStreams: jest.fn(),
      reloadConfig: jest.fn(),
      validateConfig: jest.fn(),
    } as any;

    // 创建模拟Redis服务，使用内存存储
    mockRedisService = {
      set: jest.fn().mockImplementation(async (key: string, value: string, ttl?: number) => {
        memoryStore.set(key, { 
          value, 
          expireAt: ttl ? Date.now() + ttl * 1000 : undefined 
        });
      }),
      setex: jest.fn().mockImplementation(async (key: string, ttl: number, value: string) => {
        memoryStore.set(key, { 
          value, 
          expireAt: Date.now() + ttl * 1000 
        });
      }),
      get: jest.fn().mockImplementation(async (key: string) => {
        const item = memoryStore.get(key);
        if (!item) return null;
        if (item.expireAt && item.expireAt <= Date.now()) {
          memoryStore.delete(key);
          return null;
        }
        return item.value;
      }),
      del: jest.fn().mockImplementation(async (key: string) => {
        const existed = memoryStore.has(key);
        memoryStore.delete(key);
        return existed;
      }),
      exists: jest.fn().mockImplementation(async (key: string) => {
        return memoryStore.has(key);
      }),
      expire: jest.fn().mockImplementation(async (key: string, ttl: number) => {
        const item = memoryStore.get(key);
        if (item) {
          item.expireAt = Date.now() + ttl * 1000;
        }
      }),
      ttl: jest.fn().mockResolvedValue(3600),
      incr: jest.fn().mockResolvedValue(1),
      sadd: jest.fn().mockImplementation(async (key: string, member: string) => {
        if (!memorySets.has(key)) {
          memorySets.set(key, new Set());
        }
        memorySets.get(key)!.add(member);
      }),
      srem: jest.fn().mockImplementation(async (key: string, member: string) => {
        const set = memorySets.get(key);
        if (set) {
          set.delete(member);
        }
      }),
      smembers: jest.fn().mockImplementation(async (key: string) => {
        const set = memorySets.get(key);
        return set ? Array.from(set) : [];
      }),
      flushall: jest.fn().mockImplementation(async () => {
        memoryStore.clear();
        memorySets.clear();
      }),
    } as any;

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        SessionManagerService,
        {
          provide: AppLoggerService,
          useValue: mockLogger,
        },
        {
          provide: 'REDIS_SERVICE',
          useValue: mockRedisService,
        },
        {
          provide: 'ZULIP_CONFIG_SERVICE',
          useValue: mockConfigManager,
        },
      ],
    }).compile();

    service = module.get<SessionManagerService>(SessionManagerService);
  });

  afterEach(async () => {
    // 清理内存存储
    memoryStore.clear();
    memorySets.clear();
    
    // 等待任何正在进行的异步操作完成
    await new Promise(resolve => setImmediate(resolve));
  });

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

  describe('createSession - 创建会话', () => {
    it('应该成功创建新会话', async () => {
      const session = await service.createSession(
        'socket-123',
        'user-456',
        'queue-789',
        'TestUser',
      );

      expect(session).toBeDefined();
      expect(session.socketId).toBe('socket-123');
      expect(session.userId).toBe('user-456');
      expect(session.zulipQueueId).toBe('queue-789');
      expect(session.username).toBe('TestUser');
      expect(session.currentMap).toBe('novice_village');
    });

    it('应该在socketId为空时抛出错误', async () => {
      await expect(service.createSession('', 'user-456', 'queue-789'))
        .rejects.toThrow('socketId不能为空');
    });

    it('应该在userId为空时抛出错误', async () => {
      await expect(service.createSession('socket-123', '', 'queue-789'))
        .rejects.toThrow('userId不能为空');
    });

    it('应该在zulipQueueId为空时抛出错误', async () => {
      await expect(service.createSession('socket-123', 'user-456', ''))
        .rejects.toThrow('zulipQueueId不能为空');
    });

    it('应该清理用户已有的旧会话', async () => {
      // 创建第一个会话
      await service.createSession('socket-old', 'user-456', 'queue-old');
      
      // 创建第二个会话（同一用户）
      const newSession = await service.createSession('socket-new', 'user-456', 'queue-new');

      expect(newSession.socketId).toBe('socket-new');
      
      // 旧会话应该被清理
      const oldSession = await service.getSession('socket-old');
      expect(oldSession).toBeNull();
    });
  });

  describe('getSession - 获取会话', () => {
    it('应该返回已存在的会话', async () => {
      await service.createSession('socket-123', 'user-456', 'queue-789');
      
      const session = await service.getSession('socket-123');

      expect(session).toBeDefined();
      expect(session?.socketId).toBe('socket-123');
    });

    it('应该在会话不存在时返回null', async () => {
      const session = await service.getSession('nonexistent');

      expect(session).toBeNull();
    });

    it('应该在socketId为空时返回null', async () => {
      const session = await service.getSession('');

      expect(session).toBeNull();
    });
  });

  describe('getSessionByUserId - 根据用户ID获取会话', () => {
    it('应该返回用户的会话', async () => {
      await service.createSession('socket-123', 'user-456', 'queue-789');
      
      const session = await service.getSessionByUserId('user-456');

      expect(session).toBeDefined();
      expect(session?.userId).toBe('user-456');
    });

    it('应该在用户没有会话时返回null', async () => {
      const session = await service.getSessionByUserId('nonexistent');

      expect(session).toBeNull();
    });
  });

  describe('updatePlayerPosition - 更新玩家位置', () => {
    it('应该成功更新位置', async () => {
      await service.createSession('socket-123', 'user-456', 'queue-789');
      
      const result = await service.updatePlayerPosition('socket-123', 'novice_village', 100, 200);

      expect(result).toBe(true);
      
      const session = await service.getSession('socket-123');
      expect(session?.position).toEqual({ x: 100, y: 200 });
    });

    it('应该在切换地图时更新地图玩家列表', async () => {
      await service.createSession('socket-123', 'user-456', 'queue-789');
      
      const result = await service.updatePlayerPosition('socket-123', 'tavern', 150, 250);

      expect(result).toBe(true);
      
      const session = await service.getSession('socket-123');
      expect(session?.currentMap).toBe('tavern');
      
      // 验证地图玩家列表更新
      const tavernPlayers = await service.getSocketsInMap('tavern');
      expect(tavernPlayers).toContain('socket-123');
      
      const villagePlayers = await service.getSocketsInMap('novice_village');
      expect(villagePlayers).not.toContain('socket-123');
    });

    it('应该在会话不存在时返回false', async () => {
      const result = await service.updatePlayerPosition('nonexistent', 'tavern', 100, 200);

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

  describe('destroySession - 销毁会话', () => {
    it('应该成功销毁会话', async () => {
      await service.createSession('socket-123', 'user-456', 'queue-789');
      
      const result = await service.destroySession('socket-123');

      expect(result).toBe(true);
      
      const session = await service.getSession('socket-123');
      expect(session).toBeNull();
    });

    it('应该在会话不存在时返回true', async () => {
      const result = await service.destroySession('nonexistent');

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

    it('应该清理用户会话映射', async () => {
      await service.createSession('socket-123', 'user-456', 'queue-789');
      await service.destroySession('socket-123');
      
      const session = await service.getSessionByUserId('user-456');
      expect(session).toBeNull();
    });
  });

  describe('getSocketsInMap - 获取地图玩家列表', () => {
    it('应该返回地图中的所有玩家', async () => {
      await service.createSession('socket-1', 'user-1', 'queue-1');
      await service.createSession('socket-2', 'user-2', 'queue-2');
      
      const sockets = await service.getSocketsInMap('novice_village');

      expect(sockets).toHaveLength(2);
      expect(sockets).toContain('socket-1');
      expect(sockets).toContain('socket-2');
    });

    it('应该在地图为空时返回空数组', async () => {
      const sockets = await service.getSocketsInMap('empty_map');

      expect(sockets).toHaveLength(0);
    });
  });

  describe('injectContext - 上下文注入', () => {
    it('应该返回正确的Stream', async () => {
      await service.createSession('socket-123', 'user-456', 'queue-789');
      
      const context = await service.injectContext('socket-123');

      expect(context.stream).toBe('Novice Village');
    });

    it('应该在会话不存在时返回默认上下文', async () => {
      const context = await service.injectContext('nonexistent');

      expect(context.stream).toBe('General');
    });
  });


  /**
   * 属性测试: 会话状态一致性
   * 
   * **Feature: zulip-integration, Property 6: 会话状态一致性**
   * **Validates: Requirements 6.1, 6.2, 6.3, 6.5**
   * 
   * 对于任何玩家会话，系统应该在Redis中正确维护WebSocket ID与Zulip队列ID的映射关系，
   * 及时更新位置信息，并支持服务重启后的状态恢复
   */
  describe('Property 6: 会话状态一致性', () => {
    /**
     * 属性: 对于任何有效的会话参数，创建会话后应该能够正确获取
     * 验证需求 6.1: 玩家登录成功后系统应在Redis中存储WebSocket ID与Zulip队列ID的映射关系
     */
    it('对于任何有效的会话参数，创建会话后应该能够正确获取', async () => {
      await fc.assert(
        fc.asyncProperty(
          // 生成有效的socketId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的userId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的zulipQueueId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的username
          fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0),
          async (socketId, userId, zulipQueueId, username) => {
            // 清理之前的数据
            memoryStore.clear();
            memorySets.clear();

            // 创建会话
            const createdSession = await service.createSession(
              socketId.trim(),
              userId.trim(),
              zulipQueueId.trim(),
              username.trim(),
            );

            // 验证创建的会话
            expect(createdSession.socketId).toBe(socketId.trim());
            expect(createdSession.userId).toBe(userId.trim());
            expect(createdSession.zulipQueueId).toBe(zulipQueueId.trim());
            expect(createdSession.username).toBe(username.trim());

            // 获取会话并验证一致性
            const retrievedSession = await service.getSession(socketId.trim());
            expect(retrievedSession).not.toBeNull();
            expect(retrievedSession?.socketId).toBe(createdSession.socketId);
            expect(retrievedSession?.userId).toBe(createdSession.userId);
            expect(retrievedSession?.zulipQueueId).toBe(createdSession.zulipQueueId);
          }
        ),
        { numRuns: 50, timeout: 5000 } // 添加超时控制
      );
    }, 30000);

    /**
     * 属性: 对于任何位置更新，会话应该正确反映新位置
     * 验证需求 6.2: 玩家切换地图时系统应更新玩家的当前位置信息
     */
    it('对于任何位置更新，会话应该正确反映新位置', async () => {
      await fc.assert(
        fc.asyncProperty(
          // 生成有效的socketId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的userId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的地图ID
          fc.constantFrom('novice_village', 'tavern', 'market'),
          // 生成有效的坐标
          fc.integer({ min: 0, max: 1000 }),
          fc.integer({ min: 0, max: 1000 }),
          async (socketId, userId, mapId, x, y) => {
            // 清理之前的数据
            memoryStore.clear();
            memorySets.clear();

            // 创建会话
            await service.createSession(
              socketId.trim(),
              userId.trim(),
              'queue-test',
            );

            // 更新位置
            const updateResult = await service.updatePlayerPosition(
              socketId.trim(),
              mapId,
              x,
              y,
            );

            expect(updateResult).toBe(true);

            // 验证位置更新
            const session = await service.getSession(socketId.trim());
            expect(session).not.toBeNull();
            expect(session?.currentMap).toBe(mapId);
            expect(session?.position.x).toBe(x);
            expect(session?.position.y).toBe(y);
          }
        ),
        { numRuns: 50, timeout: 5000 } // 添加超时控制
      );
    }, 30000);

    /**
     * 属性: 对于任何地图切换，玩家应该从旧地图移除并添加到新地图
     * 验证需求 6.3: 查询在线玩家时系统应从Redis中获取当前活跃的会话列表
     */
    it('对于任何地图切换，玩家应该从旧地图移除并添加到新地图', async () => {
      await fc.assert(
        fc.asyncProperty(
          // 生成有效的socketId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的userId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成初始地图和目标地图（确保不同）
          fc.constantFrom('novice_village', 'tavern', 'market'),
          fc.constantFrom('novice_village', 'tavern', 'market'),
          async (socketId, userId, initialMap, targetMap) => {
            // 清理之前的数据
            memoryStore.clear();
            memorySets.clear();

            // 创建会话（使用初始地图）
            await service.createSession(
              socketId.trim(),
              userId.trim(),
              'queue-test',
              'TestUser',
              initialMap,
            );

            // 验证初始地图包含玩家
            const initialPlayers = await service.getSocketsInMap(initialMap);
            expect(initialPlayers).toContain(socketId.trim());

            // 如果目标地图不同，切换地图
            if (initialMap !== targetMap) {
              await service.updatePlayerPosition(socketId.trim(), targetMap, 100, 100);

              // 验证旧地图不再包含玩家
              const oldMapPlayers = await service.getSocketsInMap(initialMap);
              expect(oldMapPlayers).not.toContain(socketId.trim());

              // 验证新地图包含玩家
              const newMapPlayers = await service.getSocketsInMap(targetMap);
              expect(newMapPlayers).toContain(socketId.trim());
            }
          }
        ),
        { numRuns: 50, timeout: 5000 } // 添加超时控制
      );
    }, 30000);

    /**
     * 属性: 对于任何会话销毁，所有相关数据应该被清理
     * 验证需求 6.5: 服务器重启时系统应能够从Redis中恢复会话状态（通过验证销毁后数据被正确清理）
     */
    it('对于任何会话销毁，所有相关数据应该被清理', async () => {
      await fc.assert(
        fc.asyncProperty(
          // 生成有效的socketId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的userId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的地图ID
          fc.constantFrom('novice_village', 'tavern', 'market'),
          async (socketId, userId, mapId) => {
            // 清理之前的数据
            memoryStore.clear();
            memorySets.clear();

            // 创建会话
            await service.createSession(
              socketId.trim(),
              userId.trim(),
              'queue-test',
              'TestUser',
              mapId,
            );

            // 验证会话存在
            const sessionBefore = await service.getSession(socketId.trim());
            expect(sessionBefore).not.toBeNull();

            // 销毁会话
            const destroyResult = await service.destroySession(socketId.trim());
            expect(destroyResult).toBe(true);

            // 验证会话被清理
            const sessionAfter = await service.getSession(socketId.trim());
            expect(sessionAfter).toBeNull();

            // 验证用户会话映射被清理
            const userSession = await service.getSessionByUserId(userId.trim());
            expect(userSession).toBeNull();

            // 验证地图玩家列表被清理
            const mapPlayers = await service.getSocketsInMap(mapId);
            expect(mapPlayers).not.toContain(socketId.trim());
          }
        ),
        { numRuns: 50, timeout: 5000 } // 添加超时控制
      );
    }, 30000);

    /**
     * 属性: 创建-更新-销毁的完整生命周期应该正确管理会话状态
     */
    it('创建-更新-销毁的完整生命周期应该正确管理会话状态', async () => {
      await fc.assert(
        fc.asyncProperty(
          // 生成有效的socketId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成有效的userId
          fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
          // 生成位置更新序列
          fc.array(
            fc.record({
              mapId: fc.constantFrom('novice_village', 'tavern', 'market'),
              x: fc.integer({ min: 0, max: 1000 }),
              y: fc.integer({ min: 0, max: 1000 }),
            }),
            { minLength: 1, maxLength: 5 }
          ),
          async (socketId, userId, positionUpdates) => {
            // 清理之前的数据
            memoryStore.clear();
            memorySets.clear();

            // 1. 创建会话
            const session = await service.createSession(
              socketId.trim(),
              userId.trim(),
              'queue-test',
            );
            expect(session).toBeDefined();

            // 2. 执行位置更新序列
            for (const update of positionUpdates) {
              const result = await service.updatePlayerPosition(
                socketId.trim(),
                update.mapId,
                update.x,
                update.y,
              );
              expect(result).toBe(true);

              // 验证每次更新后的状态
              const currentSession = await service.getSession(socketId.trim());
              expect(currentSession?.currentMap).toBe(update.mapId);
              expect(currentSession?.position.x).toBe(update.x);
              expect(currentSession?.position.y).toBe(update.y);
            }

            // 3. 销毁会话
            const destroyResult = await service.destroySession(socketId.trim());
            expect(destroyResult).toBe(true);

            // 4. 验证所有数据被清理
            const finalSession = await service.getSession(socketId.trim());
            expect(finalSession).toBeNull();
          }
        ),
        { numRuns: 50, timeout: 5000 } // 添加超时控制
      );
    }, 30000);
  });
});
