import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';

// Mock Redis client
const mockRedisGet = jest.fn();
const mockRedisSetex = jest.fn();
const mockRedisDel = jest.fn();
const mockRedisScan = jest.fn();
let mockIsConnected = true;

jest.mock('../../../config/redis.js', () => ({
  redisClient: {
    get: (...args: any[]) => mockRedisGet(...args),
    setex: (...args: any[]) => mockRedisSetex(...args),
    del: (...args: any[]) => mockRedisDel(...args),
    scan: (...args: any[]) => mockRedisScan(...args),
  },
  isRedisConnected: () => mockIsConnected,
}));

const mockLoggerDebug = jest.fn();
const mockLoggerInfo = jest.fn();
const mockLoggerError = jest.fn();

jest.mock('../../utils/logger.js', () => ({
  logger: {
    debug: (...args: any[]) => mockLoggerDebug(...args),
    info: (...args: any[]) => mockLoggerInfo(...args),
    error: (...args: any[]) => mockLoggerError(...args),
    warn: jest.fn(),
  },
}));

// Import after mocking
import {
  getCachedPermissions,
  setCachedPermissions,
  invalidateUserPermissions,
  invalidateTenantPermissions,
  checkCachedPermission,
  permissionCacheService,
} from '../permission-cache.service.js';

