import { AiModelStreamService } from './ai-model-stream.service';
import type { AiRuntimeConfig } from '../ai-config/dto/ai-config.dto';

const config: AiRuntimeConfig = {
  provider: 'DEEPSEEK' as AiRuntimeConfig['provider'],
  baseUrl: 'https://example.test/v1',
  apiKey: 'secret',
  defaultModel: 'deepseek-reasoner',
  timeoutMs: 1000,
  enabled: true,
  supportsVision: false,
  reasoningEffort: null,
};

describe('AiModelStreamService', () => {
  afterEach(() => jest.restoreAllMocks());

  it('分离思考、正文并拼接分片工具调用，且处理无尾随空行的最后事件', async () => {
    const chunks = [
      'data: {"choices":[{"delta":{"reasoning_content":"思考"}}]}\n\n',
      'data: {"choices":[{"delta":{"content":"答案","tool_calls":[{"index":0,"id":"call_","function":{"name":"search_","arguments":"{\\"q\\":"}}]}}]}\n\n',
      'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"1","function":{"name":"students","arguments":"\\"张三\\"}"}}]}}]}',
    ];
    async function* body() {
      for (const chunk of chunks) yield Buffer.from(chunk);
    }
    const service = new AiModelStreamService();
    jest.spyOn(service as never, 'pinnedPost' as never).mockResolvedValue({
      status: 200,
      contentType: 'text/event-stream',
      body: body(),
    } as never);

    const events = [];
    for await (const event of service.stream(
      config,
      [{ role: 'user', content: '查询' }],
      [],
      new AbortController().signal,
    )) events.push(event);

    expect(events).toEqual([
      { type: 'reasoning', delta: '思考' },
      { type: 'content', delta: '答案' },
      {
        type: 'complete',
        toolCalls: [{ id: 'call_1', name: 'search_students', arguments: '{"q":"张三"}' }],
      },
    ]);
  });

  it('不向调用方暴露上游非 JSON 错误正文', async () => {
    async function* body() { yield Buffer.from('proxy internal detail'); }
    const service = new AiModelStreamService();
    jest.spyOn(service as never, 'pinnedPost' as never).mockResolvedValue({
      status: 502,
      contentType: 'text/plain',
      body: body(),
    } as never);
    jest.spyOn(service as never, 'sleep' as never).mockResolvedValue(undefined as never);
    const consume = async () => {
      for await (const _ of service.stream(
        config,
        [{ role: 'user', content: '查询' }],
        [],
        new AbortController().signal,
      )) void _;
    };
    await expect(consume()).rejects.toThrow('AI 服务暂时不可用');
  });

  it('上游 503 时自动重试并发出重试事件', async () => {
    async function* successBody() {
      yield Buffer.from('data: [DONE]\n\n');
    }
    const service = new AiModelStreamService();
    let calls = 0;
    jest.spyOn(service as never, 'pinnedPost' as never).mockImplementation(async () => {
      calls += 1;
      if (calls === 1) {
        return {
          status: 503,
          contentType: 'text/plain',
          body: { resume: jest.fn() },
        } as never;
      }
      return {
        status: 200,
        contentType: 'text/event-stream',
        body: successBody(),
      } as never;
    });
    jest.spyOn(service as never, 'sleep' as never).mockResolvedValue(undefined as never);
    const events: Array<{ type: string; attempt?: number; maxRetries?: number; reason?: string }> = [];
    for await (const event of service.stream(
      config,
      [{ role: 'user', content: '查询' }],
      [],
      new AbortController().signal,
    )) {
      events.push(event);
    }
    expect(calls).toBe(2);
    expect(events).toContainEqual(
      expect.objectContaining({ type: 'retrying', attempt: 1, maxRetries: 3, reason: '上游返回 503' }),
    );
  });

  it('上游 503 时提示服务繁忙', async () => {
    async function* body() {
      yield Buffer.from('{"error":{"message":"Service is too busy"}}');
    }
    const service = new AiModelStreamService();
    jest.spyOn(service as never, 'pinnedPost' as never).mockResolvedValue({
      status: 503,
      contentType: 'text/plain',
      body: body(),
    } as never);
    jest.spyOn(service as never, 'sleep' as never).mockResolvedValue(undefined as never);
    const consume = async () => {
      for await (const _ of service.stream(
        config,
        [{ role: 'user', content: '查询' }],
        [],
        new AbortController().signal,
      )) void _;
    };
    await expect(consume()).rejects.toThrow('AI 服务繁忙，请稍后重试');
  });

  it('配置 reasoningEffort 时仅对非 DeepSeek 服务商发送该参数', async () => {
    const bodies: string[] = [];
    async function* body() {
      yield Buffer.from('data: [DONE]\n\n');
    }
    const service = new AiModelStreamService();
    jest.spyOn(service as never, 'pinnedPost' as never).mockImplementation(
      async (_url: string, _headers: Record<string, string>, payload: string) => {
        bodies.push(payload);
        return { status: 200, contentType: 'text/event-stream', body: body() } as never;
      },
    );

    for await (const _ of service.stream(
      { ...config, reasoningEffort: 'high' },
      [{ role: 'user', content: 'x' }],
      [],
      new AbortController().signal,
    )) void _;
    expect(JSON.parse(bodies[0])).not.toHaveProperty('reasoning_effort');

    for await (const _ of service.stream(
      {
        ...config,
        provider: 'OPENAI' as AiRuntimeConfig['provider'],
        reasoningEffort: 'high',
      },
      [{ role: 'user', content: 'x' }],
      [],
      new AbortController().signal,
    )) void _;
    expect(JSON.parse(bodies[1]).reasoning_effort).toBe('high');
  });
});
