import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { CacheService } from './cache.service';

describe('CacheService', () => {
  let service: CacheService;
  let mockRedisClient: any;

  const mockConfigService = {
    get: jest.fn((key: string) => {
      if (key === 'REDIS_URL') return 'redis://localhost:6379';
      return undefined;
    }),
  };

  beforeEach(async () => {
    // Mock Redis client
    mockRedisClient = {
      connect: jest.fn().mockResolvedValue(undefined),
      get: jest.fn(),
      set: jest.fn(),
      setEx: jest.fn(),
      del: jest.fn(),
      keys: jest.fn(),
      exists: jest.fn(),
      incr: jest.fn(),
      expire: jest.fn(),
      flushAll: jest.fn(),
      quit: jest.fn(),
      on: jest.fn(),
    };

    // Mock createClient
    const redis = require('redis');
    redis.createClient = jest.fn(() => mockRedisClient);

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CacheService,
        {
          provide: ConfigService,
          useValue: mockConfigService,
        },
      ],
    }).compile();

    service = module.get<CacheService>(CacheService);

    // Manually set isConnected to true for testing
    (service as any).isConnected = true;
    (service as any).client = mockRedisClient;

    jest.clearAllMocks();
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  describe('get', () => {
    it('should retrieve value from cache', async () => {
      const testData = { name: 'John', age: 30 };
      mockRedisClient.get.mockResolvedValue(JSON.stringify(testData));

      const result = await service.get('test-key');

      expect(result).toEqual(testData);
      expect(mockRedisClient.get).toHaveBeenCalledWith('test-key');
    });

    it('should return null if key does not exist', async () => {
      mockRedisClient.get.mockResolvedValue(null);

      const result = await service.get('missing-key');

      expect(result).toBeNull();
    });

    it('should return null if Redis is not connected', async () => {
      (service as any).isConnected = false;

      const result = await service.get('test-key');

      expect(result).toBeNull();
      expect(mockRedisClient.get).not.toHaveBeenCalled();
    });

    it('should handle JSON parse errors', async () => {
      mockRedisClient.get.mockResolvedValue('invalid json');

      const result = await service.get('test-key');

      expect(result).toBeNull();
    });
  });

  describe('set', () => {
    it('should set value in cache without TTL', async () => {
      const testData = { name: 'John' };
      mockRedisClient.set.mockResolvedValue('OK');

      const result = await service.set('test-key', testData);

      expect(result).toBe(true);
      expect(mockRedisClient.set).toHaveBeenCalledWith(
        'test-key',
        JSON.stringify(testData),
      );
    });

    it('should set value in cache with TTL', async () => {
      const testData = { name: 'John' };
      mockRedisClient.setEx.mockResolvedValue('OK');

      const result = await service.set('test-key', testData, 3600);

      expect(result).toBe(true);
      expect(mockRedisClient.setEx).toHaveBeenCalledWith(
        'test-key',
        3600,
        JSON.stringify(testData),
      );
    });

    it('should return false if Redis is not connected', async () => {
      (service as any).isConnected = false;

      const result = await service.set('test-key', { name: 'John' });

      expect(result).toBe(false);
      expect(mockRedisClient.set).not.toHaveBeenCalled();
    });

    it('should handle errors gracefully', async () => {
      mockRedisClient.setEx.mockRejectedValue(new Error('Redis error'));

      const result = await service.set('test-key', { name: 'John' }, 3600);

      expect(result).toBe(false);
    });
  });

  describe('delete', () => {
    it('should delete key from cache', async () => {
      mockRedisClient.del.mockResolvedValue(1);

      const result = await service.delete('test-key');

      expect(result).toBe(true);
      expect(mockRedisClient.del).toHaveBeenCalledWith('test-key');
    });

    it('should return false if Redis is not connected', async () => {
      (service as any).isConnected = false;

      const result = await service.delete('test-key');

      expect(result).toBe(false);
    });

    it('should handle errors gracefully', async () => {
      mockRedisClient.del.mockRejectedValue(new Error('Redis error'));

      const result = await service.delete('test-key');

      expect(result).toBe(false);
    });
  });

  describe('deletePattern', () => {
    it('should delete keys matching pattern', async () => {
      mockRedisClient.keys.mockResolvedValue([
        'user:1',
        'user:2',
        'user:3',
      ]);
      mockRedisClient.del.mockResolvedValue(3);

      const result = await service.deletePattern('user:*');

      expect(result).toBe(3);
      expect(mockRedisClient.keys).toHaveBeenCalledWith('user:*');
      expect(mockRedisClient.del).toHaveBeenCalledWith([
        'user:1',
        'user:2',
        'user:3',
      ]);
    });

    it('should return 0 if no keys match pattern', async () => {
      mockRedisClient.keys.mockResolvedValue([]);

      const result = await service.deletePattern('nonexistent:*');

      expect(result).toBe(0);
    });

    it('should return 0 if Redis is not connected', async () => {
      (service as any).isConnected = false;

      const result = await service.deletePattern('user:*');

      expect(result).toBe(0);
    });
  });

  describe('exists', () => {
    it('should return true if key exists', async () => {
      mockRedisClient.exists.mockResolvedValue(1);

      const result = await service.exists('test-key');

      expect(result).toBe(true);
      expect(mockRedisClient.exists).toHaveBeenCalledWith('test-key');
    });

    it('should return false if key does not exist', async () => {
      mockRedisClient.exists.mockResolvedValue(0);

      const result = await service.exists('missing-key');

      expect(result).toBe(false);
    });

    it('should return false if Redis is not connected', async () => {
      (service as any).isConnected = false;

      const result = await service.exists('test-key');

      expect(result).toBe(false);
    });
  });

  describe('increment', () => {
    it('should increment key value', async () => {
      mockRedisClient.incr.mockResolvedValue(5);

      const result = await service.increment('counter');

      expect(result).toBe(5);
      expect(mockRedisClient.incr).toHaveBeenCalledWith('counter');
    });

    it('should set TTL on first increment', async () => {
      mockRedisClient.incr.mockResolvedValue(1);
      mockRedisClient.expire.mockResolvedValue(true);

      const result = await service.increment('counter', 60);

      expect(result).toBe(1);
      expect(mockRedisClient.expire).toHaveBeenCalledWith('counter', 60);
    });

    it('should not set TTL on subsequent increments', async () => {
      mockRedisClient.incr.mockResolvedValue(2);

      await service.increment('counter', 60);

      expect(mockRedisClient.expire).not.toHaveBeenCalled();
    });

    it('should return 0 if Redis is not connected', async () => {
      (service as any).isConnected = false;

      const result = await service.increment('counter');

      expect(result).toBe(0);
    });
  });

  describe('cacheUserProfile', () => {
    it('should cache user profile', async () => {
      const profile = { id: 'user_123', name: 'John Doe' };
      mockRedisClient.setEx.mockResolvedValue('OK');

      const result = await service.cacheUserProfile('user_123', profile);

      expect(result).toBe(true);
      expect(mockRedisClient.setEx).toHaveBeenCalledWith(
        'user:user_123',
        3600,
        JSON.stringify(profile),
      );
    });
  });

  describe('getUserProfile', () => {
    it('should retrieve cached user profile', async () => {
      const profile = { id: 'user_123', name: 'John Doe' };
      mockRedisClient.get.mockResolvedValue(JSON.stringify(profile));

      const result = await service.getUserProfile('user_123');

      expect(result).toEqual(profile);
      expect(mockRedisClient.get).toHaveBeenCalledWith('user:user_123');
    });
  });

  describe('invalidateUserProfile', () => {
    it('should invalidate user profile cache', async () => {
      mockRedisClient.del.mockResolvedValue(1);

      const result = await service.invalidateUserProfile('user_123');

      expect(result).toBe(true);
      expect(mockRedisClient.del).toHaveBeenCalledWith('user:user_123');
    });
  });

  describe('cacheChild', () => {
    it('should cache child data', async () => {
      const childData = { id: 'child_123', name: 'Baby Jane' };
      mockRedisClient.setEx.mockResolvedValue('OK');

      const result = await service.cacheChild('child_123', childData);

      expect(result).toBe(true);
      expect(mockRedisClient.setEx).toHaveBeenCalledWith(
        'child:child_123',
        3600,
        JSON.stringify(childData),
      );
    });
  });

  describe('getChild', () => {
    it('should retrieve cached child data', async () => {
      const childData = { id: 'child_123', name: 'Baby Jane' };
      mockRedisClient.get.mockResolvedValue(JSON.stringify(childData));

      const result = await service.getChild('child_123');

      expect(result).toEqual(childData);
    });
  });

  describe('checkRateLimit', () => {
    it('should allow request within rate limit', async () => {
      mockRedisClient.incr.mockResolvedValue(5);
      mockRedisClient.expire.mockResolvedValue(true);

      const result = await service.checkRateLimit('user_123', 'api', 10, 60);

      expect(result.allowed).toBe(true);
      expect(result.remaining).toBe(5);
      expect(result.resetAt).toBeInstanceOf(Date);
    });

    it('should deny request exceeding rate limit', async () => {
      mockRedisClient.incr.mockResolvedValue(11);

      const result = await service.checkRateLimit('user_123', 'api', 10, 60);

      expect(result.allowed).toBe(false);
      expect(result.remaining).toBe(0);
    });

    it('should use default window of 60 seconds', async () => {
      mockRedisClient.incr.mockResolvedValue(1);
      mockRedisClient.expire.mockResolvedValue(true);

      await service.checkRateLimit('user_123', 'api', 10);

      expect(mockRedisClient.expire).toHaveBeenCalledWith(
        'rate:api:user_123',
        60,
      );
    });
  });

  describe('resetRateLimit', () => {
    it('should reset rate limit for user', async () => {
      mockRedisClient.del.mockResolvedValue(1);

      const result = await service.resetRateLimit('user_123', 'api');

      expect(result).toBe(true);
      expect(mockRedisClient.del).toHaveBeenCalledWith('rate:api:user_123');
    });
  });

  describe('cacheAnalytics', () => {
    it('should cache analytics data with default TTL', async () => {
      const analyticsData = { totalUsers: 1000 };
      mockRedisClient.setEx.mockResolvedValue('OK');

      const result = await service.cacheAnalytics('daily-stats', analyticsData);

      expect(result).toBe(true);
      expect(mockRedisClient.setEx).toHaveBeenCalledWith(
        'analytics:daily-stats',
        600,
        JSON.stringify(analyticsData),
      );
    });

    it('should cache analytics data with custom TTL', async () => {
      const analyticsData = { totalUsers: 1000 };
      mockRedisClient.setEx.mockResolvedValue('OK');

      await service.cacheAnalytics('daily-stats', analyticsData, 1800);

      expect(mockRedisClient.setEx).toHaveBeenCalledWith(
        'analytics:daily-stats',
        1800,
        JSON.stringify(analyticsData),
      );
    });
  });

  describe('cacheSession', () => {
    it('should cache session data', async () => {
      const sessionData = { userId: 'user_123', token: 'abc123' };
      mockRedisClient.setEx.mockResolvedValue('OK');

      const result = await service.cacheSession('session_456', sessionData);

      expect(result).toBe(true);
      expect(mockRedisClient.setEx).toHaveBeenCalledWith(
        'session:session_456',
        86400,
        JSON.stringify(sessionData),
      );
    });
  });

  describe('invalidateSession', () => {
    it('should invalidate session', async () => {
      mockRedisClient.del.mockResolvedValue(1);

      const result = await service.invalidateSession('session_456');

      expect(result).toBe(true);
      expect(mockRedisClient.del).toHaveBeenCalledWith('session:session_456');
    });
  });

  describe('invalidateUserSessions', () => {
    it('should invalidate all user sessions', async () => {
      mockRedisClient.keys.mockResolvedValue([
        'session:1:user_123',
        'session:2:user_123',
      ]);
      mockRedisClient.del.mockResolvedValue(2);

      const result = await service.invalidateUserSessions('user_123');

      expect(result).toBe(2);
      expect(mockRedisClient.keys).toHaveBeenCalledWith('session:*:user_123');
    });
  });

  describe('cacheFamily', () => {
    it('should cache family data', async () => {
      const familyData = { id: 'family_123', name: 'Smith Family' };
      mockRedisClient.setEx.mockResolvedValue('OK');

      const result = await service.cacheFamily('family_123', familyData);

      expect(result).toBe(true);
      expect(mockRedisClient.setEx).toHaveBeenCalledWith(
        'family:family_123',
        1800,
        JSON.stringify(familyData),
      );
    });
  });

  describe('invalidateFamily', () => {
    it('should invalidate family and related children', async () => {
      mockRedisClient.del.mockResolvedValue(1);
      mockRedisClient.keys.mockResolvedValue([]);

      const result = await service.invalidateFamily('family_123');

      expect(result).toBe(true);
      expect(mockRedisClient.del).toHaveBeenCalledWith('family:family_123');
      expect(mockRedisClient.keys).toHaveBeenCalledWith(
        'child:*:family:family_123',
      );
    });
  });

  describe('flushAll', () => {
    it('should flush entire cache', async () => {
      mockRedisClient.flushAll.mockResolvedValue('OK');

      const result = await service.flushAll();

      expect(result).toBe(true);
      expect(mockRedisClient.flushAll).toHaveBeenCalled();
    });

    it('should return false if Redis is not connected', async () => {
      (service as any).isConnected = false;

      const result = await service.flushAll();

      expect(result).toBe(false);
    });

    it('should handle errors gracefully', async () => {
      mockRedisClient.flushAll.mockRejectedValue(new Error('Redis error'));

      const result = await service.flushAll();

      expect(result).toBe(false);
    });
  });

  describe('getStatus', () => {
    it('should return connection status', () => {
      const result = service.getStatus();

      expect(result).toEqual({ connected: true });
    });

    it('should return disconnected status', () => {
      (service as any).isConnected = false;

      const result = service.getStatus();

      expect(result).toEqual({ connected: false });
    });
  });

  describe('disconnect', () => {
    it('should disconnect from Redis', async () => {
      mockRedisClient.quit.mockResolvedValue('OK');

      await service.disconnect();

      expect(mockRedisClient.quit).toHaveBeenCalled();
    });

    it('should handle disconnect when not connected', async () => {
      (service as any).isConnected = false;

      await expect(service.disconnect()).resolves.not.toThrow();
    });
  });

  describe('integration scenarios', () => {
    it('should handle complete user session workflow', async () => {
      // Cache user profile
      mockRedisClient.setEx.mockResolvedValue('OK');
      await service.cacheUserProfile('user_123', { name: 'John' });

      // Cache session
      await service.cacheSession('session_456', {
        userId: 'user_123',
        token: 'abc',
      });

      // Check rate limit
      mockRedisClient.incr.mockResolvedValue(1);
      mockRedisClient.expire.mockResolvedValue(true);
      const rateLimitResult = await service.checkRateLimit(
        'user_123',
        'api',
        100,
      );
      expect(rateLimitResult.allowed).toBe(true);

      // Invalidate session
      mockRedisClient.del.mockResolvedValue(1);
      await service.invalidateSession('session_456');

      expect(mockRedisClient.setEx).toHaveBeenCalledTimes(2);
      expect(mockRedisClient.del).toHaveBeenCalled();
    });

    it('should handle family data caching workflow', async () => {
      mockRedisClient.setEx.mockResolvedValue('OK');

      // Cache family
      await service.cacheFamily('family_123', { name: 'Smith' });

      // Cache children
      await service.cacheChild('child_1', { name: 'Alice' });
      await service.cacheChild('child_2', { name: 'Bob' });

      // Invalidate family (should clear children too)
      mockRedisClient.del.mockResolvedValue(1);
      mockRedisClient.keys.mockResolvedValue([
        'child:child_1:family:family_123',
        'child:child_2:family:family_123',
      ]);

      await service.invalidateFamily('family_123');

      expect(mockRedisClient.keys).toHaveBeenCalledWith(
        'child:*:family:family_123',
      );
    });
  });
});
