/**
 * delegate ライブコンソール Task 1 — サブの出力を delegate 専用チャンネルへ
 * 再タグ付けして流すことの検証（メイン onText 非汚染・入れ子の runId 浮上）。
 */
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { LLMEvent, ToolDef } from '../llm/openai-compat.js';
import type { ToolContext } from './tools/index.js';

const { executeToolMock, getToolDefsMock } = vi.hoisted(() => ({
  executeToolMock: vi.fn(),
  getToolDefsMock: vi.fn(),
}));
vi.mock('./tools/index.js', () => ({
  executeTool: executeToolMock,
  getToolDefs: getToolDefsMock,
}));

import { executeMovement, type Movement, type AgentLoopCallbacks } from './agent-loop.js';
import { Conversation } from './context/conversation.js';
import { ToolResultCache } from './context/tool-result-cache.js';

function makeToolDefs(names: string[]): ToolDef[] {
  return names.map((name) => ({
    type: 'function',
    function: { name, description: name, parameters: { type: 'object', properties: {}, required: [] } },
  }));
}
function makeContext(overrides: Partial<ToolContext> = {}): ToolContext {
  return { workspacePath: '/tmp/delegate-stream-test', editAllowed: true, ...overrides };
}
class FakeClient {
  private index = 0;
  readonly calls: Array<{ messages: unknown; tools?: unknown }> = [];
  constructor(private readonly responses: LLMEvent[][]) {}
  async *chat(messages: unknown, tools?: unknown): AsyncGenerator<LLMEvent> {
    this.calls.push({ messages, tools });
    for (const event of this.responses[this.index++] ?? []) yield event;
  }
  get callCount(): number { return this.calls.length; }
}

const PARENT_MOVEMENT: Movement = {
  name: 'parent', edit: false, persona: 'orchestrator', instruction: 'orchestrate',
  allowedTools: ['delegate'], rules: [{ condition: 'done', next: 'COMPLETE' }], defaultNext: 'COMPLETE',
};

function delegateExecuteToolImpl() {
  return async (name: string, input: Record<string, unknown>, ctx: ToolContext) => {
    if (name === 'delegate') {
      if (!ctx.runDelegate) return { output: 'no runDelegate', isError: true };
      const { result, status } = await ctx.runDelegate({
        description: String(input['description'] ?? ''),
        prompt: String(input['prompt'] ?? ''),
      });
      if (status === 'aborted') return { output: `[aborted] ${result}`, isError: true };
      return { output: result, isError: false };
    }
    return { output: 'ok', isError: false };
  };
}

describe('delegate live stream — Task 1', () => {
  afterEach(() => { executeToolMock.mockReset(); getToolDefsMock.mockReset(); });

  it('サブの text は onDelegateText に届き、メイン onText には届かない。lifecycle が running→success で発火', async () => {
    const client = new FakeClient([
      // [0] parent turn 1 — delegate
      [{ type: 'tool_use', id: 'd1', name: 'delegate', input: { description: 'sub A', prompt: 'go A' } }, { type: 'done' }],
      // [1] sub turn 1 — emits live text then complete
      [{ type: 'text', text: 'SUB_LIVE_MARKER' }, { type: 'tool_use', id: 'c1', name: 'complete', input: { status: 'success', result: 'sub done' } }, { type: 'done' }],
      // [2] parent turn 2 — complete
      [{ type: 'tool_use', id: 'pc1', name: 'complete', input: { status: 'success', result: 'parent done' } }, { type: 'done' }],
    ]);
    getToolDefsMock.mockResolvedValue(makeToolDefs(['delegate']));
    executeToolMock.mockImplementation(delegateExecuteToolImpl());

    const onText = vi.fn();
    const onDelegateText = vi.fn();
    const lifecycle: Array<{ id: string; status: string }> = [];
    const callbacks: AgentLoopCallbacks = {
      onText,
      onDelegateText,
      onDelegateLifecycle: (info) => lifecycle.push({ id: info.delegateRunId, status: info.status }),
    };

    const result = await executeMovement(
      PARENT_MOVEMENT, 'run parent', client as never, makeContext({ delegationDepth: 0 }),
      { conversation: new Conversation(undefined), toolResultCache: new ToolResultCache(), callbacks },
    );

    expect(result.next).toBe('COMPLETE');
    // サブの text は delegate チャンネルへ
    expect(onDelegateText).toHaveBeenCalledWith(expect.any(String), 'SUB_LIVE_MARKER');
    const runId = onDelegateText.mock.calls[0]![0] as string;
    expect(runId).toBeTruthy();
    // メイン onText は サブの text を受け取らない
    expect(onText.mock.calls.flat()).not.toContain('SUB_LIVE_MARKER');
    // lifecycle は同じ runId で running→success
    expect(lifecycle).toEqual([
      { id: runId, status: 'running' },
      { id: runId, status: 'success' },
    ]);
  });

  it('入れ子 depth-2 のサブ text は自分の delegateRunId で浮上する', async () => {
    const client = new FakeClient([
      // [0] parent → delegate level1
      [{ type: 'tool_use', id: 'd0', name: 'delegate', input: { description: 'L1', prompt: 'go L1' } }, { type: 'done' }],
      // [1] depth-1 → delegate level2
      [{ type: 'tool_use', id: 'd1', name: 'delegate', input: { description: 'L2', prompt: 'go L2' } }, { type: 'done' }],
      // [2] depth-2 → live text + complete
      [{ type: 'text', text: 'DEEP_MARKER' }, { type: 'tool_use', id: 'c2', name: 'complete', input: { status: 'success', result: 'deep' } }, { type: 'done' }],
      // [3] depth-1 complete
      [{ type: 'tool_use', id: 'c1', name: 'complete', input: { status: 'success', result: 'mid' } }, { type: 'done' }],
      // [4] parent complete
      [{ type: 'tool_use', id: 'cp', name: 'complete', input: { status: 'success', result: 'top' } }, { type: 'done' }],
    ]);
    getToolDefsMock.mockImplementation(async (allowed: string[]) => makeToolDefs(allowed));
    executeToolMock.mockImplementation(delegateExecuteToolImpl());

    const deepTextCalls: Array<[string, string]> = [];
    const lifecycleIds = new Set<string>();
    const callbacks: AgentLoopCallbacks = {
      onDelegateText: (id, text) => { if (text === 'DEEP_MARKER') deepTextCalls.push([id, text]); },
      onDelegateLifecycle: (info) => lifecycleIds.add(info.delegateRunId),
    };

    await executeMovement(
      PARENT_MOVEMENT, 'run nested', client as never, makeContext({ delegationDepth: 0 }),
      { conversation: new Conversation(undefined), toolResultCache: new ToolResultCache(), callbacks },
    );

    expect(deepTextCalls).toHaveLength(1);
    const deepRunId = deepTextCalls[0]![0];
    // depth-2 の runId は lifecycle にも登場（=自分の id で浮上）し、
    // 2 つの異なる delegateRunId（depth-1, depth-2）が観測される。
    expect(lifecycleIds.has(deepRunId)).toBe(true);
    expect(lifecycleIds.size).toBe(2);
  });
});
