import { ConfigService } from '@nestjs/config';
import Redis from 'ioredis';
import { RedisService } from './redis.service';

jest.mock('ioredis', () => ({ __esModule: true, default: jest.fn() }));

describe('RedisService', () => {
  const values: Record<string, unknown> = {};
  const config = { get: jest.fn((key: string) => values[key]) } as unknown as ConfigService;
  const clients: Array<{ duplicate: jest.Mock; quit: jest.Mock; disconnect: jest.Mock }> = [];

  beforeEach(() => {
    jest.clearAllMocks();
    Object.keys(values).forEach((key) => delete values[key]);
    clients.length = 0;
    jest.mocked(Redis).mockImplementation(() => {
      const duplicateClient = { quit: jest.fn(), disconnect: jest.fn() };
      const client = {
        duplicate: jest.fn(() => duplicateClient),
        quit: jest.fn().mockResolvedValue('OK'),
        disconnect: jest.fn(),
      };
      clients.push(client);
      return client as never;
    });
  });

  it('stays inert when disabled and exposes default prefix', () => {
    const service = new RedisService(config);
    expect(service.isEnabled()).toBe(false);
    expect(service.getKeyPrefix()).toBe('oudelaa');
    expect(service.getClient()).toBeNull();
    expect(service.createPubSubClients()).toBeNull();
    expect(service.createQueueClient()).toBeNull();
    expect(Redis).not.toHaveBeenCalled();
  });

  it('creates one shared host-based client and independent queue/pubsub clients', () => {
    Object.assign(values, {
      'redis.enabled': true,
      'redis.keyPrefix': 'test',
      'redis.host': 'redis.internal',
      'redis.port': 6380,
      'redis.username': 'app',
      'redis.password': 'secret',
      'redis.db': 2,
    });
    const service = new RedisService(config);
    expect(service.getKeyPrefix()).toBe('test');
    expect(service.getClient()).toBe(service.getClient());
    expect(Redis).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        host: 'redis.internal', port: 6380, username: 'app', password: 'secret', db: 2,
        maxRetriesPerRequest: null, lazyConnect: false, enableReadyCheck: true,
      }),
    );
    const pair = service.createPubSubClients();
    expect(pair?.pubClient).toBeDefined();
    expect(pair?.subClient).toBeDefined();
    expect(clients[1].duplicate).toHaveBeenCalled();
    expect(service.createQueueClient()).toBeDefined();
  });

  it('prefers a URL and omits host credentials', () => {
    Object.assign(values, { 'redis.enabled': true, 'redis.url': 'rediss://redis.example/1' });
    const service = new RedisService(config);
    service.getClient();
    expect(Redis).toHaveBeenCalledWith('rediss://redis.example/1', {
      maxRetriesPerRequest: null,
      lazyConnect: false,
      enableReadyCheck: true,
    });
  });

  it('quits the shared client and falls back to disconnect on failure', async () => {
    values['redis.enabled'] = true;
    const service = new RedisService(config);
    service.getClient();
    clients[0].quit.mockRejectedValue(new Error('closed'));
    service.onModuleDestroy();
    await new Promise((resolve) => setImmediate(resolve));
    expect(clients[0].disconnect).toHaveBeenCalled();
    expect(service.getClient()).not.toBe(clients[0]);
  });
});
