import { beforeEach, describe, expect, it, mock, type Mock } from 'bun:test';
import type { CacheProvider } from '@stock-bot/cache';
import type { Logger } from '@stock-bot/logger';
import type { Queue, QueueManager } from '@stock-bot/queue';
import type { ExecutionContext, IServiceContainer, ServiceTypes } from '@stock-bot/types';
import { BaseHandler } from '../src/base/BaseHandler';
import {
  Handler,
  Operation,
  QueueSchedule,
  ScheduledOperation,
} from '../src/decorators/decorators';
import { createJobHandler } from '../src/utils/create-job-handler';

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 MockQueue = {
  add: Mock<(name: string, data: any, options?: any) => Promise<void>>;
};

type MockQueueManager = {
  getQueue: Mock<(name: string) => MockQueue>;
};

type MockCache = {
  get: Mock<(key: string) => Promise<any>>;
  set: Mock<(key: string, value: any, ttl?: number) => Promise<void>>;
  del: Mock<(key: string) => Promise<void>>;
};

// Mock service container
const createMockServices = (): IServiceContainer => {
  const mockLogger: MockLogger = {
    info: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
    debug: mock(() => {}),
  };

  const mockQueue: MockQueue = {
    add: mock(() => Promise.resolve()),
  };

  const mockQueueManager: MockQueueManager = {
    getQueue: mock(() => mockQueue),
  };

  return {
    logger: mockLogger as unknown as ServiceTypes['logger'],
    cache: null,
    globalCache: null,
    queueManager: mockQueueManager as unknown as ServiceTypes['queueManager'],
    proxy: null,
    browser: null,
    mongodb: null,
    postgres: null,
    questdb: null,
    queue: mockQueue as unknown as ServiceTypes['queue'],
  };
};

describe('BaseHandler', () => {
  let mockServices: IServiceContainer;

  beforeEach(() => {
    mockServices = createMockServices();
  });

  it('should initialize with services', () => {
    const handler = new BaseHandler(mockServices, 'test-handler');
    expect(handler).toBeDefined();
    expect(handler.logger).toBeDefined();
  });

  it('should execute operations', async () => {
    @Handler('test')
    class TestHandler extends BaseHandler {
      @Operation('testOp')
      async handleTestOp(payload: unknown) {
        return { result: 'success', payload };
      }
    }

    const handler = new TestHandler(mockServices);
    const context: ExecutionContext = {
      type: 'queue',
      metadata: { source: 'test' },
    };

    const result = await handler.execute('testOp', { data: 'test' }, context);
    expect(result).toEqual({ result: 'success', payload: { data: 'test' } });
  });

  it('should throw for unknown operation', async () => {
    @Handler('test')
    class TestHandler extends BaseHandler {}

    const handler = new TestHandler(mockServices);
    const context: ExecutionContext = {
      type: 'queue',
      metadata: {},
    };

    await expect(handler.execute('unknown', {}, context)).rejects.toThrow(
      'Unknown operation: unknown'
    );
  });

  it('should schedule operations', async () => {
    const mockQueue: MockQueue = {
      add: mock(() => Promise.resolve()),
    };
    const mockQueueManager: MockQueueManager = {
      getQueue: mock(() => mockQueue),
    };
    mockServices.queueManager = mockQueueManager as unknown as ServiceTypes['queueManager'];

    const handler = new BaseHandler(mockServices, 'test-handler');

    await handler.scheduleOperation('test-op', { data: 'test' }, { delay: 1000 });

    expect(mockQueueManager.getQueue).toHaveBeenCalledWith('test-handler');
    expect(mockQueue.add).toHaveBeenCalledWith(
      'test-op',
      {
        handler: 'test-handler',
        operation: 'test-op',
        payload: { data: 'test' },
      },
      { delay: 1000 }
    );
  });

  describe('cache helpers', () => {
    it('should handle cache operations with namespace', async () => {
      const mockCache: MockCache = {
        set: mock(() => Promise.resolve()),
        get: mock(() => Promise.resolve('cached-value')),
        del: mock(() => Promise.resolve()),
      };

      mockServices.cache = mockCache as unknown as ServiceTypes['cache'];
      const handler = new BaseHandler(mockServices, 'my-handler');

      await handler['cacheSet']('key', 'value', 3600);
      expect(mockCache.set).toHaveBeenCalledWith('my-handler:key', 'value', 3600);

      const result = await handler['cacheGet']('key');
      expect(mockCache.get).toHaveBeenCalledWith('my-handler:key');
      expect(result).toBe('cached-value');

      await handler['cacheDel']('key');
      expect(mockCache.del).toHaveBeenCalledWith('my-handler:key');
    });

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

      await expect(handler['cacheSet']('key', 'value')).resolves.toBeUndefined();
      await expect(handler['cacheGet']('key')).resolves.toBeNull();
      await expect(handler['cacheDel']('key')).resolves.toBeUndefined();
    });
  });

  describe('metadata extraction', () => {
    it('should extract metadata from decorated class', () => {
      @Handler('metadata-test')
      class MetadataHandler extends BaseHandler {
        @Operation('op1')
        async operation1() {}

        @Operation('op2')
        async operation2() {}

        @ScheduledOperation('scheduled-op', '* * * * *', { priority: 10 })
        async scheduledOp() {}
      }

      const metadata = MetadataHandler.extractMetadata();

      expect(metadata).toBeDefined();
      expect(metadata!.name).toBe('metadata-test');
      expect(metadata!.operations).toContain('op1');
      expect(metadata!.operations).toContain('op2');
      expect(metadata!.operations).toContain('scheduled-op');
      expect(metadata!.scheduledJobs).toHaveLength(1);
      expect(metadata!.scheduledJobs![0]).toMatchObject({
        operation: 'scheduled-op',
        cronPattern: '* * * * *',
        priority: 10,
      });
    });
  });
});

