/**
 * VlmClient 单元测试
 * 覆盖 verifyState / nextAction 核心路径，mock openai 与 modelChannelService
 */

// --- mock openai ---
const createCompletion = jest.fn();
jest.mock('openai', () => {
  return {
    __esModule: true,
    default: class {
      constructor(public opts: any) {}
      chat = { completions: { create: createCompletion } };
    },
  };
});

import { VlmClient } from '../../../../src/modules/desktop_op/runtime/vlm_client.js';
import type { AdapterContext } from '../../../../src/modules/desktop_op/runtime/types.js';

/** 构造可复用的 mock modelChannelService */
function makeMockService(channelData?: any) {
  return {
    channelRepo: {
      findOneBy: jest.fn().mockResolvedValue(
        channelData ?? {
          baseUrl: 'https://x',
          apiKey: 'k',
          models: [{ name: 'doubao', capability: 'vision' }],
        }
      ),
    },
  };
}

/** 构造最小 AdapterContext */
function makeCtx(extra?: Partial<AdapterContext>): AdapterContext {
  return {
    window: {} as any,
    screenshot: {} as any,
    input: {} as any,
    vlm: {} as any,
    parser: null as any,
    logger: {} as any,
    task: { id: 't1', appId: 'weixin', target: {}, actionType: 'send-text', params: {}, modelChannelId: 42, enqueuedAt: 0 },
    modelCalls: 0,
    ...extra,
  } as AdapterContext;
}

/** 构造截图 Buffer */
const fakeBuf = Buffer.from('fake-png');

