/**
 * 位置广播核心服务单元测试
 * 
 * 功能描述：
 * - 测试位置广播核心服务的所有功能
 * - 验证会话管理和位置数据操作
 * - 确保错误处理和边界条件正确
 * - 提供完整的测试覆盖率
 * 
 * 测试范围：
 * - 用户会话管理（加入/离开）
 * - 位置数据操作（设置/获取）
 * - 数据清理和维护功能
 * - 异常情况处理
 * 
 * @author moyin
 * @version 1.0.0
 * @since 2026-01-08
 * @lastModified 2026-01-08
 */

import { Test, TestingModule } from '@nestjs/testing';
import { LocationBroadcastCore } from './location_broadcast_core.service';
import { Position } from './position.interface';
import { SessionUserStatus } from './session.interface';

describe('LocationBroadcastCore', () => {
  let service: LocationBroadcastCore;
  let mockRedisService: any;
  let mockUserProfilesService: any;

  beforeEach(async () => {
    // 创建Redis服务的Mock
    mockRedisService = {
      sadd: jest.fn(),
      setex: jest.fn(),
      expire: jest.fn(),
      srem: jest.fn(),
      del: jest.fn(),
      get: jest.fn(),
      smembers: jest.fn(),
      scard: jest.fn(),
    };

    // 创建用户档案服务的Mock
    mockUserProfilesService = {
      findOne: jest.fn(),
      update: jest.fn(),
    };

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

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

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

  describe('addUserToSession', () => {
    it('应该成功添加用户到会话', async () => {
      // Arrange
      const sessionId = 'test-session';
      const userId = 'test-user';
      const socketId = 'test-socket';

      mockRedisService.sadd.mockResolvedValue(1);
      mockRedisService.setex.mockResolvedValue('OK');
      mockRedisService.expire.mockResolvedValue(1);

      // Act
      await service.addUserToSession(sessionId, userId, socketId);

      // Assert
      expect(mockRedisService.sadd).toHaveBeenCalledWith(
        `session:${sessionId}:users`,
        userId
      );
      expect(mockRedisService.setex).toHaveBeenCalledWith(
        `user:${userId}:session`,
        3600,
        sessionId
      );
      expect(mockRedisService.setex).toHaveBeenCalledWith(
        `user:${userId}:socket`,
        3600,
        socketId
      );
      expect(mockRedisService.setex).toHaveBeenCalledWith(
        `socket:${socketId}:user`,
        3600,
        userId
      );
    });

    it('应该处理Redis操作失败的情况', async () => {
      // Arrange
      const sessionId = 'test-session';
      const userId = 'test-user';
      const socketId = 'test-socket';

      mockRedisService.sadd.mockRejectedValue(new Error('Redis连接失败'));

      // Act & Assert
      await expect(
        service.addUserToSession(sessionId, userId, socketId)
      ).rejects.toThrow('Redis连接失败');
    });
  });

  describe('removeUserFromSession', () => {
    it('应该成功从会话中移除用户', async () => {
      // Arrange
      const sessionId = 'test-session';
      const userId = 'test-user';
      const socketId = 'test-socket';

      mockRedisService.srem.mockResolvedValue(1);
      mockRedisService.get.mockResolvedValue(socketId);
      mockRedisService.del.mockResolvedValue(1);
      mockRedisService.scard.mockResolvedValue(0);

      // Act
      await service.removeUserFromSession(sessionId, userId);

      // Assert
      expect(mockRedisService.srem).toHaveBeenCalledWith(
        `session:${sessionId}:users`,
        userId
      );
      expect(mockRedisService.del).toHaveBeenCalledWith(`user:${userId}:session`);
      expect(mockRedisService.del).toHaveBeenCalledWith(`user:${userId}:socket`);
      expect(mockRedisService.del).toHaveBeenCalledWith(`socket:${socketId}:user`);
    });

    it('应该在会话为空时清理会话', async () => {
      // Arrange
      const sessionId = 'test-session';
      const userId = 'test-user';

      mockRedisService.srem.mockResolvedValue(1);
      mockRedisService.get.mockResolvedValue(null);
      mockRedisService.del.mockResolvedValue(1);
      mockRedisService.scard.mockResolvedValue(0); // 会话为空

      const cleanupEmptySessionSpy = jest.spyOn(service, 'cleanupEmptySession');
      cleanupEmptySessionSpy.mockResolvedValue();

      // Act
      await service.removeUserFromSession(sessionId, userId);

      // Assert
      expect(cleanupEmptySessionSpy).toHaveBeenCalledWith(sessionId);
    });
  });

  describe('getSessionUsers', () => {
    it('应该返回会话中的用户列表', async () => {
      // Arrange
      const sessionId = 'test-session';
      const userIds = ['user1', 'user2'];
      const socketId1 = 'socket1';
      const socketId2 = 'socket2';

      mockRedisService.smembers.mockResolvedValue(userIds);
      mockRedisService.get
        .mockResolvedValueOnce(socketId1) // user1的socket
        .mockResolvedValueOnce(socketId2); // user2的socket

      // Mock getUserPosition方法
      const getUserPositionSpy = jest.spyOn(service, 'getUserPosition');
      getUserPositionSpy
        .mockResolvedValueOnce({
          userId: 'user1',
          x: 100,
          y: 200,
          mapId: 'plaza',
          timestamp: Date.now(),
          metadata: {}
        })
        .mockResolvedValueOnce(null); // user2没有位置

      // Act
      const result = await service.getSessionUsers(sessionId);

      // Assert
      expect(result).toHaveLength(2);
      expect(result[0]).toMatchObject({
        userId: 'user1',
        socketId: socketId1,
        status: SessionUserStatus.ONLINE
      });
      expect(result[0].position).toBeDefined();
      expect(result[1]).toMatchObject({
        userId: 'user2',
        socketId: socketId2,
        status: SessionUserStatus.ONLINE
      });
      expect(result[1].position).toBeNull();
    });

    it('应该在会话不存在时返回空数组', async () => {
      // Arrange
      const sessionId = 'non-existent-session';
      mockRedisService.smembers.mockResolvedValue([]);

      // Act
      const result = await service.getSessionUsers(sessionId);

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

    it('应该处理获取用户信息失败的情况', async () => {
      // Arrange
      const sessionId = 'test-session';
      const userIds = ['user1', 'user2'];

      mockRedisService.smembers.mockResolvedValue(userIds);
      mockRedisService.get.mockRejectedValue(new Error('Redis错误'));

      // Act
      const result = await service.getSessionUsers(sessionId);

      // Assert
      expect(result).toEqual([]); // 应该返回空数组而不是抛出异常
    });
  });

  describe('setUserPosition', () => {
    it('应该成功设置用户位置', async () => {
      // Arrange
      const userId = 'test-user';
      const position: Position = {
        userId,
        x: 100,
        y: 200,
        mapId: 'plaza',
        timestamp: Date.now(),
        metadata: {}
      };
      const sessionId = 'test-session';

      mockRedisService.get.mockResolvedValue(null); // No previous position data
      mockRedisService.setex.mockResolvedValue('OK');
      mockRedisService.sadd.mockResolvedValue(1);
      mockRedisService.expire.mockResolvedValue(1);
      mockRedisService.srem.mockResolvedValue(1);

      // Act
      await service.setUserPosition(userId, position);

      // Assert
      expect(mockRedisService.setex).toHaveBeenCalledWith(
        `location:user:${userId}`,
        1800,
        expect.stringContaining('"x":100')
      );
      expect(mockRedisService.sadd).toHaveBeenCalledWith(
        `map:${position.mapId}:users`,
        userId
      );
    });

    it('应该处理用户地图切换', async () => {
      // Arrange
      const userId = 'test-user';
      const newPosition: Position = {
        userId,
        x: 100,
        y: 200,
        mapId: 'forest',
        timestamp: Date.now(),
        metadata: {}
      };
      const oldPosition = {
        x: 50,
        y: 100,
        mapId: 'plaza',
        timestamp: Date.now() - 1000
      };

      mockRedisService.get
        .mockResolvedValueOnce('test-session') // 当前会话
        .mockResolvedValueOnce(JSON.stringify(oldPosition)); // 旧位置
      mockRedisService.setex.mockResolvedValue('OK');
      mockRedisService.sadd.mockResolvedValue(1);
      mockRedisService.expire.mockResolvedValue(1);
      mockRedisService.srem.mockResolvedValue(1);

      // Act
      await service.setUserPosition(userId, newPosition);

      // Assert
      expect(mockRedisService.srem).toHaveBeenCalledWith(
        `map:${oldPosition.mapId}:users`,
        userId
      );
      expect(mockRedisService.sadd).toHaveBeenCalledWith(
        `map:${newPosition.mapId}:users`,
        userId
      );
    });
  });

  describe('getUserPosition', () => {
    it('应该成功获取用户位置', async () => {
      // Arrange
      const userId = 'test-user';
      const positionData = {
        x: 100,
        y: 200,
        mapId: 'plaza',
        timestamp: Date.now(),
        metadata: {}
      };

      mockRedisService.get.mockResolvedValue(JSON.stringify(positionData));

      // Act
      const result = await service.getUserPosition(userId);

      // Assert
      expect(result).toMatchObject({
        userId,
        x: positionData.x,
        y: positionData.y,
        mapId: positionData.mapId,
        timestamp: positionData.timestamp
      });
    });

    it('应该在用户位置不存在时返回null', async () => {
      // Arrange
      const userId = 'non-existent-user';
      mockRedisService.get.mockResolvedValue(null);

      // Act
      const result = await service.getUserPosition(userId);

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

    it('应该处理JSON解析错误', async () => {
      // Arrange
      const userId = 'test-user';
      mockRedisService.get.mockResolvedValue('invalid-json');

      // Act
      const result = await service.getUserPosition(userId);

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

  describe('getSessionPositions', () => {
    it('应该返回会话中所有用户的位置', async () => {
      // Arrange
      const sessionId = 'test-session';
      const userIds = ['user1', 'user2'];
      const position1: Position = {
        userId: 'user1',
        x: 100,
        y: 200,
        mapId: 'plaza',
        timestamp: Date.now(),
        metadata: {}
      };

      mockRedisService.smembers.mockResolvedValue(userIds);
      
      const getUserPositionSpy = jest.spyOn(service, 'getUserPosition');
      getUserPositionSpy
        .mockResolvedValueOnce(position1)
        .mockResolvedValueOnce(null); // user2没有位置

      // Act
      const result = await service.getSessionPositions(sessionId);

      // Assert
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual(position1);
    });
  });

  describe('getMapPositions', () => {
    it('应该返回地图中所有用户的位置', async () => {
      // Arrange
      const mapId = 'plaza';
      const userIds = ['user1', 'user2'];
      const position1: Position = {
        userId: 'user1',
        x: 100,
        y: 200,
        mapId: 'plaza',
        timestamp: Date.now(),
        metadata: {}
      };

      mockRedisService.smembers.mockResolvedValue(userIds);
      
      const getUserPositionSpy = jest.spyOn(service, 'getUserPosition');
      getUserPositionSpy
        .mockResolvedValueOnce(position1)
        .mockResolvedValueOnce({
          userId: 'user2',
          x: 150,
          y: 250,
          mapId: 'forest', // 不同地图，应该被过滤
          timestamp: Date.now(),
          metadata: {}
        });

      // Act
      const result = await service.getMapPositions(mapId);

      // Assert
      expect(result).toHaveLength(1);
      expect(result[0]).toEqual(position1);
    });
  });

  describe('cleanupUserData', () => {
    it('应该成功清理用户数据', async () => {
      // Arrange
      const userId = 'test-user';
      const sessionId = 'test-session';
      const socketId = 'test-socket';

      mockRedisService.get
        .mockResolvedValueOnce(sessionId)
        .mockResolvedValueOnce(socketId);
      mockRedisService.del.mockResolvedValue(1);

      const removeUserFromSessionSpy = jest.spyOn(service, 'removeUserFromSession');
      removeUserFromSessionSpy.mockResolvedValue();

      // Act
      await service.cleanupUserData(userId);

      // Assert
      expect(removeUserFromSessionSpy).toHaveBeenCalledWith(sessionId, userId);
      // removeUserFromSession 内部会调用 del 方法，所以总调用次数会更多
      expect(mockRedisService.del).toHaveBeenCalled();
    });

    it('应该处理用户不在会话中的情况', async () => {
      // Arrange
      const userId = 'test-user';

      mockRedisService.get.mockResolvedValue(null); // 用户不在任何会话中
      mockRedisService.del.mockResolvedValue(1);

      // Act & Assert
      await expect(service.cleanupUserData(userId)).resolves.not.toThrow();
    });
  });

  describe('cleanupEmptySession', () => {
    it('应该清理空会话', async () => {
      // Arrange
      const sessionId = 'empty-session';
      mockRedisService.scard.mockResolvedValue(0); // 会话为空
      mockRedisService.del.mockResolvedValue(1);

      // Act
      await service.cleanupEmptySession(sessionId);

      // Assert
      expect(mockRedisService.del).toHaveBeenCalledTimes(4); // 4个会话相关键被删除
    });

    it('应该跳过非空会话的清理', async () => {
      // Arrange
      const sessionId = 'non-empty-session';
      mockRedisService.scard.mockResolvedValue(2); // 会话不为空

      // Act
      await service.cleanupEmptySession(sessionId);

      // Assert
      expect(mockRedisService.del).not.toHaveBeenCalled();
    });
  });

  describe('cleanupExpiredData', () => {
    it('应该返回清理的记录数', async () => {
      // Arrange
      const expireTime = new Date();

      // Act
      const result = await service.cleanupExpiredData(expireTime);

      // Assert
      expect(typeof result).toBe('number');
      expect(result).toBeGreaterThanOrEqual(0);
    });
  });

  describe('错误处理', () => {
    it('应该在Redis连接失败时记录错误', async () => {
      // Arrange
      const sessionId = 'test-session';
      const userId = 'test-user';
      const socketId = 'test-socket';

      mockRedisService.sadd.mockRejectedValue(new Error('连接超时'));

      // Act & Assert
      await expect(
        service.addUserToSession(sessionId, userId, socketId)
      ).rejects.toThrow('连接超时');
    });

    it('应该处理并发操作冲突', async () => {
      // Arrange
      const userId = 'test-user';
      const position: Position = {
        userId,
        x: 100,
        y: 200,
        mapId: 'plaza',
        timestamp: Date.now(),
        metadata: {}
      };

      // 模拟并发冲突
      mockRedisService.get.mockResolvedValue('test-session');
      mockRedisService.setex.mockRejectedValueOnce(new Error('并发冲突'));
      mockRedisService.setex.mockResolvedValue('OK');

      // Act & Assert
      await expect(service.setUserPosition(userId, position)).rejects.toThrow('并发冲突');
    });
  });

  describe('边界条件测试', () => {
    it('应该处理空字符串参数', async () => {
      // 这些测试应该通过，因为服务没有对空字符串进行验证
      // 实际的验证应该在业务层进行
      
      // Act & Assert - 这些操作应该成功，因为Core层专注技术实现
      await expect(service.addUserToSession('', 'user', 'socket')).resolves.not.toThrow();
      await expect(service.addUserToSession('session', '', 'socket')).resolves.not.toThrow();
      await expect(service.addUserToSession('session', 'user', '')).resolves.not.toThrow();
    });

    it('应该处理极大的坐标值', async () => {
      // Arrange
      const userId = '123'; // 使用数字字符串
      const position: Position = {
        userId,
        x: Number.MAX_SAFE_INTEGER,
        y: Number.MAX_SAFE_INTEGER,
        mapId: 'plaza',
        timestamp: Date.now(),
        metadata: {}
      };

      mockRedisService.get.mockResolvedValue(null); // No previous position data
      mockRedisService.setex.mockResolvedValue('OK');
      mockRedisService.sadd.mockResolvedValue(1);
      mockRedisService.expire.mockResolvedValue(1);
      mockRedisService.srem.mockResolvedValue(1);

      // Act & Assert
      await expect(service.setUserPosition(userId, position)).resolves.not.toThrow();
    });

    it('应该处理大量用户的会话', async () => {
      // Arrange
      const sessionId = 'large-session';
      const userIds = Array.from({ length: 1000 }, (_, i) => `user${i}`);

      mockRedisService.smembers.mockResolvedValue(userIds);
      mockRedisService.get.mockResolvedValue('socket');
      
      const getUserPositionSpy = jest.spyOn(service, 'getUserPosition');
      getUserPositionSpy.mockResolvedValue(null);

      // Act
      const result = await service.getSessionUsers(sessionId);

      // Assert
      expect(result).toHaveLength(1000);
    });
  });
});