import { asFunction, createContainer, type AwilixContainer } from 'awilix';
import { beforeEach, describe, expect, it, mock, spyOn } from 'bun:test';
import type { HandlerRegistry } from '@stock-bot/handler-registry';
import * as logger from '@stock-bot/logger';
import type { ExecutionContext, IHandler } from '@stock-bot/types';
import { HandlerScanner } from '../src/scanner/handler-scanner';

// Mock handler class
class MockHandler implements IHandler {
  static __handlerName = 'mockHandler';
  static __operations = [
    { name: 'processData', method: 'processData' },
    { name: 'validateData', method: 'validateData' },
  ];
  static __schedules = [
    {
      operation: 'processData',
      cronPattern: '0 * * * *',
      priority: 5,
      immediately: false,
      description: 'Process data every hour',
      payload: { type: 'hourly' },
    },
  ];
  static __disabled = false;

  constructor(private serviceContainer: any) {}

  async execute(operation: string, payload: any, context: ExecutionContext): Promise<any> {
    switch (operation) {
      case 'processData':
        return { processed: true, data: payload };
      case 'validateData':
        return { valid: true, data: payload };
      default:
        throw new Error(`Unknown operation: ${operation}`);
    }
  }
}

// Disabled handler for testing
class DisabledHandler extends MockHandler {
  static __handlerName = 'disabledHandler';
  static __disabled = true;
}

// Handler without metadata
class InvalidHandler {
  constructor() {}
  execute() {}
}

