import { Type } from '@sinclair/typebox';
import { runAttempt } from '../src/modules/netaclaw/runtime/attempt.js';

jest.mock('../src/modules/netaclaw/runtime/model_selection.js', () => ({
  getProvider: jest.fn(),
}));

describe('tool process events', () => {
  const { getProvider } = require('../src/modules/netaclaw/runtime/model_selection.js');

  beforeEach(() => {
    jest.resetAllMocks();
  });

  it('normalizes tool process updates and emits them from runAttempt', async () => {
    const chat = jest.fn()
      .mockResolvedValueOnce({
        content: 'tool turn',
        toolCalls: [
          { id: 'call-1', name: 'slow_tool', arguments: '{"taskId":"task-1"}' },
        ],
        usage: { inputTokens: 1, outputTokens: 1 },
      })
      .mockResolvedValueOnce({
        content: 'done',
        toolCalls: [],
        usage: { inputTokens: 1, outputTokens: 1 },
      });
    getProvider.mockReturnValue({ chat });

    const updates: any[] = [];

    await runAttempt({
      messages: [{ role: 'user', content: 'run slow tool' }],
      tools: [{
        name: 'slow_tool',
        label: 'Slow Tool',
        description: 'Slow tool',
        parameters: Type.Object({ taskId: Type.String() }),
        execute: async (_id: string, _args: { taskId: string }, onUpdate?: (event: any) => void) => {
          onUpdate?.({
            operationId: 'tool-provided-operation-id',
            source: 'tool',
            targetType: 'tool',
            stage: 'prepare',
            status: 'running',
            message: 'Preparing work',
            detail: 'x'.repeat(600),
            current: 1,
            total: 2,
            percent: 500,
            payload: {
              apiKey: 'secret',
              note: 'payload',
            },
          });
          return 'ok';
        },
      }],
      config: { provider: 'mock', model: 'mock', apiKey: 'k' },
      onToolProcessEvent: update => updates.push(update),
    });

    expect(updates[0]).toEqual(expect.objectContaining({
      toolCallId: 'call-1',
      operationId: 'tool-call-1',
      toolName: 'slow_tool',
      args: { taskId: 'task-1' },
    }));
    expect(updates[0].event).toEqual(expect.objectContaining({
      version: 1,
      operationId: 'tool-call-1',
      targetType: 'tool',
      source: 'tool',
      percent: 100,
      detail: `${'x'.repeat(500)}...`,
      payload: {
        apiKey: '[filtered]',
        note: 'payload',
      },
    }));
  });

  it('does not fail tool execution when process event observer throws', async () => {
    const chat = jest.fn()
      .mockResolvedValueOnce({
        content: 'tool turn',
        toolCalls: [
          { id: 'call-1', name: 'slow_tool', arguments: '{"token":"secret","taskId":"task-1"}' },
        ],
        usage: { inputTokens: 1, outputTokens: 1 },
      })
      .mockResolvedValueOnce({
        content: 'done',
        toolCalls: [],
        usage: { inputTokens: 1, outputTokens: 1 },
      });
    getProvider.mockReturnValue({ chat });

    const result = await runAttempt({
      messages: [{ role: 'user', content: 'run slow tool' }],
      tools: [{
        name: 'slow_tool',
        label: 'Slow Tool',
        description: 'Slow tool',
        parameters: Type.Object({ taskId: Type.String() }),
        execute: async (_id: string, _args: Record<string, unknown>, onUpdate?: (event: any) => void) => {
          onUpdate?.({ message: 'still observational' });
          return 'ok';
        },
      }],
      config: { provider: 'mock', model: 'mock', apiKey: 'k' },
      onToolProcessEvent: () => {
        throw new Error('socket closed');
      },
    });

    expect(result.finalContent).toBe('done');
    expect(result.messages).toContainEqual(expect.objectContaining({
      role: 'tool',
      content: 'ok',
      toolCallId: 'call-1',
    }));
  });
});