describe('Decorators', () => {
  it('should apply Handler decorator', () => {
    @Handler('test-handler')
    class TestClass {}

    const decoratedClass = TestClass as typeof TestClass & { __handlerName: string };
    expect(decoratedClass.__handlerName).toBe('test-handler');
  });

  it('should apply Operation decorator', () => {
    class TestClass {
      @Operation('my-operation')
      myMethod() {}
    }

    const decoratedClass = TestClass as typeof TestClass & {
      __operations: Array<{ name: string; method: string }>;
    };
    const operations = decoratedClass.__operations;
    expect(operations).toBeDefined();
    expect(operations).toHaveLength(1);
    expect(operations[0]).toMatchObject({
      name: 'my-operation',
      method: 'myMethod',
    });
  });

  it('should apply ScheduledOperation decorator with options', () => {
    class TestClass {
      @ScheduledOperation('scheduled-task', '0 * * * *', {
        priority: 8,
        payload: { action: 'test' },
        batch: { size: 100, delayInHours: 1 },
      })
      scheduledMethod() {}
    }

    const decoratedClass = TestClass as typeof TestClass & {
      __schedules: Array<{
        operation: string;
        cronPattern: string;
        priority: number;
        payload: any;
        batch: { size: number; delayInHours: number };
      }>;
    };
    const schedules = decoratedClass.__schedules;
    expect(schedules).toBeDefined();
    expect(schedules).toHaveLength(1);
    expect(schedules[0]).toMatchObject({
      operation: 'scheduledMethod',
      cronPattern: '0 * * * *',
      priority: 8,
      payload: { action: 'test' },
      batch: { size: 100, delayInHours: 1 },
    });
  });

  it('should apply QueueSchedule decorator', () => {
    class TestClass {
      @QueueSchedule('15 * * * *', { priority: 3 })
      queueMethod() {}
    }

    const decoratedClass = TestClass as typeof TestClass & {
      __schedules: Array<{
        operation: string;
        cronPattern: string;
        priority: number;
      }>;
    };
    const schedules = decoratedClass.__schedules;
    expect(schedules).toBeDefined();
    expect(schedules[0]).toMatchObject({
      operation: 'queueMethod',
      cronPattern: '15 * * * *',
      priority: 3,
    });
  });
});

describe('createJobHandler', () => {
  it('should create a job handler', async () => {
    type TestPayload = { data: string };
    type TestResult = { success: boolean; payload: TestPayload };

    const handlerFn = mock(
      async (payload: TestPayload): Promise<TestResult> => ({
        success: true,
        payload,
      })
    );
    const jobHandler = createJobHandler(handlerFn);

    const result = await jobHandler({ data: 'test' });

    expect(handlerFn).toHaveBeenCalledWith({ data: 'test' });
    expect(result).toEqual({ success: true, payload: { data: 'test' } });
  });

  it('should handle errors in job handler', async () => {
    const handlerFn = mock(async () => {
      throw new Error('Handler error');
    });
    const jobHandler = createJobHandler(handlerFn);

    await expect(jobHandler({})).rejects.toThrow('Handler error');
  });
});
