// @vitest-environment node
import OpenAI from 'openai';
import type { Mock } from 'vitest';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import * as debugStreamModule from '../../utils/debugStream';
import * as getModelPricingModule from '../../utils/getModelPricing';
import officalOpenAIModels from './fixtures/openai-models.json';
import { LobeOpenAI, params } from './index';

// Mock the console.error to avoid polluting test output
vi.spyOn(console, 'error').mockImplementation(() => {});
vi.spyOn(getModelPricingModule, 'getModelPricing').mockResolvedValue(undefined);
vi.mock('@lobechat/business-model-bank/model-config', () => ({
  loadModels: vi.fn().mockResolvedValue([]),
}));

// Mock fetch for most tests, but will be restored for real network tests
const mockFetch = vi.fn();
global.fetch = mockFetch;

describe('LobeOpenAI', () => {
  let instance: InstanceType<typeof LobeOpenAI>;

  beforeEach(() => {
    instance = new LobeOpenAI({ apiKey: 'test' });

    // 使用 vi.spyOn 来模拟 chat.completions.create 方法
    vi.spyOn(instance['client'].chat.completions, 'create').mockResolvedValue(
      new ReadableStream() as any,
    );
    vi.spyOn(instance['client'].models, 'list').mockResolvedValue({ data: [] } as any);

    // Mock responses.create for responses API tests
    vi.spyOn(instance['client'].responses, 'create').mockResolvedValue(new ReadableStream() as any);
  });

  afterEach(() => {
    vi.clearAllMocks();
    mockFetch.mockClear();
  });

  describe('chat', () => {
    it('should return a StreamingTextResponse on successful API call', async () => {
      // Arrange
      const mockStream = new ReadableStream();
      const mockResponse = Promise.resolve(mockStream);

      (instance['client'].chat.completions.create as Mock).mockResolvedValue(mockResponse);

      // Act
      const result = await instance.chat({
        messages: [{ content: 'Hello', role: 'user' }],
        model: 'text-davinci-003',
        temperature: 0,
      });

      // Assert
      expect(result).toBeInstanceOf(Response);
    });

    describe('Error', () => {
      it('should return ProviderBizError with an openai error response when OpenAI.APIError is thrown', async () => {
        // Arrange
        const apiError = new OpenAI.APIError(
          400,
          {
            error: {
              message: 'Bad Request',
            },
            status: 400,
          },
          'Error message',
          {},
        );

        vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);

        // Act
        try {
          await instance.chat({
            messages: [{ content: 'Hello', role: 'user' }],
            model: 'text-davinci-003',
            temperature: 0,
          });
        } catch (e) {
          expect(e).toEqual({
            endpoint: 'https://api.openai.com/v1',
            error: {
              error: { message: 'Bad Request' },
              status: 400,
            },
            errorType: 'ProviderBizError',
            message: expect.any(String),
            provider: 'openai',
          });
        }
      });

      it('should throw AgentRuntimeError with NoOpenAIAPIKey if no apiKey is provided', async () => {
        try {
          new LobeOpenAI({});
        } catch (e) {
          expect(e).toEqual({ errorType: 'InvalidProviderAPIKey' });
        }
      });

      it('should return ProviderBizError with the cause when OpenAI.APIError is thrown with cause', async () => {
        // Arrange
        const errorInfo = {
          cause: {
            message: 'api is undefined',
          },
        };
        const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});

        vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);

        // Act
        try {
          await instance.chat({
            messages: [{ content: 'Hello', role: 'user' }],
            model: 'text-davinci-003',
            temperature: 0,
          });
        } catch (e) {
          expect(e).toEqual({
            endpoint: 'https://api.openai.com/v1',
            error: {
              cause: { message: 'api is undefined' },
            },
            errorType: 'ProviderBizError',
            message: expect.any(String),
            provider: 'openai',
          });
        }
      });

      it('should return ProviderBizError with an cause response with desensitize Url', async () => {
        // Arrange
        const errorInfo = {
          cause: { message: 'api is undefined' },
        };
        const apiError = new OpenAI.APIError(400, errorInfo, 'module error', {});

        instance = new LobeOpenAI({
          apiKey: 'test',

          baseURL: 'https://api.abc.com/v1',
        });

        vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(apiError);

        // Act
        try {
          await instance.chat({
            messages: [{ content: 'Hello', role: 'user' }],
            model: 'gpt-3.5-turbo',
            temperature: 0,
          });
        } catch (e) {
          expect(e).toEqual({
            endpoint: 'https://api.***.com/v1',
            error: {
              cause: { message: 'api is undefined' },
            },
            errorType: 'ProviderBizError',
            message: expect.any(String),
            provider: 'openai',
          });
        }
      });

      it('should return AgentRuntimeError for non-OpenAI errors', async () => {
        // Arrange
        const genericError = new Error('Generic Error');

        vi.spyOn(instance['client'].chat.completions, 'create').mockRejectedValue(genericError);

        // Act
        try {
          await instance.chat({
            messages: [{ content: 'Hello', role: 'user' }],
            model: 'text-davinci-003',
            temperature: 0,
          });
        } catch (e) {
          expect(e).toEqual({
            endpoint: 'https://api.openai.com/v1',
            error: {
              cause: genericError.cause,
              message: genericError.message,
              name: genericError.name,
            },
            errorType: 'AgentRuntimeError',
            message: expect.any(String),
            provider: 'openai',
          });
        }
      });
    });

    describe('DEBUG', () => {
      it('should call debugStream and return StreamingTextResponse when DEBUG_OPENAI_CHAT_COMPLETION is 1', async () => {
        // Arrange
        const mockProdStream = new ReadableStream() as any; // 模拟的 prod 流
        const mockDebugStream = new ReadableStream({
          start(controller) {
            controller.enqueue('Debug stream content');
            controller.close();
          },
        }) as any;
        mockDebugStream.toReadableStream = () => mockDebugStream; // 添加 toReadableStream 方法

        // 模拟 chat.completions.create 返回值，包括模拟的 tee 方法
        (instance['client'].chat.completions.create as Mock).mockResolvedValue({
          tee: () => [mockProdStream, { toReadableStream: () => mockDebugStream }],
        });

        // 保存原始环境变量值
        const originalDebugValue = process.env.DEBUG_OPENAI_CHAT_COMPLETION;

        // 模拟环境变量
        process.env.DEBUG_OPENAI_CHAT_COMPLETION = '1';
        vi.spyOn(debugStreamModule, 'debugStream').mockImplementation(() => Promise.resolve());

        // 执行测试
        // 运行你的测试函数，确保它会在条件满足时调用 debugStream
        // 假设的测试函数调用，你可能需要根据实际情况调整
        await instance.chat({
          messages: [{ content: 'Hello', role: 'user' }],
          model: 'text-davinci-003',
          temperature: 0,
        });

        // 验证 debugStream 被调用
        expect(debugStreamModule.debugStream).toHaveBeenCalled();

        // 恢复原始环境变量值
        process.env.DEBUG_OPENAI_CHAT_COMPLETION = originalDebugValue;
      });
    });
  });

  describe('models', () => {
    it('should get models', async () => {
      // mock the models.list method
      (instance['client'].models.list as Mock).mockResolvedValue({ data: officalOpenAIModels });

      const list = await instance.models();

      expect(Array.isArray(list)).toBe(true);
      expect(list.length).toBeGreaterThan(0);

      const gpt35Turbo = list.find((model) => model.id === 'gpt-3.5-turbo-0613');
      expect(gpt35Turbo).toBeDefined();
      expect(gpt35Turbo?.id).toBe('gpt-3.5-turbo-0613');

      const textEmbeddingAda = list.find((model) => model.id === 'text-embedding-ada-002');
      expect(textEmbeddingAda).toBeDefined();
      expect(textEmbeddingAda?.type).toBe('embedding');
    });
  });

  describe('chatCompletion.handlePayload', () => {
    it('should use responses API for responsesAPIModels without enabledSearch', async () => {
      const payload = {
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'o1-pro', // 这个模型在 responsesAPIModels 中
        temperature: 0.7,
      };

      await instance.chat(payload);

      // 应该调用 responses.create 而不是 chat.completions.create
      expect(instance['client'].responses.create).toHaveBeenCalled();
      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.model).toBe('o1-pro');
    });

    it('should use responses API for future GPT-5 minor models', async () => {
      const payload = {
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-5.6',
        temperature: 0.7,
      };

      await instance.chat(payload);

      expect(instance['client'].responses.create).toHaveBeenCalled();
      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.model).toBe('gpt-5.6');
    });

    it('should use responses API when enabledSearch is true', async () => {
      const payload = {
        enabledSearch: true,
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-4o',
        temperature: 0.7,
      };

      await instance.chat(payload);

      // 应该调用 responses.create
      expect(instance['client'].responses.create).toHaveBeenCalled();
    });

    it('should handle -search- models with stripped parameters', async () => {
      const payload = {
        frequency_penalty: 0.5,
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-4o-search-2024',
        presence_penalty: 0.3,
        temperature: 0.7,
        top_p: 0.9,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].chat.completions.create as Mock).mock.calls[0][0];
      expect(createCall.model).toBe('gpt-4o-search-2024');
      expect(createCall.temperature).toBeUndefined();
      expect(createCall.top_p).toBeUndefined();
      expect(createCall.frequency_penalty).toBeUndefined();
      expect(createCall.presence_penalty).toBeUndefined();
      expect(createCall.stream).toBe(true);
    });

    it('should handle regular models with all parameters', async () => {
      const payload = {
        frequency_penalty: 0.5,
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-4o',
        presence_penalty: 0.3,
        temperature: 0.7,
        top_p: 0.9,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].chat.completions.create as Mock).mock.calls[0][0];
      expect(createCall.model).toBe('gpt-4o');
      expect(createCall.temperature).toBe(0.7);
      expect(createCall.top_p).toBe(0.9);
      expect(createCall.frequency_penalty).toBe(0.5);
      expect(createCall.presence_penalty).toBe(0.3);
      expect(createCall.stream).toBe(true);
    });
  });

  describe('responses.handlePayload', () => {
    it('should add web_search tool when enabledSearch is true', async () => {
      const payload = {
        enabledSearch: true,
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-4o',
        // 使用常规模型，通过 enabledSearch 触发 responses API
        temperature: 0.7,
        tools: [{ function: { description: 'test', name: 'test' }, type: 'function' as const }],
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.tools).toEqual([
        { description: 'test', name: 'test', type: 'function' },
        { type: 'web_search' },
      ]);
    });

    it('should add search_context_size to web_search tool when OPENAI_SEARCH_CONTEXT_SIZE is set', async () => {
      // Note: oaiSearchContextSize is read at module load time, not runtime
      // This test verifies the tool structure is correct when the env var would be set
      const payload = {
        enabledSearch: true,
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-4o',
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      // Verify web_search tool is added, search_context_size depends on env var at module load time
      expect(createCall.tools).toEqual(
        expect.arrayContaining([expect.objectContaining({ type: 'web_search' })]),
      );
    });

    it('should handle computer-use models with truncation and reasoning', async () => {
      const payload = {
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'computer-use-preview',
        reasoning: { effort: 'medium' },
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.truncation).toBe('auto');
      expect(createCall.reasoning).toEqual({ effort: 'medium', summary: 'auto' });
    });

    it('should handle reasoning payload models without computer-use truncation', async () => {
      const payload = {
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'o3-pro',
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.reasoning).toEqual({ summary: 'auto' });
      expect(createCall.truncation).toBeUndefined();
    });

    it('should set reasoning.effort to high for gpt-5-pro models', async () => {
      const payload = {
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-5-pro',
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.reasoning).toEqual({ effort: 'high', summary: 'auto' });
    });

    it('should set reasoning.effort to high for gpt-5-pro-2025-10-06 models', async () => {
      const payload = {
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-5-pro-2025-10-06',
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.reasoning).toEqual({ effort: 'high', summary: 'auto' });
    });

    it('should set reasoning.effort to high for future gpt-5.x-pro models', async () => {
      const payload = {
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-5.6-pro',
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.reasoning).toEqual({ effort: 'high', summary: 'auto' });
    });

    it('should convert max_tokens to max_output_tokens for responses API', async () => {
      const payload = {
        max_tokens: 2048,
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'o1-pro',
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.max_output_tokens).toBe(2048);
      expect(createCall.max_tokens).toBeUndefined();
    });

    it('should not include max_output_tokens when max_tokens is undefined', async () => {
      const payload = {
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'o1-pro',
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.max_output_tokens).toBeUndefined();
    });

    it('should convert max_tokens to max_output_tokens for search-enabled models', async () => {
      const payload = {
        enabledSearch: true,
        max_tokens: 4096,
        messages: [{ content: 'Hello', role: 'user' as const }],
        model: 'gpt-4o',
        temperature: 0.7,
      };

      await instance.chat(payload);

      const createCall = (instance['client'].responses.create as Mock).mock.calls[0][0];
      expect(createCall.max_output_tokens).toBe(4096);
      expect(createCall.max_tokens).toBeUndefined();
    });
  });

  describe('debug configuration', () => {
    it('should return false when DEBUG_OPENAI_CHAT_COMPLETION is not set', () => {
      delete process.env.DEBUG_OPENAI_CHAT_COMPLETION;
      const result = params.debug.chatCompletion();
      expect(result).toBe(false);
    });

    it('should return true when DEBUG_OPENAI_CHAT_COMPLETION is set to 1', () => {
      const originalEnv = process.env.DEBUG_OPENAI_CHAT_COMPLETION;
      process.env.DEBUG_OPENAI_CHAT_COMPLETION = '1';
      const result = params.debug.chatCompletion();
      expect(result).toBe(true);
      process.env.DEBUG_OPENAI_CHAT_COMPLETION = originalEnv;
    });

    it('should return false when DEBUG_OPENAI_RESPONSES is not set', () => {
      delete process.env.DEBUG_OPENAI_RESPONSES;
      const result = params.debug.responses();
      expect(result).toBe(false);
    });

    it('should return true when DEBUG_OPENAI_RESPONSES is set to 1', () => {
      const originalEnv = process.env.DEBUG_OPENAI_RESPONSES;
      process.env.DEBUG_OPENAI_RESPONSES = '1';
      const result = params.debug.responses();
      expect(result).toBe(true);
      process.env.DEBUG_OPENAI_RESPONSES = originalEnv;
    });
  });
});
