/**
 * 相 C — E2E: sns-deep-sweep の orchestrator パターン（delegate fan-out + 統合 + 隔離）。
 *
 * 実 runPiece → executeMovement → runDelegateSubAgent を走らせ、tools/index.js だけモック。
 *
 * 検証:
 *   (a) 親が delegate を2回呼び（2投稿）、各サブが output/deepdive/tweet-N.md を書く。
 *   (b) 親が output/report.md を書き、各 deepdive への相対リンクを含む。
 *   (c) 隔離: 各サブの prompt（深掘り指示）が親の user メッセージとして現れない。
 */

import { afterEach, describe, expect, it, vi } from 'vitest';
import { mkdirSync, mkdtempSync, readFileSync, existsSync, rmSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { tmpdir } from 'node:os';
import type { LLMEvent, ToolDef } from '../llm/openai-compat.js';
import type { PieceDef } from './piece-runner.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 { runPiece } from './piece-runner.js';
import { Conversation } from './context/conversation.js';

const TWEET1_PROMPT = 'DEEPDIVE_PROMPT_TWEET_1_marker_aa11 投稿1を深掘りせよ';
const TWEET2_PROMPT = 'DEEPDIVE_PROMPT_TWEET_2_marker_bb22 投稿2を深掘りせよ';
const SUMMARY1 = 'SUMMARY_TWEET_1_cc33';
const SUMMARY2 = 'SUMMARY_TWEET_2_dd44';
const DEEP1 = 'DEEPDIVE_BODY_TWEET_1_ee55';
const DEEP2 = 'DEEPDIVE_BODY_TWEET_2_ff66';
const PARENT_TASK = 'PARENT_SWEEP_TASK_gg77';

class FakeClient {
  private index = 0;
  readonly snapshots: Array<Array<{ role: string; content?: unknown }>> = [];
  constructor(private readonly responses: LLMEvent[][]) {}
  async *chat(messages: unknown): AsyncGenerator<LLMEvent> {
    this.snapshots.push((messages as Array<Record<string, unknown>>).map((m) => ({ ...m })) as never);
    const response = this.responses[this.index++] ?? [];
    for (const event of response) yield event;
  }
}

function makeToolDefs(names: string[]): ToolDef[] {
  return names.map((name) => ({
    type: 'function' as const,
    function: { name, description: name, parameters: { type: 'object', properties: {}, required: [] } },
  }));
}

function orchestratePiece(): PieceDef {
  return {
    name: 'sns-deep-sweep-e2e',
    description: 'E2E test piece for the orchestrator pattern',
    max_movements: 5,
    initial_movement: 'orchestrate',
    movements: [
      {
        name: 'orchestrate',
        edit: true,
        persona: 'researcher',
        instruction: 'Fetch, select, delegate per tweet, integrate.',
        allowed_tools: ['delegate', 'Write', 'Glob'],
        rules: [],
        default_next: 'COMPLETE',
      },
    ],
  };
}

describe('runPiece — sns-deep-sweep orchestrator E2E (相 C)', () => {
  let workspacePath = '';

  afterEach(() => {
    executeToolMock.mockReset();
    getToolDefsMock.mockReset();
    if (workspacePath) {
      rmSync(workspacePath, { recursive: true, force: true });
      workspacePath = '';
    }
  });

  it('(a)(b)(c) delegate fan-out writes per-tweet files, parent integrates report, sub prompts isolated', async () => {
    workspacePath = mkdtempSync(join(tmpdir(), 'sns-deep-sweep-e2e-'));
    const logsRoot = join(workspacePath, 'logs');
    mkdirSync(logsRoot, { recursive: true });

    getToolDefsMock.mockResolvedValue(makeToolDefs(['delegate', 'Write', 'Glob']));

    // Write actually persists files so we can assert the deepdive/report layout.
    executeToolMock.mockImplementation(
      async (name: string, input: Record<string, unknown>, ctx: ToolContext) => {
        if (name === 'delegate') {
          if (!ctx.runDelegate) return { output: 'runDelegate not injected', isError: true };
          const { result, status } = await ctx.runDelegate({
            description: String(input['description'] ?? ''),
            prompt: String(input['prompt'] ?? ''),
          });
          if (status === 'aborted') return { output: `[delegate 中断] ${result}`, isError: true };
          return { output: result, isError: false };
        }
        if (name === 'Write') {
          const rel = String(input['path'] ?? input['file_path'] ?? '');
          const abs = join(ctx.workspacePath!, rel);
          mkdirSync(dirname(abs), { recursive: true });
          writeFileSync(abs, String(input['content'] ?? ''), 'utf8');
          return { output: `wrote ${rel}`, isError: false };
        }
        if (name === 'Glob') {
          return { output: 'output/deepdive/tweet-1.md\noutput/deepdive/tweet-2.md', isError: false };
        }
        return { output: 'ok', isError: false };
      },
    );

    const REPORT = `# まとめ\n- [tweet-1](./deepdive/tweet-1.md) ${SUMMARY1}\n- [tweet-2](./deepdive/tweet-2.md) ${SUMMARY2}`;

    const client = new FakeClient([
      // parent turn 1 — delegate tweet 1
      [
        { type: 'tool_use', id: 'p-del-1', name: 'delegate', input: { description: 'tweet 1/2', prompt: TWEET1_PROMPT } },
        { type: 'done', usage: { prompt_tokens: 100, completion_tokens: 10 } },
      ],
      // sub 1 turn 1 — Write deepdive file
      [
        { type: 'tool_use', id: 's1-w', name: 'Write', input: { path: 'output/deepdive/tweet-1.md', content: DEEP1 } },
        { type: 'done', usage: { prompt_tokens: 80, completion_tokens: 5 } },
      ],
      // sub 1 turn 2 — complete with short summary
      [
        { type: 'tool_use', id: 's1-c', name: 'complete', input: { status: 'success', result: SUMMARY1 } },
        { type: 'done', usage: { prompt_tokens: 80, completion_tokens: 5 } },
      ],
      // parent turn 2 — delegate tweet 2
      [
        { type: 'tool_use', id: 'p-del-2', name: 'delegate', input: { description: 'tweet 2/2', prompt: TWEET2_PROMPT } },
        { type: 'done', usage: { prompt_tokens: 110, completion_tokens: 10 } },
      ],
      // sub 2 turn 1 — Write deepdive file
      [
        { type: 'tool_use', id: 's2-w', name: 'Write', input: { path: 'output/deepdive/tweet-2.md', content: DEEP2 } },
        { type: 'done', usage: { prompt_tokens: 80, completion_tokens: 5 } },
      ],
      // sub 2 turn 2 — complete
      [
        { type: 'tool_use', id: 's2-c', name: 'complete', input: { status: 'success', result: SUMMARY2 } },
        { type: 'done', usage: { prompt_tokens: 80, completion_tokens: 5 } },
      ],
      // parent turn 3 — Write report
      [
        { type: 'tool_use', id: 'p-w', name: 'Write', input: { path: 'output/report.md', content: REPORT } },
        { type: 'done', usage: { prompt_tokens: 120, completion_tokens: 10 } },
      ],
      // parent turn 4 — complete
      [
        { type: 'tool_use', id: 'p-c', name: 'complete', input: { status: 'success', result: REPORT } },
        { type: 'done', usage: { prompt_tokens: 120, completion_tokens: 10 } },
      ],
    ]);

    const result = await runPiece(
      orchestratePiece(),
      PARENT_TASK,
      client as never,
      workspacePath,
      undefined,
      undefined,
      // PR B: tool availability comes from the workspace policy, not the piece.
      { runtimeDir: logsRoot, workspaceTools: { allowedTools: ['delegate', 'Write', 'Glob'], editAllowed: true } },
    );

    // --- completion ---
    expect(result.status).toBe('completed');

    // --- (a) each sub wrote its own deepdive file ---
    const f1 = join(workspacePath, 'output/deepdive/tweet-1.md');
    const f2 = join(workspacePath, 'output/deepdive/tweet-2.md');
    expect(existsSync(f1)).toBe(true);
    expect(existsSync(f2)).toBe(true);
    expect(readFileSync(f1, 'utf8')).toContain(DEEP1);
    expect(readFileSync(f2, 'utf8')).toContain(DEEP2);

    // --- (b) parent integrated report with relative links ---
    const report = readFileSync(join(workspacePath, 'output/report.md'), 'utf8');
    expect(report).toContain('./deepdive/tweet-1.md');
    expect(report).toContain('./deepdive/tweet-2.md');

    // delegate was called exactly twice (one per selected tweet).
    const delegateCalls = executeToolMock.mock.calls.filter((c) => c[0] === 'delegate');
    expect(delegateCalls).toHaveLength(2);

    // --- (c) isolation: sub prompts never appear as parent user messages ---
    // NOTE: TWEET*_PROMPT legitimately appears elsewhere in the parent transcript —
    // inside the parent's own assistant `delegate` tool_call args. That is by design.
    // The leak we guard against is a sub's prompt becoming a parent *user* message
    // (which would mean the sub's seeded conversation folded into the parent thread),
    // so the check is scoped to role === 'user'.
    const transcriptPath = join(logsRoot, 'transcript.jsonl');
    expect(existsSync(transcriptPath)).toBe(true);
    const parent = Conversation.loadFrom(transcriptPath);
    const parentUser = parent.filter((m) => m.role === 'user');
    for (const marker of [TWEET1_PROMPT, TWEET2_PROMPT]) {
      const leaked = parentUser.some((m) => typeof m.content === 'string' && m.content.includes(marker));
      expect(leaked, `sub prompt leaked into parent user message: ${marker}`).toBe(false);
    }
    // The deepdive bodies (written only inside the subs) must not pollute the parent thread.
    const parentJson = JSON.stringify(parent);
    expect(parentJson).not.toContain(DEEP1);
    expect(parentJson).not.toContain(DEEP2);
  });
});
