import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test';
import type { Collection, Db, MongoClient } from 'mongodb';
import type { Pool, QueryResult } from 'pg';
import type { SimpleBrowser } from '@stock-bot/browser';
import type { CacheProvider } from '@stock-bot/cache';
import type { Logger } from '@stock-bot/logger';
import type { SimpleProxyManager } from '@stock-bot/proxy';
import type { Queue, QueueManager } from '@stock-bot/queue';
import type { ExecutionContext, IServiceContainer, ServiceTypes } from '@stock-bot/types';
import { BaseHandler, ScheduledHandler } from '../src/base/BaseHandler';
import { Handler, Operation } from '../src/decorators/decorators';

type MockQueue = {
  add: Mock<(name: string, data: any) => Promise<{ id: string }>>;
  getName: Mock<() => string>;
};

type MockQueueManager = {
  getQueue: Mock<(name: string) => MockQueue | null>;
  createQueue: Mock<(name: string) => MockQueue>;
  hasQueue: Mock<(name: string) => boolean>;
  sendToQueue: Mock<(service: string, handler: string, data: any) => Promise<string>>;
};

type MockCache = {
  get: Mock<(key: string) => Promise<any>>;
  set: Mock<(key: string, value: any, ttl?: number) => Promise<void>>;
  del: Mock<(key: string) => Promise<void>>;
  clear: Mock<() => Promise<void>>;
  has: Mock<(key: string) => Promise<boolean>>;
  keys: Mock<(pattern?: string) => Promise<string[]>>;
  ttl: Mock<(key: string) => Promise<number>>;
  type: 'memory';
};

type MockLogger = {
  info: Mock<(message: string, meta?: any) => void>;
  error: Mock<(message: string, meta?: any) => void>;
  warn: Mock<(message: string, meta?: any) => void>;
  debug: Mock<(message: string, meta?: any) => void>;
};

type MockBrowser = {
  scrape: Mock<(url: string) => Promise<{ data: string }>>;
};

type MockProxy = {
  getProxy: Mock<() => { host: string; port: number }>;
};

type MockPostgres = {
  query: Mock<(text: string, values?: any[]) => Promise<QueryResult>>;
};

type MockMongoDB = {
  db: Mock<
    (name?: string) => {
      collection: Mock<
        (name: string) => {
          find: Mock<(filter: any) => { toArray: Mock<() => Promise<any[]>> }>;
          insertOne: Mock<(doc: any) => Promise<{ insertedId: string }>>;
        }
      >;
    }
  >;
};

