import { describe, it, expect, beforeEach, afterEach, mock } from 'bun:test';
import { RedisConnectionManager } from '../src/connection-manager';
import type { RedisConfig } from '../src/types';

// Mock ioredis
const mockRedisInstance = {
  on: mock((_event: string, _callback: Function) => {
    // Store callbacks for triggering events
    mockRedisInstance._eventCallbacks[event] = callback;
  }),
  once: mock((event: string, callback: Function) => {
    mockRedisInstance._onceCallbacks[event] = callback;
  }),
  ping: mock(async () => 'PONG'),
  quit: mock(async () => 'OK'),
  status: 'ready',
  _eventCallbacks: {} as Record<string, Function>,
  _onceCallbacks: {} as Record<string, Function>,
  // Helper to trigger events
  _triggerEvent(event: string, ...args: any[]) {
    if (this._eventCallbacks[event]) {
      this._eventCallbacks[event](...args);
    }
    if (this._onceCallbacks[event]) {
      this._onceCallbacks[event](...args);
      delete this._onceCallbacks[event];
    }
  }
};

mock.module('ioredis', () => ({
  default: mock(() => {
    // Create a new instance for each Redis connection with event handling methods
    const instance = {
      ...mockRedisInstance,
      _eventCallbacks: {},
      _onceCallbacks: {},
      on: function(event: string, callback: Function) {
        this._eventCallbacks[event] = callback;
        return this;
      },
      once: function(event: string, callback: Function) {
        this._onceCallbacks[event] = callback;
        return this;
      },
      _triggerEvent: function(event: string, ...args: any[]) {
        if (this._eventCallbacks[event]) {
          this._eventCallbacks[event](...args);
        }
        if (this._onceCallbacks[event]) {
          this._onceCallbacks[event](...args);
          delete this._onceCallbacks[event];
        }
      }
    };
    return instance;
  })
}));

