import { WorkerHeartbeatService, WorkerStatus } from './worker-heartbeat.service';
import { RedisService } from '../infrastructure/redis/redis.service';

describe('WorkerHeartbeatService', () => {
  let service: WorkerHeartbeatService;
  let mockQ: any;
  let mockRedis: jest.Mocked<Pick<RedisService, 'set' | 'get' | 'del' | 'keys' | 'ttl'>>;

  beforeEach(() => {
    mockRedis = {
      set: jest.fn().mockResolvedValue(undefined),
      get: jest.fn().mockResolvedValue(null),
      del: jest.fn().mockResolvedValue(undefined),
      keys: jest.fn().mockResolvedValue([]),
      ttl: jest.fn().mockResolvedValue(-2),
    };

    process.env.WORKER_INSTANCE_ID = 'test-worker-1';
    mockQ = { getActiveCount: jest.fn().mockResolvedValue(0) };
    service = new WorkerHeartbeatService(mockRedis as any, mockQ as any, mockQ as any);
  });

  afterEach(async () => {
    await service.onModuleDestroy().catch(() => {});
    delete process.env.WORKER_INSTANCE_ID;
    jest.useRealTimers();
  });

  describe('onModuleInit', () => {
    it('writes heartbeat immediately and starts interval', async () => {
      jest.useFakeTimers();
      await service.onModuleInit();
      expect(mockRedis.set).toHaveBeenCalledTimes(1);
      expect(mockRedis.set).toHaveBeenCalledWith(
        'worker:heartbeat:test-worker-1',
        expect.any(String),
        90,
      );

      // Parse the heartbeat data
      const data = JSON.parse((mockRedis.set as jest.Mock).mock.calls[0][1]);
      expect(data.instanceId).toBe('test-worker-1');
      expect(data.queues).toBeInstanceOf(Array);
      expect(data.queues.length).toBeGreaterThanOrEqual(1);
      expect(data.hostname).toBeDefined();
      expect(data.appVersion).toBeDefined();
      expect(data.startedAt).toBeDefined();
    });

    it('writes heartbeat periodically', async () => {
      jest.useFakeTimers();
      await service.onModuleInit();
      // First beat during init
      expect(mockRedis.set).toHaveBeenCalledTimes(1);

      // Advance timer and flush async queue (beat() awaits getActiveCount)
      jest.advanceTimersByTime(35_000);
      await Promise.resolve();
      await Promise.resolve(); // double flush for async getActiveCount() chain
      // Second beat from interval — at least one call expected
      expect(mockRedis.set).toHaveBeenCalledTimes(2);

      jest.advanceTimersByTime(35_000);
      await Promise.resolve();
      await Promise.resolve(); // flush async getActiveCount() chain
      // At least 2 calls: init beat + interval beat (may be 3 depending on timing)
      expect(mockRedis.set.mock.calls.length).toBeGreaterThanOrEqual(2);
    });

    it('uses instanceId from WORKER_INSTANCE_ID env var', async () => {
      process.env.WORKER_INSTANCE_ID = 'custom-instance';
      const svc = new WorkerHeartbeatService(mockRedis as any, mockQ as any, mockQ as any);
      await svc.onModuleInit();
      expect(mockRedis.set).toHaveBeenCalledWith(
        'worker:heartbeat:custom-instance',
        expect.any(String),
        90,
      );
      await svc.onModuleDestroy();
    });
  });

  describe('onModuleDestroy', () => {
    it('stops interval and deletes heartbeat key', async () => {
      jest.useFakeTimers();
      await service.onModuleInit();
      expect(mockRedis.set).toHaveBeenCalledTimes(1);

      await service.onModuleDestroy();

      // Advance time — no more beats
      jest.advanceTimersByTime(30_000);
      await Promise.resolve();
      expect(mockRedis.set).toHaveBeenCalledTimes(1);

      // Key deleted
      expect(mockRedis.del).toHaveBeenCalledWith('worker:heartbeat:test-worker-1');
    });
  });

  describe('computeStatuses', () => {
    it('returns empty array when no heartbeat keys', async () => {
      (mockRedis.keys as jest.Mock).mockResolvedValue([]);
      const result = await WorkerHeartbeatService.computeStatuses(mockRedis as any);
      expect(result).toEqual([]);
    });

    it('returns online when TTL > 30', async () => {
      (mockRedis.keys as jest.Mock).mockResolvedValue(['worker:heartbeat:w1']);
      (mockRedis.get as jest.Mock).mockResolvedValue(JSON.stringify({
        instanceId: 'w1', appVersion: '0.0.1', startedAt: new Date().toISOString(),
        queues: ['ai-analysis'], hostname: 'host1',
      }));
      (mockRedis.ttl as jest.Mock).mockResolvedValue(60);

      const result = await WorkerHeartbeatService.computeStatuses(mockRedis as any);
      expect(result[0].status).toBe('online');
    });

    it('returns stale when TTL between 1 and 30', async () => {
      (mockRedis.keys as jest.Mock).mockResolvedValue(['worker:heartbeat:w1']);
      (mockRedis.get as jest.Mock).mockResolvedValue(JSON.stringify({
        instanceId: 'w1', appVersion: '0.0.1', startedAt: new Date().toISOString(),
        queues: ['ai-analysis'], hostname: 'host1',
      }));
      (mockRedis.ttl as jest.Mock).mockResolvedValue(15);

      const result = await WorkerHeartbeatService.computeStatuses(mockRedis as any);
      expect(result[0].status).toBe('stale');
    });

    it('returns offline when TTL < 0', async () => {
      (mockRedis.keys as jest.Mock).mockResolvedValue(['worker:heartbeat:w1']);
      (mockRedis.get as jest.Mock).mockResolvedValue(JSON.stringify({
        instanceId: 'w1', appVersion: '0.0.1', startedAt: new Date().toISOString(),
        queues: ['ai-analysis'], hostname: 'host1',
      }));
      (mockRedis.ttl as jest.Mock).mockResolvedValue(-1);

      const result = await WorkerHeartbeatService.computeStatuses(mockRedis as any);
      expect(result[0].status).toBe('offline');
    });

    it('handles multiple worker instances', async () => {
      (mockRedis.keys as jest.Mock).mockResolvedValue(['worker:heartbeat:w1', 'worker:heartbeat:w2']);
      (mockRedis.get as jest.Mock)
        .mockResolvedValueOnce(JSON.stringify({ instanceId: 'w1', appVersion: '0.0.1', startedAt: new Date().toISOString(), queues: ['ai-analysis'], hostname: 'host1' }))
        .mockResolvedValueOnce(JSON.stringify({ instanceId: 'w2', appVersion: '0.0.1', startedAt: new Date().toISOString(), queues: ['ai-analysis'], hostname: 'host2' }));
      (mockRedis.ttl as jest.Mock)
        .mockResolvedValueOnce(60)
        .mockResolvedValueOnce(10);

      const result = await WorkerHeartbeatService.computeStatuses(mockRedis as any);
      expect(result).toHaveLength(2);
      expect(result[0].status).toBe('online');
      expect(result[1].status).toBe('stale');
    });

    it('skips malformed JSON data', async () => {
      (mockRedis.keys as jest.Mock).mockResolvedValue(['worker:heartbeat:w1']);
      (mockRedis.get as jest.Mock).mockResolvedValue('not-json');
      const result = await WorkerHeartbeatService.computeStatuses(mockRedis as any);
      expect(result).toEqual([]);
    });
  });

  describe('API heartbeat key ≠ Worker heartbeat key', () => {
    it('API process does not have WorkerHeartbeatService in its DI graph', () => {
      // WorkerHeartbeatService is only registered in WorkerModule,
      // not in AppModule. This is verified by the module configuration:
      // - AppModule does not import WorkerHeartbeatService
      // - WorkerModule imports and provides WorkerHeartbeatService
      // This test documents the architectural constraint.
      expect(WorkerHeartbeatService).toBeDefined();
    });
  });
});
