import { beforeEach, describe, expect, it, mock } from 'bun:test';
import { QuestDBHealthMonitor } from '../src/health';
import { QuestDBInfluxWriter } from '../src/influx-writer';
import { QuestDBQueryBuilder } from '../src/query-builder';
import { QuestDBSchemaManager } from '../src/schema';
import type { OHLCVData, QuestDBClientConfig, TradeData } from '../src/types';

// Simple in-memory QuestDB client for testing
class SimpleQuestDBClient {
  private data = new Map<string, any[]>();
  private schemas = new Map<string, any>();
  private logger: any;
  private config: QuestDBClientConfig;
  private connected = false;

  constructor(config: QuestDBClientConfig, logger?: any) {
    this.config = config;
    this.logger = logger || console;
  }

  async connect(): Promise<void> {
    this.connected = true;
    this.logger.info('Connected to QuestDB');
  }

  async disconnect(): Promise<void> {
    this.connected = false;
    this.logger.info('Disconnected from QuestDB');
  }

  async query<T = any>(sql: string): Promise<T[]> {
    if (!this.connected) {
      throw new Error('Not connected to QuestDB');
    }

    // Parse simple SELECT queries
    const match = sql.match(/SELECT \* FROM (\w+)/i);
    if (match) {
      const table = match[1];
      return (this.data.get(table) || []) as T[];
    }

    return [];
  }

  async execute(sql: string): Promise<void> {
    if (!this.connected) {
      throw new Error('Not connected to QuestDB');
    }

    // Parse simple CREATE TABLE
    const createMatch = sql.match(/CREATE TABLE IF NOT EXISTS (\w+)/i);
    if (createMatch) {
      const table = createMatch[1];
      this.schemas.set(table, {});
      this.data.set(table, []);
    }
  }

  async insertOHLCV(data: OHLCVData[]): Promise<void> {
    if (!this.connected) {
      throw new Error('Not connected to QuestDB');
    }

    const ohlcv = this.data.get('ohlcv') || [];
    ohlcv.push(...data);
    this.data.set('ohlcv', ohlcv);
  }

  async insertTrades(trades: TradeData[]): Promise<void> {
    if (!this.connected) {
      throw new Error('Not connected to QuestDB');
    }

    const tradesData = this.data.get('trades') || [];
    tradesData.push(...trades);
    this.data.set('trades', tradesData);
  }

  async getLatestOHLCV(symbol: string, limit = 100): Promise<OHLCVData[]> {
    const ohlcv = this.data.get('ohlcv') || [];
    return ohlcv.filter(item => item.symbol === symbol).slice(-limit);
  }

  async getOHLCVRange(symbol: string, startTime: Date, endTime: Date): Promise<OHLCVData[]> {
    const ohlcv = this.data.get('ohlcv') || [];
    const start = startTime.getTime();
    const end = endTime.getTime();

    return ohlcv.filter(
      item => item.symbol === symbol && item.timestamp >= start && item.timestamp <= end
    );
  }

  async healthCheck(): Promise<boolean> {
    return this.connected;
  }
}