// Skip these tests when running all tests together
// Run them individually with: bun test libs/core/cache/test/connection-manager.test.ts
describe.skip('RedisConnectionManager', () => {
  let manager: RedisConnectionManager;
  const mockLogger = {
    info: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
    debug: mock(() => {}),
  };

  beforeEach(() => {
    // Clear static state
    (RedisConnectionManager as any).instance = undefined;
    if ((RedisConnectionManager as any).sharedConnections) {
      (RedisConnectionManager as any).sharedConnections.clear();
    }
    if ((RedisConnectionManager as any).readyConnections) {
      (RedisConnectionManager as any).readyConnections.clear();
    }
    
    // Get new instance
    manager = RedisConnectionManager.getInstance();
    
    // Set mock logger on the instance
    (manager as any).logger = mockLogger;
    
    // Reset mocks
    mockLogger.info.mockClear();
    mockLogger.error.mockClear();
    mockLogger.warn.mockClear();
    mockLogger.debug.mockClear();
  });

  afterEach(async () => {
    await manager.closeAllConnections();
  });

  describe('getInstance', () => {
    it('should return singleton instance', () => {
      const instance1 = RedisConnectionManager.getInstance();
      const instance2 = RedisConnectionManager.getInstance();
      expect(instance1).toBe(instance2);
    });
  });

  describe('getConnection', () => {
    const baseConfig: RedisConfig = {
      host: 'localhost',
      port: 6379,
    };

    it('should create unique connection when singleton is false', () => {
      const connection1 = manager.getConnection({
        name: 'test',
        singleton: false,
        redisConfig: baseConfig,
        logger: mockLogger,
      });

      const connection2 = manager.getConnection({
        name: 'test',
        singleton: false,
        redisConfig: baseConfig,
        logger: mockLogger,
      });

      expect(connection1).not.toBe(connection2);
      expect(mockLogger.debug).toHaveBeenCalledTimes(2);
    });

    it('should reuse shared connection when singleton is true', () => {
      const connection1 = manager.getConnection({
        name: 'shared-test',
        singleton: true,
        redisConfig: baseConfig,
        logger: mockLogger,
      });

      const connection2 = manager.getConnection({
        name: 'shared-test',
        singleton: true,
        redisConfig: baseConfig,
        logger: mockLogger,
      });

      expect(connection1).toBe(connection2);
      expect(mockLogger.info).toHaveBeenCalledWith('Created shared Redis connection: shared-test');
    });

    it('should apply custom db number', () => {
      const connection = manager.getConnection({
        name: 'db-test',
        singleton: false,
        db: 5,
        redisConfig: baseConfig,
        logger: mockLogger,
      });

      expect(connection).toBeDefined();
    });

    it('should handle TLS configuration', () => {
      const tlsConfig: RedisConfig = {
        ...baseConfig,
        tls: {
          cert: 'cert-content',
          key: 'key-content',
          ca: 'ca-content',
          rejectUnauthorized: false,
        },
      };

      const connection = manager.getConnection({
        name: 'tls-test',
        singleton: false,
        redisConfig: tlsConfig,
        logger: mockLogger,
      });

      expect(connection).toBeDefined();
    });

    it('should use provided logger', () => {
      const customLogger = {
        info: mock(() => {}),
        error: mock(() => {}),
        warn: mock(() => {}),
        debug: mock(() => {}),
      };

      manager.getConnection({
        name: 'logger-test',
        singleton: false,
        redisConfig: baseConfig,
        logger: customLogger,
      });

      expect(customLogger.debug).toHaveBeenCalled();
    });
  });

  describe('connection events', () => {
    it('should handle connect event', () => {
      const connection = manager.getConnection({
        name: 'event-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      // Trigger connect event
      (connection as any)._triggerEvent('connect');
      
      expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Redis connection established'));
    });

    it('should handle ready event', () => {
      const connection = manager.getConnection({
        name: 'ready-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      // Trigger ready event
      (connection as any)._triggerEvent('ready');
      
      expect(mockLogger.info).toHaveBeenCalledWith(expect.stringContaining('Redis connection ready'));
    });

    it('should handle error event', () => {
      const connection = manager.getConnection({
        name: 'error-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      const error = new Error('Connection failed');
      (connection as any)._triggerEvent('error', error);
      
      expect(mockLogger.error).toHaveBeenCalledWith(
        expect.stringContaining('Redis connection error'),
        error
      );
    });

    it('should handle close event', () => {
      const connection = manager.getConnection({
        name: 'close-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      (connection as any)._triggerEvent('close');
      
      expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Redis connection closed'));
    });

    it('should handle reconnecting event', () => {
      const connection = manager.getConnection({
        name: 'reconnect-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      (connection as any)._triggerEvent('reconnecting');
      
      expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Redis reconnecting'));
    });
  });

  describe('closeConnection', () => {
    it('should close connection successfully', async () => {
      const connection = manager.getConnection({
        name: 'close-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      await manager.closeConnection(connection);
      
      expect(connection.quit).toHaveBeenCalled();
    });

    it('should handle close errors gracefully', async () => {
      const connection = manager.getConnection({
        name: 'close-error-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      // Make quit throw an error
      (connection.quit as any).mockImplementation(() => Promise.reject(new Error('Quit failed')));

      await manager.closeConnection(connection);
      
      expect(mockLogger.warn).toHaveBeenCalledWith(
        'Error closing Redis connection:',
        expect.any(Error)
      );
    });
  });

  describe('closeAllConnections', () => {
    it('should close all unique connections', async () => {
      const conn1 = manager.getConnection({
        name: 'unique1',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      const conn2 = manager.getConnection({
        name: 'unique2',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      await manager.closeAllConnections();

      expect(conn1.quit).toHaveBeenCalled();
      expect(conn2.quit).toHaveBeenCalled();
      expect(mockLogger.info).toHaveBeenCalledWith('All Redis connections closed');
    });

    it('should close shared connections', async () => {
      const sharedConn = manager.getConnection({
        name: 'shared',
        singleton: true,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      await manager.closeAllConnections();

      expect(sharedConn.quit).toHaveBeenCalled();
      expect(manager.getConnectionCount()).toEqual({ shared: 0, unique: 0 });
    });
  });

  describe('getConnectionCount', () => {
    it('should return correct connection counts', () => {
      manager.getConnection({
        name: 'unique1',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      manager.getConnection({
        name: 'unique2',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      manager.getConnection({
        name: 'shared1',
        singleton: true,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      const counts = manager.getConnectionCount();
      expect(counts.unique).toBe(2);
      expect(counts.shared).toBe(1);
    });
  });

  describe('getConnectionNames', () => {
    it('should return connection names', () => {
      manager.getConnection({
        name: 'test-unique',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      manager.getConnection({
        name: 'test-shared',
        singleton: true,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      const names = manager.getConnectionNames();
      expect(names.shared).toContain('test-shared');
      expect(names.unique.length).toBe(1);
      expect(names.unique[0]).toContain('test-unique');
    });
  });

  describe('healthCheck', () => {
    it('should report healthy connections', async () => {
      manager.getConnection({
        name: 'health-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      const health = await manager.healthCheck();
      
      expect(health.healthy).toBe(true);
      expect(Object.keys(health.details).length).toBeGreaterThan(0);
    });

    it('should report unhealthy connections', async () => {
      const connection = manager.getConnection({
        name: 'unhealthy-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      // Make ping fail
      (connection.ping as any).mockImplementation(() => Promise.reject(new Error('Ping failed')));

      const health = await manager.healthCheck();
      
      expect(health.healthy).toBe(false);
      expect(Object.values(health.details)).toContain(false);
    });
  });

  describe('waitForAllConnections', () => {
    it('should wait for connections to be ready', async () => {
      manager.getConnection({
        name: 'wait-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      // Connection is already ready
      await RedisConnectionManager.waitForAllConnections(1000);

      expect(mockLogger.info).toHaveBeenCalledWith('All Redis connections are ready');
    });

    it('should handle no connections', async () => {
      await RedisConnectionManager.waitForAllConnections(1000);
      
      expect(mockLogger.debug).toHaveBeenCalledWith('No Redis connections to wait for');
    });

    it('should timeout if connection not ready', async () => {
      const connection = manager.getConnection({
        name: 'timeout-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      // Make connection not ready
      (connection as any).status = 'connecting';

      await expect(RedisConnectionManager.waitForAllConnections(100)).rejects.toThrow(
        'failed to be ready within 100ms'
      );
    });

    it('should handle connection errors during wait', async () => {
      const connection = manager.getConnection({
        name: 'error-wait-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
        logger: mockLogger,
      });

      // Make connection not ready
      (connection as any).status = 'connecting';

      // Trigger error after a delay
      setTimeout(() => {
        (connection as any)._triggerEvent('error', new Error('Connection failed'));
      }, 50);

      await expect(RedisConnectionManager.waitForAllConnections(1000)).rejects.toThrow(
        'Connection failed'
      );
    });
  });

  describe('areAllConnectionsReady', () => {
    it('should return false when no connections', () => {
      expect(RedisConnectionManager.areAllConnectionsReady()).toBe(false);
    });

    it('should return true when all connections ready', async () => {
      manager.getConnection({
        name: 'ready-check-test',
        singleton: false,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      await RedisConnectionManager.waitForAllConnections(1000);
      
      expect(RedisConnectionManager.areAllConnectionsReady()).toBe(true);
    });
  });

  describe('edge cases', () => {
    it('should handle concurrent access to shared connections', () => {
      // Test that multiple requests for the same shared connection return the same instance
      const conn1 = manager.getConnection({
        name: 'shared-concurrent',
        singleton: true,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      const conn2 = manager.getConnection({
        name: 'shared-concurrent',
        singleton: true,
        redisConfig: { host: 'localhost', port: 6379 },
      });

      expect(conn1).toBe(conn2);
      expect(manager.getConnectionCount().shared).toBe(1);
    });

    it('should apply all Redis options', () => {
      const fullConfig: RedisConfig = {
        host: 'localhost',
        port: 6379,
        username: 'user',
        password: 'pass',
        db: 2,
        maxRetriesPerRequest: 5,
        retryDelayOnFailover: 200,
        connectTimeout: 20000,
        commandTimeout: 10000,
        keepAlive: 5000,
      };

      const connection = manager.getConnection({
        name: 'full-config-test',
        singleton: false,
        redisConfig: fullConfig,
      });

      expect(connection).toBeDefined();
    });
  });
});