describe('BaseHandler', () => {
  let mockServices: IServiceContainer;
  let mockContext: ExecutionContext;
  let mockQueue: MockQueue;
  let mockQueueManager: MockQueueManager;
  let mockCache: MockCache;
  let mockLogger: MockLogger;

  beforeEach(() => {
    mockQueue = {
      add: mock(async () => ({ id: 'job-456' })),
      getName: mock(() => 'test-queue'),
    };

    mockQueueManager = {
      getQueue: mock(() => mockQueue),
      createQueue: mock(() => mockQueue),
      hasQueue: mock(() => true),
      sendToQueue: mock(async () => 'job-123'),
    };

    mockCache = {
      get: mock(async () => null),
      set: mock(async () => {}),
      del: mock(async () => {}),
      clear: mock(async () => {}),
      has: mock(async () => false),
      keys: mock(async () => []),
      ttl: mock(async () => -1),
      type: 'memory',
    };

    mockLogger = {
      info: mock(() => {}),
      error: mock(() => {}),
      warn: mock(() => {}),
      debug: mock(() => {}),
    };

    const mockBrowser: MockBrowser = {
      scrape: mock(async () => ({ data: 'scraped' })),
    };

    const mockProxy: MockProxy = {
      getProxy: mock(() => ({ host: 'proxy.example.com', port: 8080 })),
    };

    const mockPostgres: MockPostgres = {
      query: mock(async () => ({ rows: [], rowCount: 0 }) as QueryResult),
    };

    const mockMongoDB: MockMongoDB = {
      db: mock(() => ({
        collection: mock(() => ({
          find: mock(() => ({ toArray: mock(async () => []) })),
          insertOne: mock(async () => ({ insertedId: 'id-123' })),
        })),
      })),
    };

    mockServices = {
      cache: mockCache as unknown as ServiceTypes['cache'],
      globalCache: { ...mockCache } as unknown as ServiceTypes['globalCache'],
      queueManager: mockQueueManager as unknown as ServiceTypes['queueManager'],
      proxy: mockProxy as unknown as ServiceTypes['proxy'],
      browser: mockBrowser as unknown as ServiceTypes['browser'],
      mongodb: mockMongoDB as unknown as ServiceTypes['mongodb'],
      postgres: mockPostgres as unknown as ServiceTypes['postgres'],
      questdb: null,
      logger: mockLogger as unknown as ServiceTypes['logger'],
      queue: mockQueue as unknown as ServiceTypes['queue'],
    };

    mockContext = {
      logger: mockLogger as unknown as Logger,
      traceId: 'trace-123',
      handlerName: 'TestHandler',
      operationName: 'testOperation',
      metadata: {},
      startTime: new Date(),
      container: {
        resolve: mock((name: string) => mockServices[name as keyof IServiceContainer]),
        resolveAsync: mock(async (name: string) => mockServices[name as keyof IServiceContainer]),
      },
    };

    // Reset all mocks
    Object.values(mockServices).forEach(service => {
      if (service && typeof service === 'object') {
        Object.values(service).forEach(method => {
          if (typeof method === 'function' && 'mockClear' in method) {
            (method as unknown as Mock<any>).mockClear();
          }
        });
      }
    });
  });

  class TestHandler extends BaseHandler {
    constructor() {
      super(mockServices, 'TestHandler');
    }

    async testOperation(data: unknown): Promise<{ processed: unknown }> {
      return { processed: data };
    }
  }

  describe('service access', () => {
    it('should provide access to cache service', async () => {
      const handler = new TestHandler();

      await handler.cache.set('key', 'value');

      expect(mockCache.set).toHaveBeenCalledWith('key', 'value');
    });

    it('should have logger initialized', () => {
      const handler = new TestHandler();

      expect(handler.logger).toBeDefined();
      // Logger is created by getLogger, not from mockServices
    });

    it('should provide access to queue service', () => {
      const handler = new TestHandler();

      expect(handler.queue).toBeDefined();
      expect(mockQueue.getName()).toBe('test-queue');
    });

    it('should provide access to mongodb', () => {
      const handler = new TestHandler();

      expect(handler.mongodb).toBe(mockServices.mongodb);
    });

    it('should provide access to postgres', async () => {
      const handler = new TestHandler();

      const result = await handler.postgres.query('SELECT 1');

      expect(result.rows).toEqual([]);
      expect(mockServices.postgres.query).toHaveBeenCalledWith('SELECT 1');
    });

    it('should provide access to browser', async () => {
      const handler = new TestHandler();

      const result = await handler.browser.scrape('https://example.com');

      expect(result).toEqual({ data: 'scraped' });
      expect((mockServices.browser as unknown as MockBrowser).scrape).toHaveBeenCalledWith(
        'https://example.com'
      );
    });

    it('should provide access to proxy manager', () => {
      const handler = new TestHandler();

      const proxy = handler.proxy.getProxy();

      expect(proxy).toEqual({ host: 'proxy.example.com', port: 8080 });
    });
  });

  describe('caching utilities', () => {
    it('should set and get cache values with handler namespace', async () => {
      const handler = new TestHandler();
      mockCache.set.mockClear();
      mockCache.get.mockClear();

      // Test cacheSet
      await handler['cacheSet']('testKey', 'testValue', 3600);
      expect(mockCache.set).toHaveBeenCalledWith('TestHandler:testKey', 'testValue', 3600);

      // Test cacheGet
      mockCache.get.mockImplementation(async () => 'cachedValue');
      const result = await handler['cacheGet']('testKey');
      expect(mockCache.get).toHaveBeenCalledWith('TestHandler:testKey');
      expect(result).toBe('cachedValue');
    });

    it('should delete cache values with handler namespace', async () => {
      const handler = new TestHandler();
      mockCache.del.mockClear();

      await handler['cacheDel']('testKey');
      expect(mockCache.del).toHaveBeenCalledWith('TestHandler:testKey');
    });

    it('should handle null cache gracefully', async () => {
      mockServices.cache = null;
      const handler = new TestHandler();

      // Should not throw when cache is null
      await expect(handler['cacheSet']('key', 'value')).resolves.toBeUndefined();
      await expect(handler['cacheGet']('key')).resolves.toBeNull();
      await expect(handler['cacheDel']('key')).resolves.toBeUndefined();
    });
  });

  describe('scheduling', () => {
    it('should schedule operations', async () => {
      const handler = new TestHandler();
      mockQueueManager.hasQueue.mockClear();
      mockQueue.add.mockClear();

      await handler.scheduleOperation('processData', { data: 'test' }, { delay: 5000 });

      expect(mockQueueManager.getQueue).toHaveBeenCalledWith('TestHandler');
      expect(mockQueue.add).toHaveBeenCalledWith(
        'processData',
        {
          handler: 'TestHandler',
          operation: 'processData',
          payload: { data: 'test' },
        },
        { delay: 5000 }
      );
    });
  });

  describe('HTTP client', () => {
    it('should provide http methods', () => {
      const handler = new TestHandler();

      const http = handler['http'];
      expect(http).toBeDefined();
      expect(http.get).toBeDefined();
      expect(http.post).toBeDefined();
      expect(http.put).toBeDefined();
      expect(http.delete).toBeDefined();
    });
  });

  describe('handler metadata', () => {
    it('should extract handler metadata', () => {
      // For metadata extraction, we need a decorated handler
      @Handler('MetadataTestHandler')
      class MetadataTestHandler extends BaseHandler {
        @Operation('testOp')
        async handleTestOp() {
          return { result: 'success' };
        }
      }

      const metadata = MetadataTestHandler.extractMetadata();
      expect(metadata).toBeDefined();
      expect(metadata!.name).toBe('MetadataTestHandler');
      expect(metadata!.operations).toContain('testOp');
    });
  });

  describe('lifecycle hooks', () => {
    class LifecycleHandler extends BaseHandler {
      onInitCalled = false;
      onStartCalled = false;
      onStopCalled = false;
      onDisposeCalled = false;

      constructor() {
        super(mockServices, 'LifecycleHandler');
      }

      async onInit(): Promise<void> {
        this.onInitCalled = true;
      }

      async onStart(): Promise<void> {
        this.onStartCalled = true;
      }

      async onStop(): Promise<void> {
        this.onStopCalled = true;
      }

      async onDispose(): Promise<void> {
        this.onDisposeCalled = true;
      }
    }

    it('should call lifecycle hooks', async () => {
      const handler = new LifecycleHandler();

      await handler.onInit();
      expect(handler.onInitCalled).toBe(true);

      await handler.onStart();
      expect(handler.onStartCalled).toBe(true);

      await handler.onStop();
      expect(handler.onStopCalled).toBe(true);

      await handler.onDispose();
      expect(handler.onDisposeCalled).toBe(true);
    });
  });
});