describe('VlmClient', () => {
  let client: VlmClient;
  let svc: ReturnType<typeof makeMockService>;

  beforeEach(() => {
    jest.clearAllMocks();
    client = new VlmClient();
    svc = makeMockService();
    (client as any).modelChannelService = svc;
  });

  // 测试 1: verifyState 成功路径
  it('verifyState 正常返回 true，ctx.modelCalls+1，OpenAI client 构造参数和请求模型正确', async () => {
    createCompletion.mockResolvedValueOnce({
      choices: [{ message: { content: '{"verified":true}' } }],
      usage: { total_tokens: 50 },
    });

    const ctx = makeCtx();
    const result = await client.verifyState(ctx, '是否打开微信?', fakeBuf);

    // 返回 true
    expect(result).toBe(true);
    // ctx.modelCalls 增加 1
    expect(ctx.modelCalls).toBe(1);
    // OpenAI 构造器收到正确 baseURL 和 apiKey
    const OpenAI = jest.requireMock('openai').default;
    const instance = createCompletion.mock.instances?.[0] ?? (new OpenAI({}));
    // 验证 create 被调用
    expect(createCompletion).toHaveBeenCalledTimes(1);
    const callArg = createCompletion.mock.calls[0][0];
    // 模型名正确
    expect(callArg.model).toBe('doubao');
    // user message 包含 base64 图片
    const userMsg = callArg.messages.find((m: any) => m.role === 'user');
    const imgPart = userMsg.content.find((p: any) => p.type === 'image_url');
    expect(imgPart.image_url.url).toContain('data:image/png;base64,');
    expect(imgPart.image_url.url).toContain(fakeBuf.toString('base64'));
  });

  // 测试 2: verifyState 返回 false
  it('verifyState 当内容为 {"verified":false} 时返回 false', async () => {
    createCompletion.mockResolvedValueOnce({
      choices: [{ message: { content: '{"verified":false}' } }],
    });

    const ctx = makeCtx();
    const result = await client.verifyState(ctx, '？', fakeBuf);
    expect(result).toBe(false);
  });

  // 测试 3: verifyState 优先使用 ctx.parser.parseVerify
  it('verifyState 当 ctx.parser.parseVerify 存在时调用它', async () => {
    createCompletion.mockResolvedValueOnce({
      choices: [{ message: { content: 'some raw text' } }],
    });
    const mockParseVerify = jest.fn().mockReturnValue(true);
    const ctx = makeCtx({ parser: { parseVerify: mockParseVerify } as any });

    const result = await client.verifyState(ctx, '？', fakeBuf);
    expect(mockParseVerify).toHaveBeenCalledWith('some raw text');
    expect(result).toBe(true);
  });

  // 测试 4: verifyState 无 parser 时使用本地兜底
  it('verifyState 无 ctx.parser 时用默认兜底解析 yes/no', async () => {
    createCompletion.mockResolvedValueOnce({
      choices: [{ message: { content: 'yes, it is visible' } }],
    });
    const ctx = makeCtx({ parser: null as any });

    const result = await client.verifyState(ctx, '？', fakeBuf);
    expect(result).toBe(true);
  });

  // 测试 5: verifyState 抛出 model-channel-not-configured
  it('verifyState 当 ctx.task.modelChannelId 缺失时抛出 model-channel-not-configured', async () => {
    const ctx = makeCtx();
    (ctx.task as any).modelChannelId = undefined;

    await expect(client.verifyState(ctx, '？', fakeBuf)).rejects.toThrow('model-channel-not-configured');
  });

  // 测试 6: verifyState 抛出 model-channel-not-found
  it('verifyState 当 repo 返回 null 时抛出 model-channel-not-found', async () => {
    svc.channelRepo.findOneBy.mockResolvedValueOnce(null);
    const ctx = makeCtx();

    await expect(client.verifyState(ctx, '？', fakeBuf)).rejects.toThrow('model-channel-not-found');
  });

  // 测试 7: verifyState 抛出 model-channel-no-models
  it('verifyState 当 channel.models 为空时抛出 model-channel-no-models', async () => {
    svc.channelRepo.findOneBy.mockResolvedValueOnce({ baseUrl: 'https://x', apiKey: 'k', models: [] });
    const ctx = makeCtx();

    await expect(client.verifyState(ctx, '？', fakeBuf)).rejects.toThrow('model-channel-no-models');
  });

  // 测试 8: resolveModelName 优先选 capability:'vision'
  it('resolveModelName 优先选 capability=vision 而非 text', async () => {
    svc.channelRepo.findOneBy.mockResolvedValueOnce({
      baseUrl: 'https://x',
      apiKey: 'k',
      models: [
        { name: 'a', capability: 'text' },
        { name: 'b', capability: 'vision' },
      ],
    });
    createCompletion.mockResolvedValueOnce({
      choices: [{ message: { content: '{"verified":true}' } }],
    });

    await client.verifyState(makeCtx(), '？', fakeBuf);

    const callArg = createCompletion.mock.calls[0][0];
    expect(callArg.model).toBe('b');
  });

  // 测试 9: nextAction 截断 history 到最近 1 条 + 当前
  it('nextAction 传入 3 条 history 时 messages 只含 system + 1条history + 1条current', async () => {
    createCompletion.mockResolvedValueOnce({
      choices: [{ message: { content: '{"type":"finished"}' } }],
      usage: { total_tokens: 100 },
    });
    const mockParser = {
      buildSystemPrompt: jest.fn().mockReturnValue('system-prompt'),
      parseAction: jest.fn().mockReturnValue({ type: 'finished' }),
      buildVerifyPrompt: jest.fn(),
      parseVerify: jest.fn(),
    };
    const ctx = makeCtx({ parser: mockParser as any });
    const history = [
      { screenshot: Buffer.from('h1') },
      { screenshot: Buffer.from('h2') },
      { screenshot: Buffer.from('h3') },
    ];

    await client.nextAction(ctx, mockParser as any, fakeBuf, history);

    const callArg = createCompletion.mock.calls[0][0];
    // system(1) + history截断为1条(1) + current(1) = 3 条
    expect(callArg.messages).toHaveLength(3);
    expect(callArg.messages[0].role).toBe('system');
    expect(callArg.messages[1].role).toBe('user');
    expect(callArg.messages[2].role).toBe('user');
  });

  // 测试 10: nextAction 调用 parser.parseAction 并返回 action
  it('nextAction 调用 parser.parseAction 并返回解析后的 action', async () => {
    const expectedAction = { type: 'click', x: 10, y: 20 };
    createCompletion.mockResolvedValueOnce({
      choices: [{ message: { content: '{"type":"click","x":10,"y":20}' } }],
      usage: { total_tokens: 80 },
    });
    const mockParser = {
      buildSystemPrompt: jest.fn().mockReturnValue('sys'),
      parseAction: jest.fn().mockReturnValue(expectedAction),
      buildVerifyPrompt: jest.fn(),
      parseVerify: jest.fn(),
    };
    const ctx = makeCtx({ parser: mockParser as any });

    const { action, tokensUsed } = await client.nextAction(ctx, mockParser as any, fakeBuf, []);

    expect(mockParser.parseAction).toHaveBeenCalledWith('{"type":"click","x":10,"y":20}');
    expect(action).toEqual(expectedAction);
    expect(tokensUsed).toBe(80);
  });

  // 测试 11: ctx.modelCalls 每次调用都递增
  it('每次调用 verifyState / nextAction 均递增 ctx.modelCalls', async () => {
    createCompletion.mockResolvedValue({
      choices: [{ message: { content: '{"verified":true}' } }],
      usage: { total_tokens: 10 },
    });
    const mockParser = {
      buildSystemPrompt: jest.fn().mockReturnValue('sys'),
      parseAction: jest.fn().mockReturnValue({ type: 'finished' }),
      buildVerifyPrompt: jest.fn(),
      parseVerify: jest.fn(),
    };
    const ctx = makeCtx({ parser: mockParser as any });

    await client.verifyState(ctx, '？', fakeBuf);
    expect(ctx.modelCalls).toBe(1);

    createCompletion.mockResolvedValueOnce({
      choices: [{ message: { content: '{"type":"finished"}' } }],
      usage: { total_tokens: 10 },
    });
    await client.nextAction(ctx, mockParser as any, fakeBuf, []);
    expect(ctx.modelCalls).toBe(2);
  });
});
