/**
 * 聊天会话管理服务测试
 * 
 * 测试范围：
 * - 会话创建和销毁
 * - 位置更新和地图切换
 * - 上下文注入和Stream/Topic映射
 * - 过期会话清理
 * 
 * @author moyin
 * @version 1.0.0
 * @since 2026-01-14
 * @lastModified 2026-01-14
 */

import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@nestjs/common';
import { ChatSessionService } from './chat_session.service';

describe('ChatSessionService', () => {
  let service: ChatSessionService;
  let redisService: any;
  let configManager: any;

  beforeEach(async () => {
    const mockRedisService = {
      get: jest.fn(),
      setex: jest.fn(),
      del: jest.fn(),
      sadd: jest.fn(),
      srem: jest.fn(),
      smembers: jest.fn(),
      expire: jest.fn(),
    };

    const mockConfigManager = {
      getStreamByMap: jest.fn(),
      findNearbyObject: jest.fn(),
      getAllMapIds: jest.fn(),
    };

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

    service = module.get<ChatSessionService>(ChatSessionService);
    redisService = module.get('REDIS_SERVICE');
    configManager = module.get('ZULIP_CONFIG_SERVICE');

    // 禁用日志输出
    jest.spyOn(Logger.prototype, 'log').mockImplementation();
    jest.spyOn(Logger.prototype, 'error').mockImplementation();
  });

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

  describe('初始化', () => {
    it('应该成功创建服务实例', () => {
      expect(service).toBeDefined();
    });
  });

  describe('createSession', () => {
    const socketId = 'socket_123';
    const userId = 'user_123';
    const zulipQueueId = 'queue_123';
    const username = 'testuser';

    beforeEach(() => {
      redisService.get.mockResolvedValue(null);
      redisService.setex.mockResolvedValue('OK');
      redisService.sadd.mockResolvedValue(1);
      redisService.expire.mockResolvedValue(1);
    });

    it('应该成功创建会话', async () => {
      const session = await service.createSession(socketId, userId, zulipQueueId, username);

      expect(session).toBeDefined();
      expect(session.socketId).toBe(socketId);
      expect(session.userId).toBe(userId);
      expect(session.username).toBe(username);
      expect(session.zulipQueueId).toBe(zulipQueueId);
      expect(redisService.setex).toHaveBeenCalled();
    });

    it('应该使用默认地图和位置', async () => {
      const session = await service.createSession(socketId, userId, zulipQueueId);

      expect(session.currentMap).toBe('novice_village');
      expect(session.position).toEqual({ x: 400, y: 300 });
    });

    it('应该使用提供的初始地图和位置', async () => {
      const initialMap = 'whale_port';
      const initialPosition = { x: 500, y: 400 };

      const session = await service.createSession(
        socketId,
        userId,
        zulipQueueId,
        username,
        initialMap,
        initialPosition
      );

      expect(session.currentMap).toBe(initialMap);
      expect(session.position).toEqual(initialPosition);
    });

    it('应该拒绝空socketId', async () => {
      await expect(service.createSession('', userId, zulipQueueId)).rejects.toThrow('参数不能为空');
    });

    it('应该拒绝空userId', async () => {
      await expect(service.createSession(socketId, '', zulipQueueId)).rejects.toThrow('参数不能为空');
    });

    it('应该拒绝空zulipQueueId', async () => {
      await expect(service.createSession(socketId, userId, '')).rejects.toThrow('参数不能为空');
    });

    it('应该清理旧会话', async () => {
      const oldSocketId = 'old_socket_123';
      redisService.get.mockResolvedValueOnce(oldSocketId);
      redisService.get.mockResolvedValueOnce(JSON.stringify({
        socketId: oldSocketId,
        userId,
        username,
        zulipQueueId,
        currentMap: 'novice_village',
        position: { x: 400, y: 300 },
        lastActivity: new Date().toISOString(),
        createdAt: new Date().toISOString(),
      }));

      await service.createSession(socketId, userId, zulipQueueId, username);

      expect(redisService.del).toHaveBeenCalled();
    });

    it('应该添加到地图玩家列表', async () => {
      await service.createSession(socketId, userId, zulipQueueId, username);

      expect(redisService.sadd).toHaveBeenCalledWith(
        expect.stringContaining('chat:map_players:'),
        socketId
      );
    });

    it('应该生成默认用户名', async () => {
      const session = await service.createSession(socketId, userId, zulipQueueId);

      expect(session.username).toBe(`user_${userId}`);
    });
  });

  describe('getSession', () => {
    const socketId = 'socket_123';
    const mockSessionData = {
      socketId,
      userId: 'user_123',
      username: 'testuser',
      zulipQueueId: 'queue_123',
      currentMap: 'whale_port',
      position: { x: 400, y: 300 },
      lastActivity: new Date().toISOString(),
      createdAt: new Date().toISOString(),
    };

    it('应该返回会话信息', async () => {
      redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
      redisService.setex.mockResolvedValue('OK');

      const session = await service.getSession(socketId);

      expect(session).toBeDefined();
      expect(session?.socketId).toBe(socketId);
      expect(session?.userId).toBe(mockSessionData.userId);
    });

    it('应该更新最后活动时间', async () => {
      redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
      redisService.setex.mockResolvedValue('OK');

      await service.getSession(socketId);

      expect(redisService.setex).toHaveBeenCalled();
    });

    it('应该处理会话不存在', async () => {
      redisService.get.mockResolvedValue(null);

      const session = await service.getSession(socketId);

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

    it('应该拒绝空socketId', async () => {
      const session = await service.getSession('');

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

    it('应该处理Redis错误', async () => {
      redisService.get.mockRejectedValue(new Error('Redis error'));

      const session = await service.getSession(socketId);

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

  describe('injectContext', () => {
    const socketId = 'socket_123';
    const mockSessionData = {
      socketId,
      userId: 'user_123',
      username: 'testuser',
      zulipQueueId: 'queue_123',
      currentMap: 'whale_port',
      position: { x: 400, y: 300 },
      lastActivity: new Date().toISOString(),
      createdAt: new Date().toISOString(),
    };

    beforeEach(() => {
      redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
      redisService.setex.mockResolvedValue('OK');
      configManager.getStreamByMap.mockReturnValue('Whale Port');
      configManager.findNearbyObject.mockReturnValue(null);
    });

    it('应该返回正确的Stream', async () => {
      const context = await service.injectContext(socketId);

      expect(context.stream).toBe('Whale Port');
    });

    it('应该使用默认Topic', async () => {
      const context = await service.injectContext(socketId);

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

    it('应该根据附近对象设置Topic', async () => {
      configManager.findNearbyObject.mockReturnValue({
        zulipTopic: 'Tavern',
      });

      const context = await service.injectContext(socketId);

      expect(context.topic).toBe('Tavern');
    });

    it('应该支持指定地图ID', async () => {
      configManager.getStreamByMap.mockReturnValue('Market');

      const context = await service.injectContext(socketId, 'market');

      expect(configManager.getStreamByMap).toHaveBeenCalledWith('market');
    });

    it('应该处理会话不存在', async () => {
      redisService.get.mockResolvedValue(null);

      const context = await service.injectContext(socketId);

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

    it('应该处理地图没有对应Stream', async () => {
      configManager.getStreamByMap.mockReturnValue(null);

      const context = await service.injectContext(socketId);

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

  describe('getSocketsInMap', () => {
    const mapId = 'whale_port';

    it('应该返回地图中的所有Socket', async () => {
      const sockets = ['socket_1', 'socket_2', 'socket_3'];
      redisService.smembers.mockResolvedValue(sockets);

      const result = await service.getSocketsInMap(mapId);

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

    it('应该处理空地图', async () => {
      redisService.smembers.mockResolvedValue([]);

      const result = await service.getSocketsInMap(mapId);

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

    it('应该处理Redis错误', async () => {
      redisService.smembers.mockRejectedValue(new Error('Redis error'));

      const result = await service.getSocketsInMap(mapId);

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

  describe('updatePlayerPosition', () => {
    const socketId = 'socket_123';
    const mapId = 'whale_port';
    const x = 500;
    const y = 400;
    const mockSessionData = {
      socketId,
      userId: 'user_123',
      username: 'testuser',
      zulipQueueId: 'queue_123',
      currentMap: 'novice_village',
      position: { x: 400, y: 300 },
      lastActivity: new Date().toISOString(),
      createdAt: new Date().toISOString(),
    };

    beforeEach(() => {
      redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
      redisService.setex.mockResolvedValue('OK');
      redisService.srem.mockResolvedValue(1);
      redisService.sadd.mockResolvedValue(1);
      redisService.expire.mockResolvedValue(1);
    });

    it('应该成功更新位置', async () => {
      const result = await service.updatePlayerPosition(socketId, mapId, x, y);

      expect(result).toBe(true);
      expect(redisService.setex).toHaveBeenCalled();
    });

    it('应该更新地图玩家列表当切换地图', async () => {
      await service.updatePlayerPosition(socketId, mapId, x, y);

      expect(redisService.srem).toHaveBeenCalled();
      expect(redisService.sadd).toHaveBeenCalled();
    });

    it('应该不更新地图玩家列表当在同一地图', async () => {
      const sameMapData = { ...mockSessionData, currentMap: mapId };
      redisService.get.mockResolvedValue(JSON.stringify(sameMapData));

      await service.updatePlayerPosition(socketId, mapId, x, y);

      expect(redisService.srem).not.toHaveBeenCalled();
    });

    it('应该拒绝空socketId', async () => {
      const result = await service.updatePlayerPosition('', mapId, x, y);

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

    it('应该拒绝空mapId', async () => {
      const result = await service.updatePlayerPosition(socketId, '', x, y);

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

    it('应该处理会话不存在', async () => {
      redisService.get.mockResolvedValue(null);

      const result = await service.updatePlayerPosition(socketId, mapId, x, y);

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

    it('应该处理Redis错误', async () => {
      redisService.get.mockRejectedValue(new Error('Redis error'));

      const result = await service.updatePlayerPosition(socketId, mapId, x, y);

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

  describe('destroySession', () => {
    const socketId = 'socket_123';
    const mockSessionData = {
      socketId,
      userId: 'user_123',
      username: 'testuser',
      zulipQueueId: 'queue_123',
      currentMap: 'whale_port',
      position: { x: 400, y: 300 },
      lastActivity: new Date().toISOString(),
      createdAt: new Date().toISOString(),
    };

    beforeEach(() => {
      redisService.get.mockResolvedValue(JSON.stringify(mockSessionData));
      redisService.srem.mockResolvedValue(1);
      redisService.del.mockResolvedValue(1);
    });

    it('应该成功销毁会话', async () => {
      const result = await service.destroySession(socketId);

      expect(result).toBe(true);
      expect(redisService.del).toHaveBeenCalledTimes(2);
    });

    it('应该从地图玩家列表移除', async () => {
      await service.destroySession(socketId);

      expect(redisService.srem).toHaveBeenCalled();
    });

    it('应该删除用户会话映射', async () => {
      await service.destroySession(socketId);

      expect(redisService.del).toHaveBeenCalledWith(
        expect.stringContaining('chat:user_session:')
      );
    });

    it('应该处理会话不存在', async () => {
      redisService.get.mockResolvedValue(null);

      const result = await service.destroySession(socketId);

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

    it('应该拒绝空socketId', async () => {
      const result = await service.destroySession('');

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

    it('应该处理Redis错误', async () => {
      redisService.get.mockRejectedValue(new Error('Redis error'));

      const result = await service.destroySession(socketId);

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

  describe('cleanupExpiredSessions', () => {
    beforeEach(() => {
      configManager.getAllMapIds.mockReturnValue(['novice_village', 'whale_port']);
    });

    it('应该清理过期会话', async () => {
      const expiredSession = {
        socketId: 'socket_123',
        userId: 'user_123',
        username: 'testuser',
        zulipQueueId: 'queue_123',
        currentMap: 'whale_port',
        position: { x: 400, y: 300 },
        lastActivity: new Date(Date.now() - 60 * 60 * 1000).toISOString(),
        createdAt: new Date().toISOString(),
      };

      redisService.smembers.mockResolvedValue(['socket_123']);
      redisService.get.mockResolvedValueOnce(JSON.stringify(expiredSession));
      redisService.get.mockResolvedValueOnce(JSON.stringify(expiredSession));
      redisService.srem.mockResolvedValue(1);
      redisService.del.mockResolvedValue(1);

      const result = await service.cleanupExpiredSessions(30);

      expect(result.cleanedCount).toBeGreaterThanOrEqual(1);
      expect(result.zulipQueueIds).toContain('queue_123');
    });

    it('应该不清理未过期会话', async () => {
      const activeSession = {
        socketId: 'socket_123',
        userId: 'user_123',
        username: 'testuser',
        zulipQueueId: 'queue_123',
        currentMap: 'whale_port',
        position: { x: 400, y: 300 },
        lastActivity: new Date().toISOString(),
        createdAt: new Date().toISOString(),
      };

      redisService.smembers.mockResolvedValue(['socket_123']);
      redisService.get.mockResolvedValue(JSON.stringify(activeSession));

      const result = await service.cleanupExpiredSessions(30);

      expect(result.cleanedCount).toBe(0);
    });

    it('应该处理多个地图', async () => {
      redisService.smembers.mockResolvedValue([]);

      const result = await service.cleanupExpiredSessions(30);

      expect(redisService.smembers).toHaveBeenCalledTimes(2);
      expect(result.cleanedCount).toBe(0);
    });

    it('应该使用默认地图当配置为空', async () => {
      configManager.getAllMapIds.mockReturnValue([]);
      redisService.smembers.mockResolvedValue([]);

      const result = await service.cleanupExpiredSessions(30);

      expect(result.cleanedCount).toBe(0);
    });

    it('应该处理清理过程中的错误', async () => {
      redisService.smembers.mockRejectedValue(new Error('Redis error'));

      const result = await service.cleanupExpiredSessions(30);

      expect(result.cleanedCount).toBe(0);
      expect(result.zulipQueueIds).toEqual([]);
    });

    it('应该清理不存在的会话数据', async () => {
      redisService.smembers.mockResolvedValue(['socket_123']);
      redisService.get.mockResolvedValue(null);
      redisService.srem.mockResolvedValue(1);

      const result = await service.cleanupExpiredSessions(30);

      expect(redisService.srem).toHaveBeenCalled();
    });
  });

  describe('边界情况', () => {
    it('应该处理极大的坐标值', async () => {
      const socketId = 'socket_123';
      const userId = 'user_123';
      const zulipQueueId = 'queue_123';
      
      redisService.get.mockResolvedValue(null);
      redisService.setex.mockResolvedValue('OK');
      redisService.sadd.mockResolvedValue(1);
      redisService.expire.mockResolvedValue(1);

      const session = await service.createSession(
        socketId,
        userId,
        zulipQueueId,
        'testuser',
        'whale_port',
        { x: 999999, y: 999999 }
      );

      expect(session.position).toEqual({ x: 999999, y: 999999 });
    });

    it('应该处理负坐标值', async () => {
      const socketId = 'socket_123';
      const userId = 'user_123';
      const zulipQueueId = 'queue_123';
      
      redisService.get.mockResolvedValue(null);
      redisService.setex.mockResolvedValue('OK');
      redisService.sadd.mockResolvedValue(1);
      redisService.expire.mockResolvedValue(1);

      const session = await service.createSession(
        socketId,
        userId,
        zulipQueueId,
        'testuser',
        'whale_port',
        { x: -100, y: -100 }
      );

      expect(session.position).toEqual({ x: -100, y: -100 });
    });

    it('应该处理特殊字符的用户名', async () => {
      const socketId = 'socket_123';
      const userId = 'user_123';
      const zulipQueueId = 'queue_123';
      const username = 'test@user#123';
      
      redisService.get.mockResolvedValue(null);
      redisService.setex.mockResolvedValue('OK');
      redisService.sadd.mockResolvedValue(1);
      redisService.expire.mockResolvedValue(1);

      const session = await service.createSession(socketId, userId, zulipQueueId, username);

      expect(session.username).toBe(username);
    });
  });
});
