import { describe, expect, it, jest } from '@jest/globals';
import { getWorkerHeartbeatKey, WorkerHeartbeat } from '../../workerHeartbeat';

function createRedis() {
  return {
    zadd: jest.fn<() => Promise<number>>().mockResolvedValue(1),
    zrem: jest.fn<() => Promise<number>>().mockResolvedValue(1),
    ping: jest.fn<() => Promise<string>>().mockResolvedValue('PONG'),
    set: jest.fn<() => Promise<'OK'>>().mockResolvedValue('OK'),
    del: jest.fn<() => Promise<number>>().mockResolvedValue(1),
    quit: jest.fn<() => Promise<'OK'>>().mockResolvedValue('OK'),
  };
}

describe('WorkerHeartbeat', () => {
  it('writes a short-TTL heartbeat only after event-loop, PostgreSQL, and Redis checks succeed', async () => {
    const redis = createRedis();
    const checkEventLoop = jest.fn<() => Promise<void>>().mockResolvedValue(undefined);
    const checkQueueReady = jest.fn<() => Promise<unknown>>().mockResolvedValue({});
    const checkDatabase = jest.fn<() => Promise<unknown>>().mockResolvedValue([{ result: 1 }]);
    const heartbeat = new WorkerHeartbeat({
      redis,
      checkEventLoop,
      checkQueueReady,
      checkDatabase,
      key: 'worker:health:test',
      ttlSeconds: 15,
      now: () => 1234,
    });

    await heartbeat.pulse();

    expect(checkEventLoop).toHaveBeenCalledTimes(1);
    expect(checkQueueReady).toHaveBeenCalledTimes(1);
    expect(checkDatabase).toHaveBeenCalledTimes(1);
    expect(redis.ping).toHaveBeenCalledTimes(1);
    expect(redis.set).toHaveBeenCalledWith('worker:health:test', '1234', 'EX', 15);
    expect(redis.zadd).toHaveBeenCalledWith('worker:heartbeats', 1234, 'test');
    expect(checkEventLoop.mock.invocationCallOrder[0]).toBeLessThan(checkQueueReady.mock.invocationCallOrder[0]);
    expect(checkQueueReady.mock.invocationCallOrder[0]).toBeLessThan(checkDatabase.mock.invocationCallOrder[0]);
    expect(checkDatabase.mock.invocationCallOrder[0]).toBeLessThan(redis.ping.mock.invocationCallOrder[0]);
    expect(redis.ping.mock.invocationCallOrder[0]).toBeLessThan(redis.set.mock.invocationCallOrder[0]);
  });

  it('does not refresh the heartbeat when PostgreSQL fails', async () => {
    const redis = createRedis();
    const heartbeat = new WorkerHeartbeat({
      redis,
      checkDatabase: jest.fn<() => Promise<unknown>>().mockRejectedValue(new Error('DB unavailable')),
      key: 'worker:health:test',
    });

    await expect(heartbeat.pulse()).rejects.toThrow('DB unavailable');
    expect(redis.set).not.toHaveBeenCalled();
  });

  it('does not refresh the heartbeat when Redis fails', async () => {
    const redis = createRedis();
    redis.ping.mockRejectedValueOnce(new Error('Redis unavailable'));
    const heartbeat = new WorkerHeartbeat({
      redis,
      checkDatabase: jest.fn<() => Promise<unknown>>().mockResolvedValue([{ result: 1 }]),
      key: 'worker:health:test',
    });

    await expect(heartbeat.pulse()).rejects.toThrow('Redis unavailable');
    expect(redis.set).not.toHaveBeenCalled();
  });

  it('does not refresh the heartbeat until configuration Redis clients are ready', async () => {
    const redis = createRedis();
    const checkRuntimeReady = jest.fn<() => Promise<void>>()
      .mockRejectedValueOnce(new Error('Config subscription is not ready'))
      .mockResolvedValue(undefined);
    const heartbeat = new WorkerHeartbeat({
      redis,
      checkRuntimeReady,
      checkDatabase: jest.fn<() => Promise<unknown>>().mockResolvedValue([{ result: 1 }]),
      key: 'worker:health:test',
    });

    await expect(heartbeat.pulse()).rejects.toThrow('Config subscription is not ready');
    expect(redis.set).not.toHaveBeenCalled();
    await expect(heartbeat.pulse()).resolves.toBeUndefined();
    expect(redis.set).toHaveBeenCalledTimes(1);
  });

  it('does not refresh the heartbeat when Bull is not ready', async () => {
    const redis = createRedis();
    const heartbeat = new WorkerHeartbeat({
      redis,
      checkQueueReady: jest.fn<() => Promise<unknown>>().mockRejectedValue(new Error('Queue unavailable')),
      checkDatabase: jest.fn<() => Promise<unknown>>().mockResolvedValue([{ result: 1 }]),
      key: 'worker:health:test',
    });

    await expect(heartbeat.pulse()).rejects.toThrow('Queue unavailable');
    expect(redis.set).not.toHaveBeenCalled();
  });

  it('uses WORKER_ID when no container hostname is inherited', () => {
    const originalWorkerId = process.env.WORKER_ID;
    const originalHostname = process.env.HOSTNAME;
    process.env.WORKER_ID = 'presszone-worker';
    delete process.env.HOSTNAME;

    try {
      expect(getWorkerHeartbeatKey()).toBe('worker:health:presszone-worker');
    } finally {
      if (originalWorkerId === undefined) {
        delete process.env.WORKER_ID;
      } else {
        process.env.WORKER_ID = originalWorkerId;
      }
      if (originalHostname === undefined) {
        delete process.env.HOSTNAME;
      } else {
        process.env.HOSTNAME = originalHostname;
      }
    }
  });

  it('removes only its own heartbeat during two-worker shutdown', async () => {
    const firstRedis = createRedis();
    const secondRedis = createRedis();
    const first = new WorkerHeartbeat({
      redis: firstRedis,
      checkDatabase: jest.fn<() => Promise<unknown>>().mockResolvedValue([]),
      key: getWorkerHeartbeatKey('worker-a'),
    });
    const second = new WorkerHeartbeat({
      redis: secondRedis,
      checkDatabase: jest.fn<() => Promise<unknown>>().mockResolvedValue([]),
      key: getWorkerHeartbeatKey('worker-b'),
    });

    await first.stop();

    expect(firstRedis.del).toHaveBeenCalledWith('worker:health:worker-a');
    expect(firstRedis.zrem).toHaveBeenCalledWith('worker:heartbeats', 'worker-a');
    expect(secondRedis.del).not.toHaveBeenCalled();
    expect(getWorkerHeartbeatKey('worker-a')).not.toBe(getWorkerHeartbeatKey('worker-b'));
    await second.stop();
  });

  it('removes the heartbeat and closes Redis on shutdown', async () => {
    const redis = createRedis();
    const heartbeat = new WorkerHeartbeat({
      redis,
      checkDatabase: jest.fn<() => Promise<unknown>>().mockResolvedValue([]),
      key: 'worker:health:test',
    });

    await heartbeat.stop();

    expect(redis.del).toHaveBeenCalledWith('worker:health:test');
    expect(redis.quit).toHaveBeenCalledTimes(1);
  });
});
