jest.mock('uuid', () => ({
  v4: jest.fn(() => 'session-id'),
}));

import { NetaClawChatOrchestratorService } from '../src/modules/netaclaw/service/chat_orchestrator.js';
import { SessionTreeProviderError } from '../src/modules/netaclaw/session-tree/provider.js';

describe('NetaClawChatOrchestratorService session-tree mode', () => {
  it('uses session-tree APIs to create a user entry and assistant placeholder instead of old message APIs', async () => {
    const sessionService = {
      getOrCreateSession: jest.fn().mockResolvedValue('session-tree-1'),
      getRuntimeSnapshot: jest.fn().mockResolvedValue({
        session: {
          sessionId: 'session-tree-1',
          provider: 'file',
          rootEntryId: 'entry_root',
          leafEntryId: 'entry_assistant_prev',
        },
        runtimeContext: {
          messages: [
            { role: 'system', content: 'system prompt' },
            { role: 'assistant', content: 'previous answer' },
          ],
        },
      }),
      appendUserMessageEntry: jest.fn()
        .mockResolvedValueOnce({
          id: 'entry_user_new',
          parentId: 'entry_assistant_prev',
        }),
      appendAssistantPlaceholderEntry: jest.fn()
        .mockResolvedValueOnce({
          id: 'entry_assistant_new',
          parentId: 'entry_user_new',
        }),
      finalizeAssistantEntry: jest.fn().mockResolvedValue(undefined),
    } as any;

    const service = new NetaClawChatOrchestratorService();
    service.sessionService = sessionService;

    const runner = jest.fn().mockResolvedValue({
      finalContent: 'final answer',
      toolCallCount: 0,
    });

    const result = await service.executeSessionTreeChat({
      sessionId: 'session-tree-1',
      requestedAgentName: 'default',
      message: 'hello tree',
      runner,
    });

    expect(sessionService.getOrCreateSession).toHaveBeenCalledWith('session-tree-1', 'default', undefined, undefined, undefined, undefined);
    expect(sessionService.getRuntimeSnapshot).toHaveBeenCalledWith('session-tree-1', undefined);
    expect(sessionService.appendUserMessageEntry).toHaveBeenCalledWith('session-tree-1', {
      parentEntryId: 'entry_assistant_prev',
      message: 'hello tree',
    });
    expect(sessionService.appendAssistantPlaceholderEntry).toHaveBeenCalledWith('session-tree-1', {
      parentEntryId: 'entry_user_new',
    });
    expect(runner).toHaveBeenCalledWith(expect.objectContaining({
      sessionId: 'session-tree-1',
      userEntryId: 'entry_user_new',
      assistantEntryId: 'entry_assistant_new',
      history: [
        { role: 'system', content: 'system prompt' },
        { role: 'assistant', content: 'previous answer' },
      ],
      userMessage: 'hello tree',
    }));
    expect(sessionService.finalizeAssistantEntry).toHaveBeenCalledWith('session-tree-1', 'entry_assistant_new', {
      content: 'final answer',
      thinking: undefined,
      metadata: {
        inFlight: false,
      },
    });
    expect(result).toEqual({
      sessionId: 'session-tree-1',
      finalContent: 'final answer',
      content: 'final answer',
      usage: undefined,
      toolCallCount: 0,
      assistantEntryId: 'entry_assistant_new',
      subagentBatches: [],
    });
  });

  it('marks the assistant entry failed before rethrowing the runner error', async () => {
    const sessionService = {
      getOrCreateSession: jest.fn().mockResolvedValue('session-tree-2'),
      getRuntimeSnapshot: jest.fn().mockResolvedValue({
        session: {
          sessionId: 'session-tree-2',
          provider: 'file',
          rootEntryId: null,
          leafEntryId: null,
        },
        runtimeContext: {
          messages: [],
        },
      }),
      appendUserMessageEntry: jest.fn().mockResolvedValue({
        id: 'entry_user',
        parentId: null,
      }),
      appendAssistantPlaceholderEntry: jest.fn().mockResolvedValue({
        id: 'entry_assistant',
        parentId: 'entry_user',
      }),
      finalizeAssistantEntry: jest.fn().mockResolvedValue(undefined),
    } as any;

    const service = new NetaClawChatOrchestratorService();
    service.sessionService = sessionService;
    const runnerError = new Error('runner failed') as Error & { runtimeMetadata?: Record<string, unknown> };
    runnerError.runtimeMetadata = {
      skillExecutions: [
        {
          name: 'search_web',
          label: 'search_web',
          status: 'error',
          result: 'timeout',
          toolCallId: 'tool-call-1',
        },
      ],
    };

    await expect(service.executeSessionTreeChat({
      sessionId: 'session-tree-2',
      requestedAgentName: 'default',
      message: 'hello tree',
      runner: jest.fn().mockRejectedValue(runnerError),
    })).rejects.toThrow('runner failed');

    expect(sessionService.finalizeAssistantEntry).toHaveBeenCalledWith('session-tree-2', 'entry_assistant', {
      content: 'runner failed',
      thinking: undefined,
      metadata: {
        skillExecutions: [
          {
            name: 'search_web',
            label: 'search_web',
            status: 'error',
            result: 'timeout',
            toolCallId: 'tool-call-1',
          },
        ],
        inFlight: false,
        error: {
          message: 'runner failed',
          name: 'Error',
        },
      },
    });
  });

  it('fails fast when a session-tree snapshot cannot be loaded', async () => {
    const sessionService = {
      getOrCreateSession: jest.fn().mockResolvedValue('session-tree-3'),
      getRuntimeSnapshot: jest.fn().mockRejectedValue(new SessionTreeProviderError('Session session-tree-3 not found')),
    } as any;

    const service = new NetaClawChatOrchestratorService();
    service.sessionService = sessionService;

    await expect(service.executeSessionTreeChat({
      sessionId: 'session-tree-3',
      requestedAgentName: 'default',
      message: 'hello tree',
      runner: jest.fn(),
    })).rejects.toThrow('Session session-tree-3 not found');
  });

  it('patches the subagent batch entry during runtime before appending the final result entry', async () => {
    const sessionService = {
      getOrCreateSession: jest.fn().mockResolvedValue('session-tree-4'),
      getRuntimeSnapshot: jest.fn().mockResolvedValue({
        session: {
          sessionId: 'session-tree-4',
          provider: 'file',
          rootEntryId: 'entry_root',
          leafEntryId: 'entry_assistant_prev',
        },
        runtimeContext: {
          messages: [
            { role: 'assistant', content: 'previous answer' },
          ],
        },
      }),
      appendUserMessageEntry: jest.fn().mockResolvedValue({
        id: 'entry_user_new',
        parentId: 'entry_assistant_prev',
      }),
      appendAssistantPlaceholderEntry: jest.fn().mockResolvedValue({
        id: 'entry_assistant_new',
        parentId: 'entry_user_new',
      }),
      finalizeAssistantEntry: jest.fn().mockResolvedValue(undefined),
      appendSubagentBatchEntry: jest.fn().mockResolvedValue({
        id: 'entry_subagent_batch',
        parentId: 'entry_assistant_new',
        entry: {
          id: 'entry_subagent_batch',
          parentId: 'entry_assistant_new',
          timestamp: '2026-04-19T12:00:00.000Z',
          type: 'subagent_batch',
          content: {
            batchId: 'batch-1',
            mode: 'single',
            tasks: [{ goal: 'inspect docs' }],
            status: 'running',
            parentEntryId: 'entry_assistant_new',
          },
        },
        session: {
          sessionId: 'session-tree-4',
          provider: 'file',
          rootEntryId: 'entry_root',
          leafEntryId: 'entry_subagent_batch',
        },
      }),
      updateSubagentBatchEntry: jest.fn().mockResolvedValue({
        entry: {
          id: 'entry_subagent_batch',
          parentId: 'entry_assistant_new',
          timestamp: '2026-04-19T12:00:01.000Z',
          type: 'subagent_batch',
          content: {
            batchId: 'batch-1',
            mode: 'single',
            tasks: [{ goal: 'inspect docs' }],
            status: 'completed',
            parentEntryId: 'entry_assistant_new',
          },
          metadata: {
            latestEvent: {
              sequence: 1,
            },
          },
        },
        session: {
          sessionId: 'session-tree-4',
          provider: 'file',
          rootEntryId: 'entry_root',
          leafEntryId: 'entry_subagent_batch',
        },
      }),
      appendSubagentResultEntry: jest.fn().mockResolvedValue({
        id: 'entry_subagent_result',
        parentId: 'entry_assistant_new',
        entry: {
          id: 'entry_subagent_result',
          parentId: 'entry_assistant_new',
          timestamp: '2026-04-19T12:00:02.000Z',
          type: 'subagent_result',
          content: {
            batchId: 'batch-1',
            results: [{ summary: 'done', status: 'completed' }],
            status: 'completed',
            parentEntryId: 'entry_assistant_new',
          },
        },
        session: {
          sessionId: 'session-tree-4',
          provider: 'file',
          rootEntryId: 'entry_root',
          leafEntryId: 'entry_subagent_result',
        },
      }),
    } as any;

    const service = new NetaClawChatOrchestratorService();
    service.sessionService = sessionService;

    const runner = jest.fn().mockImplementation(async ({ subagents, assistantEntryId }) => {
      subagents.onBatchStart({
        batchId: 'batch-1',
        sessionId: 'session-tree-4',
        parentMessageId: assistantEntryId as any,
        mode: 'single',
        tasks: [{ goal: 'inspect docs' }],
        toolRuntimeRoutes: [
          {
            name: 'bash',
            runtimeRoute: 'worker-local',
            executionMode: 'parallel',
          },
        ],
      });
      subagents.onUpdate({
        batchId: 'batch-1',
        sessionId: 'session-tree-4',
        parentMessageId: assistantEntryId as any,
        sequence: 1,
        timestamp: '2026-04-19T12:00:01.000Z',
        event: {
          kind: 'result',
          status: 'completed',
          subagent: {
            id: 101,
            summary: 'done',
            status: 'completed',
          },
        },
      });
      subagents.onDone({
        batchId: 'batch-1',
        sessionId: 'session-tree-4',
        parentMessageId: assistantEntryId as any,
        results: [{
          id: 101,
          summary: 'done',
          status: 'completed',
          resultPayload: {
            processEvents: [
              {
                type: 'tool_call',
                runId: 'subagent-101',
                timestamp: '2026-04-19T12:00:01.500Z',
                name: 'bash',
              },
            ],
          },
        }],
      });

      await Promise.resolve();
      return {
        finalContent: 'subagent finished',
        toolCallCount: 0,
      };
    });

    await service.executeSessionTreeChat({
      sessionId: 'session-tree-4',
      requestedAgentName: 'default',
      message: 'run subagent',
      runner,
    });
    await Promise.resolve();
    await Promise.resolve();

    expect(sessionService.appendSubagentBatchEntry).toHaveBeenCalledWith('session-tree-4', {
      parentEntryId: 'entry_assistant_new',
      batchId: 'batch-1',
      mode: 'single',
      tasks: [{ goal: 'inspect docs' }],
      status: 'running',
    });
    expect(sessionService.updateSubagentBatchEntry).toHaveBeenCalled();
    expect(sessionService.updateSubagentBatchEntry.mock.calls).toEqual(
      expect.arrayContaining([
        [
          'session-tree-4',
          'entry_subagent_batch',
          expect.objectContaining({
            batchId: 'batch-1',
            metadata: expect.objectContaining({
              toolRuntimeRoutes: [
                expect.objectContaining({
                  name: 'bash',
                  runtimeRoute: 'worker-local',
                  executionMode: 'parallel',
                }),
              ],
              events: expect.arrayContaining([
                expect.objectContaining({
                  sequence: 1,
                  event: expect.objectContaining({
                    kind: 'result',
                    status: 'completed',
                    subagent: expect.objectContaining({
                      id: 101,
                      summary: 'done',
                    }),
                  }),
                }),
              ]),
            }),
          }),
        ],
      ]),
    );
    expect(sessionService.appendSubagentResultEntry).toHaveBeenCalledWith('session-tree-4', {
      parentEntryId: 'entry_assistant_new',
      batchId: 'batch-1',
      results: [{
        id: 101,
        summary: 'done',
        status: 'completed',
        resultPayload: {
          processEvents: [
            {
              type: 'tool_call',
              runId: 'subagent-101',
              timestamp: '2026-04-19T12:00:01.500Z',
              name: 'bash',
            },
          ],
        },
      }],
      status: 'completed',
      metadata: {
        finalResults: [
          {
            id: 101,
            summary: 'done',
            status: 'completed',
            resultPayload: {
              processEvents: [
                {
                  type: 'tool_call',
                  runId: 'subagent-101',
                  timestamp: '2026-04-19T12:00:01.500Z',
                  name: 'bash',
                },
              ],
            },
          },
        ],
        processEvents: [
          {
            type: 'tool_call',
            runId: 'subagent-101',
            timestamp: '2026-04-19T12:00:01.500Z',
            name: 'bash',
          },
        ],
      },
    });
  });
});