describe('QuestDB', () => {
  describe('QuestDBClient', () => {
    let client: SimpleQuestDBClient;
    const logger = {
      info: mock(() => {}),
      error: mock(() => {}),
      warn: mock(() => {}),
      debug: mock(() => {}),
    };

    const config: QuestDBClientConfig = {
      host: 'localhost',
      httpPort: 9000,
      pgPort: 8812,
      influxPort: 9009,
      database: 'questdb',
    };

    beforeEach(() => {
      logger.info = mock(() => {});
      logger.error = mock(() => {});
      client = new SimpleQuestDBClient(config, logger);
    });

    it('should connect to database', async () => {
      await client.connect();
      expect(logger.info).toHaveBeenCalledWith('Connected to QuestDB');
    });

    it('should disconnect from database', async () => {
      await client.connect();
      await client.disconnect();
      expect(logger.info).toHaveBeenCalledWith('Disconnected from QuestDB');
    });

    it('should throw error when querying without connection', async () => {
      await expect(client.query('SELECT * FROM ohlcv')).rejects.toThrow('Not connected to QuestDB');
    });

    it('should execute CREATE TABLE statements', async () => {
      await client.connect();
      await client.execute('CREATE TABLE IF NOT EXISTS ohlcv');

      const result = await client.query('SELECT * FROM ohlcv');
      expect(result).toEqual([]);
    });

    it('should insert and retrieve OHLCV data', async () => {
      await client.connect();

      const ohlcvData: OHLCVData[] = [
        {
          symbol: 'AAPL',
          timestamp: Date.now(),
          open: 150.0,
          high: 152.0,
          low: 149.0,
          close: 151.0,
          volume: 1000000,
        },
      ];

      await client.insertOHLCV(ohlcvData);
      const result = await client.getLatestOHLCV('AAPL');

      expect(result).toHaveLength(1);
      expect(result[0].symbol).toBe('AAPL');
      expect(result[0].close).toBe(151.0);
    });

    it('should insert and retrieve trade data', async () => {
      await client.connect();

      const trades: TradeData[] = [
        {
          symbol: 'AAPL',
          timestamp: Date.now(),
          price: 151.5,
          quantity: 100,
          side: 'buy',
          exchange: 'NASDAQ',
        },
      ];

      await client.insertTrades(trades);
      // Just verify it doesn't throw
      expect(true).toBe(true);
    });

    it('should get OHLCV data within time range', async () => {
      await client.connect();

      const now = Date.now();
      const ohlcvData: OHLCVData[] = [
        {
          symbol: 'AAPL',
          timestamp: now - 3600000, // 1 hour ago
          open: 149.0,
          high: 150.0,
          low: 148.0,
          close: 149.5,
          volume: 500000,
        },
        {
          symbol: 'AAPL',
          timestamp: now - 1800000, // 30 minutes ago
          open: 149.5,
          high: 151.0,
          low: 149.0,
          close: 150.5,
          volume: 600000,
        },
        {
          symbol: 'AAPL',
          timestamp: now, // now
          open: 150.5,
          high: 152.0,
          low: 150.0,
          close: 151.5,
          volume: 700000,
        },
      ];

      await client.insertOHLCV(ohlcvData);

      const result = await client.getOHLCVRange(
        'AAPL',
        new Date(now - 2700000), // 45 minutes ago
        new Date(now)
      );

      expect(result).toHaveLength(2);
      expect(result[0].timestamp).toBe(now - 1800000);
      expect(result[1].timestamp).toBe(now);
    });

    it('should perform health check', async () => {
      expect(await client.healthCheck()).toBe(false);

      await client.connect();
      expect(await client.healthCheck()).toBe(true);

      await client.disconnect();
      expect(await client.healthCheck()).toBe(false);
    });
  });

  describe('QuestDBQueryBuilder', () => {
    it('should build SELECT query', () => {
      const mockClient = {
        query: async () => ({ rows: [], count: 0 }),
      };

      const builder = new QuestDBQueryBuilder(mockClient);

      const query = builder
        .select('symbol', 'close', 'volume')
        .from('ohlcv_data')
        .whereSymbol('AAPL')
        .orderBy('timestamp', 'DESC')
        .limit(100)
        .build();

      expect(query).toContain('SELECT symbol, close, volume');
      expect(query).toContain('FROM ohlcv_data');
      expect(query).toContain("symbol = 'AAPL'");
      expect(query).toContain('ORDER BY timestamp DESC');
      expect(query).toContain('LIMIT 100');
    });

    it('should build query with time range', () => {
      const mockClient = {
        query: async () => ({ rows: [], count: 0 }),
      };

      const builder = new QuestDBQueryBuilder(mockClient);
      const startTime = new Date('2023-01-01');
      const endTime = new Date('2023-01-31');

      const query = builder.from('trades').whereTimeRange(startTime, endTime).build();

      expect(query).toContain('timestamp >=');
      expect(query).toContain('timestamp <=');
    });

    it('should build aggregation query', () => {
      const mockClient = {
        query: async () => ({ rows: [], count: 0 }),
      };

      const builder = new QuestDBQueryBuilder(mockClient);

      const query = builder
        .selectAgg({
          avg_close: 'AVG(close)',
          total_volume: 'SUM(volume)',
        })
        .from('ohlcv_data')
        .groupBy('symbol')
        .build();

      expect(query).toContain('AVG(close) as avg_close');
      expect(query).toContain('SUM(volume) as total_volume');
      expect(query).toContain('GROUP BY symbol');
    });

    it('should build sample by query', () => {
      const mockClient = {
        query: async () => ({ rows: [], count: 0 }),
      };

      const builder = new QuestDBQueryBuilder(mockClient);

      const query = builder
        .select('timestamp', 'symbol', 'close')
        .from('ohlcv_data')
        .sampleBy('1h')
        .build();

      expect(query).toContain('SAMPLE BY 1h');
    });
  });

  describe('QuestDBInfluxWriter', () => {
    it('should write OHLCV data', async () => {
      const mockClient = {
        getHttpUrl: () => 'http://localhost:9000',
      };

      const writer = new QuestDBInfluxWriter(mockClient);

      const data = [
        {
          timestamp: new Date('2022-01-01T00:00:00.000Z'),
          open: 150.0,
          high: 152.0,
          low: 149.0,
          close: 151.0,
          volume: 1000000,
        },
      ];

      // Mock fetch
      global.fetch = mock(async () => ({
        ok: true,
        status: 200,
        statusText: 'OK',
      }));

      await writer.writeOHLCV('AAPL', 'NASDAQ', data);

      expect(global.fetch).toHaveBeenCalled();
    });

    it('should write trade execution data', async () => {
      const mockClient = {
        getHttpUrl: () => 'http://localhost:9000',
      };

      const writer = new QuestDBInfluxWriter(mockClient);

      // Mock fetch
      global.fetch = mock(async () => ({
        ok: true,
        status: 200,
        statusText: 'OK',
      }));

      await writer.writeTradeExecution({
        symbol: 'AAPL',
        side: 'buy',
        quantity: 100,
        price: 151.5,
        timestamp: new Date(),
        executionTime: 50,
        orderId: 'order-123',
        strategy: 'momentum',
      });

      expect(global.fetch).toHaveBeenCalled();
    });

    it('should handle batch writes', async () => {
      const mockClient = {
        getHttpUrl: () => 'http://localhost:9000',
      };

      const writer = new QuestDBInfluxWriter(mockClient);

      // Mock fetch
      global.fetch = mock(async () => ({
        ok: true,
        status: 200,
        statusText: 'OK',
      }));

      const points = [
        {
          measurement: 'test',
          tags: { symbol: 'AAPL' },
          fields: { value: 100 },
          timestamp: new Date(),
        },
        {
          measurement: 'test',
          tags: { symbol: 'GOOGL' },
          fields: { value: 200 },
          timestamp: new Date(),
        },
      ];

      await writer.writePoints(points);

      expect(global.fetch).toHaveBeenCalled();
    });
  });

  describe('QuestDBSchemaManager', () => {
    let mockClient: any;
    let schemaManager: QuestDBSchemaManager;

    beforeEach(() => {
      mockClient = {
        query: mock(async () => ({ rows: [], count: 0 })),
      };
      schemaManager = new QuestDBSchemaManager(mockClient);
    });

    it('should create table with schema', async () => {
      const schema = schemaManager.getSchema('ohlcv_data');
      expect(schema).toBeDefined();
      expect(schema?.tableName).toBe('ohlcv_data');

      await schemaManager.createTable(schema!);

      expect(mockClient.query).toHaveBeenCalled();
      const sql = mockClient.query.mock.calls[0][0];
      expect(sql).toContain('CREATE TABLE IF NOT EXISTS ohlcv_data');
    });

    it('should check if table exists', async () => {
      mockClient.query = mock(async () => ({
        rows: [{ count: 1 }],
        count: 1,
      }));

      const exists = await schemaManager.tableExists('ohlcv_data');
      expect(exists).toBe(true);
    });

    it('should create all tables', async () => {
      await schemaManager.createAllTables();

      // Should create multiple tables
      expect(mockClient.query).toHaveBeenCalled();
      expect(mockClient.query.mock.calls.length).toBeGreaterThan(3);
    });

    it('should get table stats', async () => {
      mockClient.query = mock(async () => ({
        rows: [
          {
            row_count: 1000,
            min_timestamp: new Date('2023-01-01'),
            max_timestamp: new Date('2023-12-31'),
          },
        ],
        count: 1,
      }));

      const stats = await schemaManager.getTableStats('ohlcv_data');

      expect(stats.row_count).toBe(1000);
      expect(stats.min_timestamp).toBeDefined();
      expect(stats.max_timestamp).toBeDefined();
    });
  });

  describe('QuestDBHealthMonitor', () => {
    let mockClient: any;
    let monitor: QuestDBHealthMonitor;

    beforeEach(() => {
      mockClient = {
        query: mock(async () => ({ rows: [{ health_check: 1 }], count: 1 })),
        isPgPoolHealthy: mock(() => true),
      };
      monitor = new QuestDBHealthMonitor(mockClient);
    });

    it('should perform health check', async () => {
      const health = await monitor.performHealthCheck();

      expect(health.isHealthy).toBe(true);
      expect(health.lastCheck).toBeInstanceOf(Date);
      expect(health.responseTime).toBeGreaterThanOrEqual(0);
      expect(health.message).toBe('Connection healthy');
    });

    it('should handle failed health check', async () => {
      mockClient.query = mock(async () => {
        throw new Error('Connection failed');
      });

      const health = await monitor.performHealthCheck();

      expect(health.isHealthy).toBe(false);
      expect(health.error).toBeDefined();
      expect(health.message).toContain('Connection failed');
    });

    it('should record query metrics', () => {
      monitor.recordQuery(true, 50);
      monitor.recordQuery(true, 100);
      monitor.recordQuery(false, 200);

      const metrics = monitor.getPerformanceMetrics();

      expect(metrics.totalQueries).toBe(3);
      expect(metrics.successfulQueries).toBe(2);
      expect(metrics.failedQueries).toBe(1);
      expect(metrics.averageResponseTime).toBeCloseTo(116.67, 1);
    });

    it('should start and stop monitoring', () => {
      monitor.startMonitoring(1000);

      // Just verify it doesn't throw
      expect(true).toBe(true);

      monitor.stopMonitoring();
    });
  });
});
