/**
 * Cross-Database Transaction Integration Tests
 *
 * Tests for atomic transaction support across SQLite and Redis.
 * Issue #13: Cross-Database Transaction Atomicity
 */

import { RedisAdapter } from '../redis-adapter';
import { SqliteAdapter } from '../sqlite-adapter';
import { TransactionManager } from '../transaction-manager';
import { DatabaseConfig } from '../types';

// Mock Redis and SQLite
jest.mock('redis', () => ({
  createClient: jest.fn(),
}));

jest.mock('better-sqlite3', () => {
  return jest.fn().mockImplementation(() => ({
    prepare: jest.fn(),
    exec: jest.fn(),
    close: jest.fn(),
  }));
});

describe('Cross-Database Transaction Integration', () => {
  let redisAdapter: RedisAdapter;
  let sqliteAdapter: SqliteAdapter;
  let transactionManager: TransactionManager;
  let mockRedisClient: any;
  let mockRedisMulti: any;
  let mockSqliteDb: any;

  const redisConfig: DatabaseConfig = {
    type: 'redis',
    host: 'localhost',
    port: 6379,
  };

  const sqliteConfig: DatabaseConfig = {
    type: 'sqlite',
    database: ':memory:',
  };

  beforeEach(() => {
    // Setup Redis mocks
    mockRedisMulti = {
      exec: jest.fn().mockResolvedValue(['OK', 'OK']),
      discard: jest.fn().mockResolvedValue(undefined),
      set: jest.fn().mockReturnThis(),
      get: jest.fn().mockReturnThis(),
      del: jest.fn().mockReturnThis(),
    };

    mockRedisClient = {
      connect: jest.fn().mockResolvedValue(undefined),
      disconnect: jest.fn().mockResolvedValue(undefined),
      isOpen: true,
      multi: jest.fn().mockReturnValue(mockRedisMulti),
      sendCommand: jest.fn(),
    };

    const { createClient } = require('redis');
    (createClient as jest.Mock).mockReturnValue(mockRedisClient);

    // Setup SQLite mocks
    const BetterSqlite3 = require('better-sqlite3');
    mockSqliteDb = {
      prepare: jest.fn().mockReturnValue({
        run: jest.fn(),
        get: jest.fn(),
        all: jest.fn(),
      }),
      exec: jest.fn(),
      run: jest.fn(),
      close: jest.fn(),
    };
    (BetterSqlite3 as jest.Mock).mockReturnValue(mockSqliteDb);

    // Create adapters
    redisAdapter = new RedisAdapter(redisConfig);
    sqliteAdapter = new SqliteAdapter(sqliteConfig);
    transactionManager = new TransactionManager();
  });

  afterEach(async () => {
    if (redisAdapter.isConnected()) {
      await redisAdapter.disconnect();
    }
    if (sqliteAdapter.isConnected()) {
      await sqliteAdapter.disconnect();
    }
    jest.clearAllMocks();
  });

  describe('atomic commit across databases', () => {
    it('should commit SQLite + Redis atomically', async () => {
      await redisAdapter.connect();
      await sqliteAdapter.connect();

      transactionManager.registerAdapter('redis', redisAdapter);
      transactionManager.registerAdapter('sqlite', sqliteAdapter);

      // Begin cross-database transaction
      const contexts = await transactionManager.beginTransaction(['redis', 'sqlite']);

      expect(contexts).toHaveLength(2);
      expect(contexts[0].databases).toContain('redis');
      expect(contexts[1].databases).toContain('sqlite');

      // Both should be in pending state
      expect(contexts[0].status).toBe('pending');
      expect(contexts[1].status).toBe('pending');

      // Commit both
      await transactionManager.commitTransaction(contexts);

      // Both should be committed
      expect(contexts[0].status).toBe('committed');
      expect(contexts[1].status).toBe('committed');

      expect(mockRedisMulti.exec).toHaveBeenCalled();
      expect(mockSqliteDb.run).toHaveBeenCalledWith('COMMIT');
    });

    it('should handle partial commit failures with proper error propagation', async () => {
      await redisAdapter.connect();
      await sqliteAdapter.connect();

      transactionManager.registerAdapter('redis', redisAdapter);
      transactionManager.registerAdapter('sqlite', sqliteAdapter);

      const contexts = await transactionManager.beginTransaction(['redis', 'sqlite']);

      // Simulate Redis commit failure
      mockRedisMulti.exec.mockResolvedValue(null); // Transaction aborted

      await expect(transactionManager.commitTransaction(contexts))
        .rejects
        .toThrow('Transaction aborted');

      // Redis should remain pending (failed)
      // SQLite might have committed (depending on order)
      const redisContext = contexts.find(c => c.databases.includes('redis'));
      expect(redisContext?.status).toBe('pending');
    });
  });

  describe('rollback consistency across databases', () => {
    it('should rollback SQLite + Redis consistently', async () => {
      await redisAdapter.connect();
      await sqliteAdapter.connect();

      transactionManager.registerAdapter('redis', redisAdapter);
      transactionManager.registerAdapter('sqlite', sqliteAdapter);

      const contexts = await transactionManager.beginTransaction(['redis', 'sqlite']);

      // Rollback both
      await transactionManager.rollbackTransaction(contexts);

      expect(contexts[0].status).toBe('rolled_back');
      expect(contexts[1].status).toBe('rolled_back');

      expect(mockRedisMulti.discard).toHaveBeenCalled();
      expect(mockSqliteDb.run).toHaveBeenCalledWith('ROLLBACK');
    });

    it('should handle partial rollback failures gracefully', async () => {
      await redisAdapter.connect();
      await sqliteAdapter.connect();

      transactionManager.registerAdapter('redis', redisAdapter);
      transactionManager.registerAdapter('sqlite', sqliteAdapter);

      const contexts = await transactionManager.beginTransaction(['redis', 'sqlite']);

      // Simulate Redis rollback failure
      mockRedisMulti.discard.mockRejectedValue(new Error('DISCARD failed'));

      await expect(transactionManager.rollbackTransaction(contexts))
        .rejects
        .toThrow();

      // At least one should have attempted rollback
      expect(mockRedisMulti.discard).toHaveBeenCalled();
    });
  });

  describe('transaction isolation across databases', () => {
    it('should maintain isolation between concurrent cross-db transactions', async () => {
      await redisAdapter.connect();
      await sqliteAdapter.connect();

      transactionManager.registerAdapter('redis', redisAdapter);
      transactionManager.registerAdapter('sqlite', sqliteAdapter);

      // Start two concurrent transactions
      const tx1 = await transactionManager.beginTransaction(['redis', 'sqlite']);
      const tx2 = await transactionManager.beginTransaction(['redis', 'sqlite']);

      expect(tx1[0].id).not.toBe(tx2[0].id);
      expect(tx1[1].id).not.toBe(tx2[1].id);

      // Commit tx1
      await transactionManager.commitTransaction(tx1);
      expect(tx1[0].status).toBe('committed');
      expect(tx1[1].status).toBe('committed');

      // tx2 should still be pending and committable
      expect(tx2[0].status).toBe('pending');
      expect(tx2[1].status).toBe('pending');

      await transactionManager.commitTransaction(tx2);
      expect(tx2[0].status).toBe('committed');
      expect(tx2[1].status).toBe('committed');
    });
  });

  describe('error propagation in cross-db transactions', () => {
    it('should propagate errors from Redis to transaction manager', async () => {
      await redisAdapter.connect();
      await sqliteAdapter.connect();

      transactionManager.registerAdapter('redis', redisAdapter);
      transactionManager.registerAdapter('sqlite', sqliteAdapter);

      const contexts = await transactionManager.beginTransaction(['redis', 'sqlite']);

      // Simulate critical Redis error
      mockRedisMulti.exec.mockRejectedValue(new Error('Redis connection lost'));

      await expect(transactionManager.commitTransaction(contexts))
        .rejects
        .toThrow('Redis connection lost');
    });

    it('should propagate errors from SQLite to transaction manager', async () => {
      await redisAdapter.connect();
      await sqliteAdapter.connect();

      transactionManager.registerAdapter('redis', redisAdapter);
      transactionManager.registerAdapter('sqlite', sqliteAdapter);

      const contexts = await transactionManager.beginTransaction(['redis', 'sqlite']);

      // Simulate SQLite error
      mockSqliteDb.run.mockImplementation((sql: string) => {
        if (sql === 'COMMIT') {
          throw new Error('SQLite disk full');
        }
      });

      await expect(transactionManager.commitTransaction(contexts))
        .rejects
        .toThrow('SQLite disk full');
    });
  });
});