describe('ScheduledHandler', () => {
  const mockQueue: MockQueue = {
    add: mock(async () => ({ id: 'job-456' })),
    getName: mock(() => 'test-queue'),
  };

  const mockServices: IServiceContainer = {
    cache: { type: 'memory' } as unknown as ServiceTypes['cache'],
    globalCache: { type: 'memory' } as unknown as ServiceTypes['globalCache'],
    queueManager: {
      getQueue: () => mockQueue,
    } as unknown as ServiceTypes['queueManager'],
    proxy: null as unknown as ServiceTypes['proxy'],
    browser: null as unknown as ServiceTypes['browser'],
    mongodb: null as unknown as ServiceTypes['mongodb'],
    postgres: null as unknown as ServiceTypes['postgres'],
    questdb: null,
    logger: null as unknown as ServiceTypes['logger'],
    queue: mockQueue as unknown as ServiceTypes['queue'],
  };

  class TestScheduledHandler extends ScheduledHandler {
    constructor() {
      super(mockServices, 'TestScheduledHandler');
    }

    getScheduledJobs() {
      return [
        {
          name: 'dailyJob',
          schedule: '0 0 * * *',
          handler: 'processDailyData',
        },
        {
          name: 'hourlyJob',
          schedule: '0 * * * *',
          handler: 'processHourlyData',
          options: {
            timezone: 'UTC',
          },
        },
      ];
    }

    async processDailyData(): Promise<{ processed: string }> {
      return { processed: 'daily' };
    }

    async processHourlyData(): Promise<{ processed: string }> {
      return { processed: 'hourly' };
    }
  }

  it('should define scheduled jobs', () => {
    const handler = new TestScheduledHandler();

    const jobs = handler.getScheduledJobs();

    expect(jobs).toHaveLength(2);
    expect(jobs[0]).toEqual({
      name: 'dailyJob',
      schedule: '0 0 * * *',
      handler: 'processDailyData',
    });
    expect(jobs[1]).toEqual({
      name: 'hourlyJob',
      schedule: '0 * * * *',
      handler: 'processHourlyData',
      options: {
        timezone: 'UTC',
      },
    });
  });

  it('should be a BaseHandler', () => {
    const handler = new TestScheduledHandler();

    expect(handler).toBeInstanceOf(BaseHandler);
    expect(handler).toBeInstanceOf(ScheduledHandler);
  });
});
