import { Test, TestingModule } from '@nestjs/testing';
import { RecordHeartbeatHandler } from '../../../../src/application/commands/record-heartbeat/record-heartbeat.handler';
import { RecordHeartbeatCommand } from '../../../../src/application/commands/record-heartbeat/record-heartbeat.command';
import { PresenceRedisRepository } from '../../../../src/infrastructure/redis/presence-redis.repository';
import { EventPublisherService } from '../../../../src/infrastructure/kafka/event-publisher.service';
import { MetricsService } from '../../../../src/infrastructure/metrics/metrics.service';
import { HeartbeatReceivedEvent } from '../../../../src/domain/events/heartbeat-received.event';

describe('RecordHeartbeatHandler', () => {
  let handler: RecordHeartbeatHandler;
  let presenceRedisRepository: jest.Mocked<PresenceRedisRepository>;
  let eventPublisher: jest.Mocked<EventPublisherService>;

  beforeEach(async () => {
    const mockPresenceRedisRepository = {
      updateUserPresence: jest.fn(),
      countOnlineUsers: jest.fn(),
      getOnlineUsers: jest.fn(),
      removeExpiredUsers: jest.fn(),
    };

    const mockEventPublisher = {
      publish: jest.fn(),
    };

    const mockMetricsService = {
      recordHeartbeat: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        RecordHeartbeatHandler,
        {
          provide: PresenceRedisRepository,
          useValue: mockPresenceRedisRepository,
        },
        {
          provide: EventPublisherService,
          useValue: mockEventPublisher,
        },
        {
          provide: MetricsService,
          useValue: mockMetricsService,
        },
      ],
    }).compile();

    handler = module.get<RecordHeartbeatHandler>(RecordHeartbeatHandler);
    presenceRedisRepository = module.get(PresenceRedisRepository);
    eventPublisher = module.get(EventPublisherService);
  });

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

  describe('execute', () => {
    it('should record heartbeat and return success', async () => {
      const command = new RecordHeartbeatCommand(
        'D25121400005',  // userSerialNum
        'install-id-123',
        '1.0.0',
        Date.now(),
      );

      presenceRedisRepository.updateUserPresence.mockResolvedValue(undefined);
      eventPublisher.publish.mockResolvedValue(undefined);

      const result = await handler.execute(command);

      expect(result.ok).toBe(true);
      expect(result.serverTs).toBeDefined();
      expect(typeof result.serverTs).toBe('number');
    });

    it('should update Redis presence with correct userId (userSerialNum)', async () => {
      const userId = 'D25121499999';  // userSerialNum
      const command = new RecordHeartbeatCommand(
        userId,
        'install-id-456',
        '2.0.0',
        Date.now(),
      );

      presenceRedisRepository.updateUserPresence.mockResolvedValue(undefined);
      eventPublisher.publish.mockResolvedValue(undefined);

      await handler.execute(command);

      expect(presenceRedisRepository.updateUserPresence).toHaveBeenCalledWith(
        userId,  // userSerialNum is passed directly as string
        expect.any(Number),
      );
    });

    it('should publish HeartbeatReceivedEvent', async () => {
      const command = new RecordHeartbeatCommand(
        'D25121400005',
        'install-id-789',
        '1.0.0',
        Date.now(),
      );

      presenceRedisRepository.updateUserPresence.mockResolvedValue(undefined);
      eventPublisher.publish.mockResolvedValue(undefined);

      await handler.execute(command);

      expect(eventPublisher.publish).toHaveBeenCalledWith(
        HeartbeatReceivedEvent.EVENT_NAME,
        expect.any(HeartbeatReceivedEvent),
      );
    });

    it('should return server timestamp close to current time', async () => {
      const command = new RecordHeartbeatCommand(
        'D25121400005',
        'install-id',
        '1.0.0',
        Date.now(),
      );

      presenceRedisRepository.updateUserPresence.mockResolvedValue(undefined);
      eventPublisher.publish.mockResolvedValue(undefined);

      const beforeExecute = Math.floor(Date.now() / 1000);
      const result = await handler.execute(command);
      const afterExecute = Math.floor(Date.now() / 1000);

      expect(result.serverTs).toBeGreaterThanOrEqual(beforeExecute);
      expect(result.serverTs).toBeLessThanOrEqual(afterExecute);
    });

    it('should throw error when Redis update fails', async () => {
      const command = new RecordHeartbeatCommand(
        'D25121400005',
        'install-id',
        '1.0.0',
        Date.now(),
      );

      presenceRedisRepository.updateUserPresence.mockRejectedValue(
        new Error('Redis connection failed'),
      );

      await expect(handler.execute(command)).rejects.toThrow('Redis connection failed');
    });

    it('should throw error when event publish fails', async () => {
      const command = new RecordHeartbeatCommand(
        'D25121400005',
        'install-id',
        '1.0.0',
        Date.now(),
      );

      presenceRedisRepository.updateUserPresence.mockResolvedValue(undefined);
      eventPublisher.publish.mockRejectedValue(new Error('Kafka unavailable'));

      await expect(handler.execute(command)).rejects.toThrow('Kafka unavailable');
    });
  });
});
