import { RedisContainer, StartedRedisContainer } from '@testcontainers/redis';
import Redis from 'ioredis';
import { RedisLockService } from '../RedisLockService';

describe('RedisLockService Integration', () => {
  let redisContainer: StartedRedisContainer;
  let redis: Redis;
  let lockService: RedisLockService;

  beforeAll(async () => {
    // Start Redis container
    redisContainer = await new RedisContainer('redis:7-alpine')
      .withExposedPorts(6379)
      .start();

    // Create Redis connection
    const connectionUrl = redisContainer.getConnectionUrl();
    redis = new Redis(connectionUrl, { maxRetriesPerRequest: null });

    // Create lock service
    lockService = new RedisLockService(redis);
  }, 60000); // Increase timeout for container startup

  afterAll(async () => {
    // Clean up
    if (redis) {
      await redis.quit();
    }
    if (redisContainer) {
      await redisContainer.stop();
    }
  });

  beforeEach(async () => {
    // Clear Redis data between tests
    await redis.flushall();
  });

  describe('Basic Lock Operations', () => {
    it('should acquire and release a lock successfully', async () => {
      // Arrange
      const lockKey = 'test-lock-key';
      let executionCount = 0;
      const testFunction = async () => {
        executionCount++;
        return 'success';
      };

      // Act
      const requestLock = lockService.createRequestLock();
      const result = await requestLock(lockKey, testFunction);

      // Assert
      expect(result).toBe('success');
      expect(executionCount).toBe(1);

      // Verify lock was released by checking Redis directly
      const lockPattern = `oauth:lock:*:${lockKey}`;
      const keys = await redis.keys(lockPattern);
      expect(keys).toHaveLength(0);
    });

    it('should prevent concurrent execution of the same lock key', async () => {
      // Arrange
      const lockKey = 'concurrent-test-lock';
      let executionOrder: number[] = [];
      let currentExecution = 0;

      const createTestFunction = (id: number) => async () => {
        const executionId = ++currentExecution;
        executionOrder.push(id);

        // Simulate some work
        await new Promise((resolve) => setTimeout(resolve, 100));

        return `result-${id}-${executionId}`;
      };

      // Act - Start two concurrent operations with same lock key
      const requestLock = lockService.createRequestLock();
      const [result1, result2] = await Promise.all([
        requestLock(lockKey, createTestFunction(1)),
        requestLock(lockKey, createTestFunction(2)),
      ]);

      // Assert - Both should succeed but execute sequentially
      expect(result1).toMatch(/^result-1-\d+$/);
      expect(result2).toMatch(/^result-2-\d+$/);
      expect(executionOrder).toHaveLength(2);

      // Verify they executed sequentially (not concurrently)
      expect(currentExecution).toBe(2);
    });

    it('should allow concurrent execution with different lock keys', async () => {
      // Arrange
      const lockKey1 = 'lock-key-1';
      const lockKey2 = 'lock-key-2';
      let startTimes: number[] = [];

      const createTestFunction = (id: number) => async () => {
        startTimes.push(Date.now());
        await new Promise((resolve) => setTimeout(resolve, 200));
        return `result-${id}`;
      };

      // Act - Start concurrent operations with different lock keys
      const requestLock = lockService.createRequestLock();
      const startTime = Date.now();
      const [result1, result2] = await Promise.all([
        requestLock(lockKey1, createTestFunction(1)),
        requestLock(lockKey2, createTestFunction(2)),
      ]);
      const totalTime = Date.now() - startTime;

      // Assert - Both should succeed and execute concurrently
      expect(result1).toBe('result-1');
      expect(result2).toBe('result-2');
      expect(startTimes).toHaveLength(2);

      // Should complete in roughly 200ms (concurrent) rather than 400ms (sequential)
      expect(totalTime).toBeLessThan(350);

      // Start times should be close together (concurrent execution)
      const timeDiff = Math.abs(startTimes[1]! - startTimes[0]!);
      expect(timeDiff).toBeLessThan(50);
    });

    it('should handle function that throws an error', async () => {
      // Arrange
      const lockKey = 'error-test-lock';
      const errorMessage = 'Test error';
      const errorFunction = async () => {
        throw new Error(errorMessage);
      };

      // Act & Assert
      const requestLock = lockService.createRequestLock();
      await expect(requestLock(lockKey, errorFunction)).rejects.toThrow(
        errorMessage,
      );

      // Verify lock was released even after error
      const lockPattern = `oauth:lock:*:${lockKey}`;
      const keys = await redis.keys(lockPattern);
      expect(keys).toHaveLength(0);
    });

    it('should handle async function that returns a promise', async () => {
      // Arrange
      const lockKey = 'async-test-lock';
      const asyncFunction = async () => {
        await new Promise((resolve) => setTimeout(resolve, 50));
        return { data: 'async-result', timestamp: Date.now() };
      };

      // Act
      const requestLock = lockService.createRequestLock();
      const result = await requestLock(lockKey, asyncFunction);

      // Assert
      expect(result).toHaveProperty('data', 'async-result');
      expect(result).toHaveProperty('timestamp');
      expect(typeof result.timestamp).toBe('number');
    });
  });

  describe('Lock Key Isolation', () => {
    it('should include Fly.io instance ID in lock key when available', async () => {
      // Arrange
      const originalAllocId = process.env.FLY_ALLOC_ID;
      process.env.FLY_ALLOC_ID = 'test-instance-123';

      const lockKey = 'instance-test-lock';
      let lockKeyUsed = '';

      // Mock redlock to capture the actual lock key used
      const originalAcquire = lockService['redlock'].acquire;
      lockService['redlock'].acquire = jest
        .fn()
        .mockImplementation(async (keys: string[]) => {
          lockKeyUsed = keys[0]!;
          return originalAcquire.call(lockService['redlock'], keys, 30000);
        });

      try {
        // Act
        const requestLock = lockService.createRequestLock();
        await requestLock(lockKey, async () => 'test');

        // Assert
        expect(lockKeyUsed).toBe(`oauth:lock:test-instance-123:${lockKey}`);
      } finally {
        // Cleanup
        process.env.FLY_ALLOC_ID = originalAllocId;
        lockService['redlock'].acquire = originalAcquire;
      }
    });

    it('should use "local" as default instance ID when FLY_ALLOC_ID is not set', async () => {
      // Arrange
      const originalAllocId = process.env.FLY_ALLOC_ID;
      delete process.env.FLY_ALLOC_ID;

      const lockKey = 'local-test-lock';
      let lockKeyUsed = '';

      // Mock redlock to capture the actual lock key used
      const originalAcquire = lockService['redlock'].acquire;
      lockService['redlock'].acquire = jest
        .fn()
        .mockImplementation(async (keys: string[]) => {
          lockKeyUsed = keys[0]!;
          return originalAcquire.call(lockService['redlock'], keys, 30000);
        });

      try {
        // Act
        const requestLock = lockService.createRequestLock();
        await requestLock(lockKey, async () => 'test');

        // Assert
        expect(lockKeyUsed).toBe(`oauth:lock:local:${lockKey}`);
      } finally {
        // Cleanup
        process.env.FLY_ALLOC_ID = originalAllocId;
        lockService['redlock'].acquire = originalAcquire;
      }
    });
  });

  describe('Lock Timeout and TTL', () => {
    it('should automatically release lock after TTL expires', async () => {
      // Arrange
      const lockKey = 'ttl-test-lock';

      // Manually acquire a lock with short TTL to simulate timeout
      const instanceId = process.env.FLY_ALLOC_ID || 'local';
      const fullLockKey = `oauth:lock:${instanceId}:${lockKey}`;

      // Use redlock directly to set a very short TTL (100ms)
      const shortLock = await lockService['redlock'].acquire(
        [fullLockKey],
        100,
      );

      // Act - Wait for lock to expire
      await new Promise((resolve) => setTimeout(resolve, 200));

      // Try to acquire the same lock - should succeed if previous lock expired
      const requestLock = lockService.createRequestLock();
      const result = await requestLock(
        lockKey,
        async () => 'success-after-timeout',
      );

      // Assert
      expect(result).toBe('success-after-timeout');

      // Cleanup - release the short lock (may already be expired)
      try {
        await lockService['redlock'].release(shortLock);
      } catch {
        // Ignore errors if lock already expired
      }
    });

    it('should handle high concurrency with retry mechanism', async () => {
      // Arrange
      const lockKey = 'high-concurrency-lock';
      const concurrentOperations = 5;
      let completedOperations = 0;

      const testFunction = async () => {
        await new Promise((resolve) => setTimeout(resolve, 50));
        return ++completedOperations;
      };

      // Act - Start multiple concurrent operations
      const requestLock = lockService.createRequestLock();
      const promises = Array.from({ length: concurrentOperations }, () =>
        requestLock(lockKey, testFunction),
      );

      const results = await Promise.all(promises);

      // Assert - All operations should complete successfully
      expect(results).toHaveLength(concurrentOperations);
      expect(completedOperations).toBe(concurrentOperations);

      // Results should be sequential numbers (1, 2, 3, 4, 5)
      const sortedResults = results.sort((a, b) => a - b);
      expect(sortedResults).toEqual([1, 2, 3, 4, 5]);
    });
  });

  describe('Error Handling', () => {
    it('should handle Redis connection issues gracefully', async () => {
      // Arrange - Create a new Redis connection that we can close
      const testRedis = new Redis(redisContainer.getConnectionUrl(), {
        maxRetriesPerRequest: null,
      });
      const testLockService = new RedisLockService(testRedis);

      // Close the connection to simulate network issues
      await testRedis.quit();

      // Act & Assert - Should throw an error when trying to acquire lock
      const requestLock = testLockService.createRequestLock();
      await expect(
        requestLock('test-key', async () => 'should-not-execute'),
      ).rejects.toThrow();
    });

    it('should release lock even when function execution is interrupted', async () => {
      // Arrange
      const lockKey = 'interrupt-test-lock';
      let lockAcquired = false;

      const interruptedFunction = async () => {
        lockAcquired = true;
        // Simulate an interruption/error after lock is acquired
        throw new Error('Simulated interruption');
      };

      // Act & Assert
      const requestLock = lockService.createRequestLock();
      await expect(requestLock(lockKey, interruptedFunction)).rejects.toThrow(
        'Simulated interruption',
      );

      // Verify lock was acquired initially
      expect(lockAcquired).toBe(true);

      // Verify lock was released after error
      const lockPattern = `oauth:lock:*:${lockKey}`;
      const keys = await redis.keys(lockPattern);
      expect(keys).toHaveLength(0);

      // Verify we can acquire the same lock again
      const result = await requestLock(lockKey, async () => 'recovered');
      expect(result).toBe('recovered');
    });
  });
});