describe('HandlerScanner', () => {
  let scanner: HandlerScanner;
  let mockRegistry: HandlerRegistry;
  let container: AwilixContainer;
  let mockLogger: any;

  beforeEach(() => {
    // Create mock logger
    mockLogger = {
      info: mock(() => {}),
      debug: mock(() => {}),
      error: mock(() => {}),
      warn: mock(() => {}),
    };

    // Mock getLogger to return our mock logger
    spyOn(logger, 'getLogger').mockReturnValue(mockLogger);

    // Create mock registry
    mockRegistry = {
      register: mock(() => {}),
      getHandler: mock(() => null),
      getHandlerMetadata: mock(() => null),
      getAllHandlers: mock(() => []),
      clear: mock(() => {}),
    } as unknown as HandlerRegistry;

    // Create container
    container = createContainer();

    // Create scanner
    scanner = new HandlerScanner(mockRegistry, container, {
      serviceName: 'test-service',
      autoRegister: true,
    });
  });

  describe('scanHandlers', () => {
    it('should handle empty patterns gracefully', async () => {
      await scanner.scanHandlers([]);

      // Should complete without errors
      expect(mockLogger.info).toHaveBeenCalledWith('Starting handler scan', { patterns: [] });
    });

    it('should handle file scan errors gracefully', async () => {
      // We'll test that the scanner handles errors properly
      // by calling internal methods directly
      const filePath = '/non-existent-file.ts';

      // This should not throw
      await (scanner as any).scanFile(filePath);

      expect(mockLogger.error).toHaveBeenCalled();
    });
  });

  describe('registerHandlerClass', () => {
    it('should register a handler class with registry and container', () => {
      scanner.registerHandlerClass(MockHandler);

      // Check registry registration
      expect(mockRegistry.register).toHaveBeenCalledWith(
        {
          name: 'mockHandler',
          service: 'test-service',
          operations: [
            { name: 'processData', method: 'processData' },
            { name: 'validateData', method: 'validateData' },
          ],
          schedules: [
            {
              operation: 'processData',
              cronPattern: '0 * * * *',
              priority: 5,
              immediately: false,
              description: 'Process data every hour',
              payload: { type: 'hourly' },
            },
          ],
        },
        expect.objectContaining({
          name: 'mockHandler',
          operations: expect.any(Object),
          scheduledJobs: expect.arrayContaining([
            expect.objectContaining({
              type: 'mockHandler-processData',
              operation: 'processData',
              cronPattern: '0 * * * *',
              priority: 5,
              immediately: false,
              description: 'Process data every hour',
              payload: { type: 'hourly' },
            }),
          ]),
        })
      );

      // Check container registration
      expect(container.hasRegistration('mockHandler')).toBe(true);
    });

    it('should skip disabled handlers', () => {
      scanner.registerHandlerClass(DisabledHandler);

      expect(mockRegistry.register).not.toHaveBeenCalled();
      expect(container.hasRegistration('disabledHandler')).toBe(false);
    });

    it('should handle handlers without schedules', () => {
      class NoScheduleHandler extends MockHandler {
        static __handlerName = 'noScheduleHandler';
        static __schedules = [];
      }

      scanner.registerHandlerClass(NoScheduleHandler);

      expect(mockRegistry.register).toHaveBeenCalledWith(
        expect.objectContaining({
          schedules: [],
        }),
        expect.objectContaining({
          scheduledJobs: [],
        })
      );
    });

    it('should use custom service name when provided', () => {
      scanner.registerHandlerClass(MockHandler, { serviceName: 'custom-service' });

      expect(mockRegistry.register).toHaveBeenCalledWith(
        expect.objectContaining({
          service: 'custom-service',
        }),
        expect.any(Object)
      );
    });

    it('should not register with container when autoRegister is false', () => {
      scanner = new HandlerScanner(mockRegistry, container, {
        serviceName: 'test-service',
        autoRegister: false,
      });

      scanner.registerHandlerClass(MockHandler);

      expect(mockRegistry.register).toHaveBeenCalled();
      expect(container.hasRegistration('mockHandler')).toBe(false);
    });
  });

  describe('handler validation', () => {
    it('should identify valid handlers', () => {
      const isHandler = (scanner as any).isHandler;

      expect(isHandler(MockHandler)).toBe(true);
      expect(isHandler(InvalidHandler)).toBe(false);
      expect(isHandler({})).toBe(false);
      expect(isHandler('not a function')).toBe(false);
      expect(isHandler(null)).toBe(false);
    });

    it('should handle handlers with batch configuration', () => {
      class BatchHandler extends MockHandler {
        static __handlerName = 'batchHandler';
        static __schedules = [
          {
            operation: 'processBatch',
            cronPattern: '*/5 * * * *',
            priority: 10,
            batch: {
              size: 100,
              window: 60000,
            },
          },
        ];
      }

      scanner.registerHandlerClass(BatchHandler);

      expect(mockRegistry.register).toHaveBeenCalledWith(
        expect.any(Object),
        expect.objectContaining({
          scheduledJobs: expect.arrayContaining([
            expect.objectContaining({
              batch: {
                size: 100,
                window: 60000,
              },
            }),
          ]),
        })
      );
    });
  });

  describe('getDiscoveredHandlers', () => {
    it('should return all discovered handlers', () => {
      scanner.registerHandlerClass(MockHandler);

      const discovered = scanner.getDiscoveredHandlers();

      expect(discovered.size).toBe(1);
      expect(discovered.get('mockHandler')).toBe(MockHandler);
    });

    it('should return a copy of the map', () => {
      scanner.registerHandlerClass(MockHandler);

      const discovered1 = scanner.getDiscoveredHandlers();
      const discovered2 = scanner.getDiscoveredHandlers();

      expect(discovered1).not.toBe(discovered2);
      expect(discovered1.get('mockHandler')).toBe(discovered2.get('mockHandler'));
    });
  });

  describe('operation handler creation', () => {
    it('should create job handlers for operations', () => {
      scanner.registerHandlerClass(MockHandler);

      const registrationCall = (mockRegistry.register as any).mock.calls[0];
      const configuration = registrationCall[1];

      expect(configuration.operations).toHaveProperty('processData');
      expect(configuration.operations).toHaveProperty('validateData');
      expect(typeof configuration.operations.processData).toBe('function');
    });

    it('should resolve handler from container when executing operations', async () => {
      // Register handler with container
      container.register({
        serviceContainer: asFunction(() => ({})).singleton(),
      });

      scanner.registerHandlerClass(MockHandler);

      // Create handler instance
      const handlerInstance = container.resolve<IHandler>('mockHandler');

      // Test execution
      const context: ExecutionContext = {
        type: 'queue',
        metadata: { source: 'test', timestamp: Date.now() },
      };

      const result = await handlerInstance.execute('processData', { test: true }, context);

      expect(result).toEqual({ processed: true, data: { test: true } });
    });
  });

  describe('module scanning', () => {
    it('should handle modules with multiple exports', () => {
      const mockModule = {
        Handler1: MockHandler,
        Handler2: class SecondHandler extends MockHandler {
          static __handlerName = 'secondHandler';
        },
        notAHandler: { some: 'object' },
        helperFunction: () => {},
      };

      (scanner as any).registerHandlersFromModule(mockModule, 'test.ts');

      expect(mockRegistry.register).toHaveBeenCalledTimes(2);
      expect(mockRegistry.register).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'mockHandler' }),
        expect.any(Object)
      );
      expect(mockRegistry.register).toHaveBeenCalledWith(
        expect.objectContaining({ name: 'secondHandler' }),
        expect.any(Object)
      );
    });

    it('should handle empty modules', () => {
      const mockModule = {};

      (scanner as any).registerHandlersFromModule(mockModule, 'empty.ts');

      expect(mockRegistry.register).not.toHaveBeenCalled();
    });
  });
});
