/**
 * @fileoverview Tests for handler factory functions
 */

import { vi, MockedFunction } from 'vitest';
import {
  createBaseHandlerFactory,
  createDefaultHandlerDeps,
  wrapInApiResponse,
  createErrorResponse,
  isApiResponse,
} from '../../../handlers/base/handler.factory.js';
import { BaseHandlerDeps } from '../../../handlers/base/handler.interface.js';
import { DeepSourceClientFactory } from '../../../client/factory.js';
import { Logger } from '../../../utils/logging/logger.js';

// Mock dependencies
vi.mock('../../../client/factory.js');
vi.mock('../../../config/index.js', () => ({
  getApiKey: vi.fn(() => 'test-api-key'),
}));
vi.mock('../../../utils/logging/logger.js', () => ({
  createLogger: vi.fn(() => ({
    info: vi.fn(),
    debug: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  })),
}));

// Set up test environment
beforeEach(() => {
  process.env.DEEPSOURCE_API_KEY = 'test-api-key';
});

afterEach(() => {
  delete process.env.DEEPSOURCE_API_KEY;
});

describe('handler.factory', () => {
  describe('createBaseHandlerFactory', () => {
    let mockDeps: BaseHandlerDeps;
    let mockLogger: {
      info: MockedFunction<(message: string, data?: unknown) => void>;
      error: MockedFunction<(message: string, error?: unknown) => void>;
    };

    beforeEach(() => {
      mockLogger = {
        info: vi.fn(),
        error: vi.fn(),
      };

      mockDeps = {
        clientFactory: new DeepSourceClientFactory('test-key'),
        logger: mockLogger as Logger,
        getApiKey: vi.fn(() => 'test-api-key'),
      };
    });

    it('should create a handler factory with logging', async () => {
      const handlerLogic = vi.fn(async (deps, params) => ({
        data: `Hello ${params.name}`,
      }));

      const factory = createBaseHandlerFactory('test_handler', handlerLogic);
      const handler = factory(mockDeps);

      const result = await handler({ name: 'World' });

      expect(handlerLogic).toHaveBeenCalledWith(mockDeps, { name: 'World' });
      expect(result).toEqual({ data: 'Hello World' });

      // Check logging
      expect(mockLogger.info).toHaveBeenCalledWith(
        'Handler test_handler invoked',
        expect.objectContaining({ hasParams: true, params: { name: 'World' } })
      );
      expect(mockLogger.info).toHaveBeenCalledWith(
        'Handler test_handler completed successfully',
        expect.objectContaining({ duration: expect.any(Number) })
      );
    });

    it('should log handler errors and re-throw', async () => {
      const error = new Error('Handler failed');
      const handlerLogic = vi.fn(async () => {
        throw error;
      });

      const factory = createBaseHandlerFactory('error_handler', handlerLogic);
      const handler = factory(mockDeps);

      await expect(handler({})).rejects.toThrow('Handler failed');

      expect(mockLogger.error).toHaveBeenCalledWith(
        'Handler error_handler failed',
        expect.objectContaining({
          duration: expect.any(Number),
          errorType: 'Error',
          errorMessage: 'Handler failed',
          errorStack: expect.stringContaining('Error: Handler failed'),
        })
      );
    });

    it('should handle non-Error throws', async () => {
      const handlerLogic = vi.fn(async () => {
        throw 'String error';
      });

      const factory = createBaseHandlerFactory('string_error_handler', handlerLogic);
      const handler = factory(mockDeps);

      await expect(handler({})).rejects.toBe('String error');

      expect(mockLogger.error).toHaveBeenCalledWith(
        'Handler string_error_handler failed',
        expect.objectContaining({
          errorType: 'string',
          errorMessage: 'String error',
          errorStack: undefined,
        })
      );
    });

    it('should log result count for array data', async () => {
      const handlerLogic = vi.fn(async () => ({
        data: ['item1', 'item2', 'item3'],
      }));

      const factory = createBaseHandlerFactory('array_handler', handlerLogic);
      const handler = factory(mockDeps);

      await handler({});

      expect(mockLogger.info).toHaveBeenCalledWith(
        'Handler array_handler completed successfully',
        expect.objectContaining({
          duration: expect.any(Number),
          resultCount: 3,
        })
      );
    });
  });

  describe('createDefaultHandlerDeps', () => {
    it('should create default dependencies', () => {
      const deps = createDefaultHandlerDeps();

      expect(deps.clientFactory).toBeInstanceOf(DeepSourceClientFactory);
      expect(deps.logger).toBeDefined();
      expect(deps.getApiKey).toBeDefined();
      expect(deps.getApiKey()).toBe('test-api-key');
    });

    it('should accept overrides', () => {
      const customLogger = {
        info: vi.fn(),
        debug: vi.fn(),
        warn: vi.fn(),
        error: vi.fn(),
      };

      const deps = createDefaultHandlerDeps({
        logger: customLogger as Logger,
      });

      expect(deps.logger).toBe(customLogger);
    });
  });

  describe('wrapInApiResponse', () => {
    it('should wrap data in ApiResponse format', () => {
      const data = { message: 'Hello', value: 42 };
      const response = wrapInApiResponse(data);

      expect(response).toEqual({
        content: [
          {
            type: 'text',
            text: JSON.stringify(data, null, 2),
          },
        ],
      });
    });

    it('should handle arrays', () => {
      const data = ['item1', 'item2'];
      const response = wrapInApiResponse(data);

      expect(response.content[0].text).toBe('[\n  "item1",\n  "item2"\n]');
    });

    it('should handle null and undefined', () => {
      expect(wrapInApiResponse(null).content[0].text).toBe('null');
      expect(wrapInApiResponse(undefined).content[0].text).toBe(undefined);
    });
  });

  describe('createErrorResponse', () => {
    it('should handle Error instances', () => {
      const error = new Error('Something went wrong');
      const response = createErrorResponse(error);

      expect(response).toEqual({
        isError: true,
        content: [
          {
            type: 'text',
            text: JSON.stringify({
              code: 'HANDLER_ERROR',
              message: 'Something went wrong',
            }),
          },
        ],
      });
    });

    it('should handle Error with code property', () => {
      const error = new Error('API failed');
      (error as Record<string, unknown>).code = 'API_ERROR';

      const response = createErrorResponse(error);

      expect(response.content[0].text).toContain('"code":"API_ERROR"');
      expect(response.content[0].text).toContain('"message":"API failed"');
    });

    it('should handle Error with details and suggestions', () => {
      const error = new Error('Validation failed');
      (error as Record<string, unknown>).code = 'VALIDATION_ERROR';
      (error as Record<string, unknown>).details = { field: 'email', reason: 'invalid format' };
      (error as Record<string, unknown>).suggestions = ['Check email format', 'Use valid domain'];

      const response = createErrorResponse(error);
      const errorData = JSON.parse(response.content[0].text);

      expect(errorData).toEqual({
        code: 'VALIDATION_ERROR',
        message: 'Validation failed',
        details: { field: 'email', reason: 'invalid format' },
        suggestions: ['Check email format', 'Use valid domain'],
      });
    });

    it('should handle non-Error objects', () => {
      const response = createErrorResponse({ error: 'Custom error object' });

      expect(response.content[0].text).toContain('"code":"UNKNOWN_ERROR"');
      expect(response.content[0].text).toContain('[object Object]');
    });

    it('should handle string errors', () => {
      const response = createErrorResponse('Simple string error');

      expect(response.content[0].text).toContain('"code":"UNKNOWN_ERROR"');
      expect(response.content[0].text).toContain('"message":"Simple string error"');
    });

    it('should handle null/undefined errors', () => {
      const nullResponse = createErrorResponse(null);
      const undefinedResponse = createErrorResponse(undefined);

      expect(nullResponse.content[0].text).toContain('"code":"UNKNOWN_ERROR"');
      expect(nullResponse.content[0].text).toContain('"message":"null"');

      expect(undefinedResponse.content[0].text).toContain('"code":"UNKNOWN_ERROR"');
      // undefined becomes "undefined" string
      const undefinedData = JSON.parse(undefinedResponse.content[0].text);
      expect(undefinedData.code).toBe('UNKNOWN_ERROR');
      expect(undefinedData.message).toBe('undefined');
    });

    it('should use custom default message', () => {
      const response = createErrorResponse(null, 'Custom error message');

      // null still produces "null" as the message when stringified
      expect(response.content[0].text).toContain('"message":"null"');
    });

    it('should handle Error with empty message', () => {
      const error = new Error('');
      const response = createErrorResponse(error, 'Default message');

      expect(response.content[0].text).toContain('"message":"Default message"');
    });
  });

  describe('isApiResponse', () => {
    it('should return true for valid ApiResponse', () => {
      const validResponse = {
        content: [{ type: 'text', text: 'data' }],
      };

      expect(isApiResponse(validResponse)).toBe(true);
    });

    it('should return true for ApiResponse with isError', () => {
      const errorResponse = {
        content: [{ type: 'text', text: 'error' }],
        isError: true,
      };

      expect(isApiResponse(errorResponse)).toBe(true);
    });

    it('should return false for null', () => {
      expect(isApiResponse(null)).toBe(false);
    });

    it('should return false for undefined', () => {
      expect(isApiResponse(undefined)).toBe(false);
    });

    it('should return false for non-objects', () => {
      expect(isApiResponse('string')).toBe(false);
      expect(isApiResponse(123)).toBe(false);
      expect(isApiResponse(true)).toBe(false);
    });

    it('should return false for objects without content', () => {
      expect(isApiResponse({})).toBe(false);
      expect(isApiResponse({ data: 'test' })).toBe(false);
    });

    it('should return false for objects with non-array content', () => {
      expect(isApiResponse({ content: 'not an array' })).toBe(false);
      expect(isApiResponse({ content: { type: 'text' } })).toBe(false);
    });

    it('should return true for empty content array', () => {
      expect(isApiResponse({ content: [] })).toBe(true);
    });
  });
});