describe('Permission Cache Service', () => {
  const tenantId = 'tenant-uuid-1';
  const userId = 'user-uuid-1';
  const mockPermissions = [
    { resource: 'invoices', action: 'create' },
    { resource: 'invoices', action: 'read' },
    { resource: 'partners', action: 'read' },
  ];

  beforeEach(() => {
    jest.clearAllMocks();
    mockIsConnected = true;
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  describe('getCachedPermissions', () => {
    it('should return null when Redis is not connected', async () => {
      mockIsConnected = false;

      const result = await getCachedPermissions(tenantId, userId);

      expect(result).toBeNull();
      expect(mockRedisGet).not.toHaveBeenCalled();
    });

    it('should return cached permissions on cache hit', async () => {
      mockRedisGet.mockResolvedValue(JSON.stringify(mockPermissions));

      const result = await getCachedPermissions(tenantId, userId);

      expect(mockRedisGet).toHaveBeenCalledWith(`permissions:${tenantId}:${userId}`);
      expect(result).toEqual(mockPermissions);
      expect(mockLoggerDebug).toHaveBeenCalledWith(
        'Permission cache hit',
        expect.objectContaining({ tenantId, userId })
      );
    });

    it('should return null on cache miss', async () => {
      mockRedisGet.mockResolvedValue(null);

      const result = await getCachedPermissions(tenantId, userId);

      expect(result).toBeNull();
      expect(mockLoggerDebug).toHaveBeenCalledWith(
        'Permission cache miss',
        expect.objectContaining({ tenantId, userId })
      );
    });

    it('should return null and log error on Redis error', async () => {
      const error = new Error('Redis connection failed');
      mockRedisGet.mockRejectedValue(error);

      const result = await getCachedPermissions(tenantId, userId);

      expect(result).toBeNull();
      expect(mockLoggerError).toHaveBeenCalledWith(
        'Error reading permission cache',
        expect.objectContaining({
          error: 'Redis connection failed',
          tenantId,
          userId,
        })
      );
    });
  });

  describe('setCachedPermissions', () => {
    it('should not set cache when Redis is not connected', async () => {
      mockIsConnected = false;

      await setCachedPermissions(tenantId, userId, mockPermissions);

      expect(mockRedisSetex).not.toHaveBeenCalled();
    });

    it('should cache permissions with 5 minute TTL', async () => {
      await setCachedPermissions(tenantId, userId, mockPermissions);

      expect(mockRedisSetex).toHaveBeenCalledWith(
        `permissions:${tenantId}:${userId}`,
        300, // 5 minutes TTL
        JSON.stringify(mockPermissions)
      );
      expect(mockLoggerDebug).toHaveBeenCalledWith(
        'Permissions cached',
        expect.objectContaining({
          tenantId,
          userId,
          count: mockPermissions.length,
          ttl: 300,
        })
      );
    });

    it('should log error on Redis error', async () => {
      const error = new Error('Redis write failed');
      mockRedisSetex.mockRejectedValue(error);

      await setCachedPermissions(tenantId, userId, mockPermissions);

      expect(mockLoggerError).toHaveBeenCalledWith(
        'Error caching permissions',
        expect.objectContaining({
          error: 'Redis write failed',
          tenantId,
          userId,
        })
      );
    });
  });

  describe('invalidateUserPermissions', () => {
    it('should not delete when Redis is not connected', async () => {
      mockIsConnected = false;

      await invalidateUserPermissions(tenantId, userId);

      expect(mockRedisDel).not.toHaveBeenCalled();
    });

    it('should delete user permission cache', async () => {
      await invalidateUserPermissions(tenantId, userId);

      expect(mockRedisDel).toHaveBeenCalledWith(`permissions:${tenantId}:${userId}`);
      expect(mockLoggerInfo).toHaveBeenCalledWith(
        'User permission cache invalidated',
        expect.objectContaining({ tenantId, userId })
      );
    });

    it('should log error on Redis error', async () => {
      const error = new Error('Redis delete failed');
      mockRedisDel.mockRejectedValue(error);

      await invalidateUserPermissions(tenantId, userId);

      expect(mockLoggerError).toHaveBeenCalledWith(
        'Error invalidating user permission cache',
        expect.objectContaining({
          error: 'Redis delete failed',
          tenantId,
          userId,
        })
      );
    });
  });

  describe('invalidateTenantPermissions', () => {
    it('should not scan when Redis is not connected', async () => {
      mockIsConnected = false;

      await invalidateTenantPermissions(tenantId);

      expect(mockRedisScan).not.toHaveBeenCalled();
    });

    it('should delete all tenant permission caches', async () => {
      // Simulate scan returning keys in one batch
      mockRedisScan.mockResolvedValueOnce([
        '0', // cursor = 0 means done
        [
          `permissions:${tenantId}:user1`,
          `permissions:${tenantId}:user2`,
          `permissions:${tenantId}:user3`,
        ],
      ]);

      await invalidateTenantPermissions(tenantId);

      expect(mockRedisScan).toHaveBeenCalledWith(
        '0',
        'MATCH',
        `permissions:${tenantId}:*`,
        'COUNT',
        100
      );
      expect(mockRedisDel).toHaveBeenCalledWith(
        `permissions:${tenantId}:user1`,
        `permissions:${tenantId}:user2`,
        `permissions:${tenantId}:user3`
      );
      expect(mockLoggerInfo).toHaveBeenCalledWith(
        'Tenant permission cache invalidated',
        expect.objectContaining({
          tenantId,
          keysDeleted: 3,
        })
      );
    });

    it('should handle multiple scan batches', async () => {
      // First batch
      mockRedisScan.mockResolvedValueOnce([
        '42', // cursor for next batch
        [`permissions:${tenantId}:user1`],
      ]);
      // Second batch (final)
      mockRedisScan.mockResolvedValueOnce([
        '0', // cursor = 0 means done
        [`permissions:${tenantId}:user2`],
      ]);

      await invalidateTenantPermissions(tenantId);

      expect(mockRedisScan).toHaveBeenCalledTimes(2);
      expect(mockRedisDel).toHaveBeenCalledTimes(2);
      expect(mockLoggerInfo).toHaveBeenCalledWith(
        'Tenant permission cache invalidated',
        expect.objectContaining({
          keysDeleted: 2,
        })
      );
    });

    it('should handle empty tenant (no cached permissions)', async () => {
      mockRedisScan.mockResolvedValueOnce(['0', []]);

      await invalidateTenantPermissions(tenantId);

      expect(mockRedisDel).not.toHaveBeenCalled();
      expect(mockLoggerInfo).toHaveBeenCalledWith(
        'Tenant permission cache invalidated',
        expect.objectContaining({ keysDeleted: 0 })
      );
    });

    it('should log error on Redis error', async () => {
      const error = new Error('Redis scan failed');
      mockRedisScan.mockRejectedValue(error);

      await invalidateTenantPermissions(tenantId);

      expect(mockLoggerError).toHaveBeenCalledWith(
        'Error invalidating tenant permission cache',
        expect.objectContaining({
          error: 'Redis scan failed',
          tenantId,
        })
      );
    });
  });

  describe('checkCachedPermission', () => {
    it('should return null when permissions not cached', async () => {
      mockRedisGet.mockResolvedValue(null);

      const result = await checkCachedPermission(tenantId, userId, 'invoices', 'create');

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

    it('should return true when permission exists in cache', async () => {
      mockRedisGet.mockResolvedValue(JSON.stringify(mockPermissions));

      const result = await checkCachedPermission(tenantId, userId, 'invoices', 'create');

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

    it('should return false when permission not in cache', async () => {
      mockRedisGet.mockResolvedValue(JSON.stringify(mockPermissions));

      const result = await checkCachedPermission(tenantId, userId, 'invoices', 'delete');

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

    it('should return null when Redis not connected', async () => {
      mockIsConnected = false;

      const result = await checkCachedPermission(tenantId, userId, 'invoices', 'create');

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

  describe('permissionCacheService object', () => {
    it('should expose all methods', () => {
      expect(permissionCacheService.get).toBe(getCachedPermissions);
      expect(permissionCacheService.set).toBe(setCachedPermissions);
      expect(permissionCacheService.invalidateUser).toBe(invalidateUserPermissions);
      expect(permissionCacheService.invalidateTenant).toBe(invalidateTenantPermissions);
      expect(permissionCacheService.checkPermission).toBe(checkCachedPermission);
    });
  });

  describe('Redis unavailable fallback', () => {
    it('should gracefully handle Redis being unavailable for reads', async () => {
      mockIsConnected = false;

      const result = await getCachedPermissions(tenantId, userId);

      expect(result).toBeNull();
      expect(mockLoggerError).not.toHaveBeenCalled();
    });

    it('should gracefully handle Redis being unavailable for writes', async () => {
      mockIsConnected = false;

      await setCachedPermissions(tenantId, userId, mockPermissions);

      expect(mockRedisSetex).not.toHaveBeenCalled();
      expect(mockLoggerError).not.toHaveBeenCalled();
    });

    it('should gracefully handle Redis being unavailable for invalidation', async () => {
      mockIsConnected = false;

      await invalidateUserPermissions(tenantId, userId);

      expect(mockRedisDel).not.toHaveBeenCalled();
      expect(mockLoggerError).not.toHaveBeenCalled();
    });
  });
});
