import { buildSessionTreeSnapshot } from '../src/modules/netaclaw/session-tree/snapshot.js';
import { ROOT_PARENT_KEY } from '../src/modules/netaclaw/session-tree/path.js';
import type {
  LabelEntry,
  ModelChangeEntry,
  SessionMessageEntry,
  SessionTreeEntry,
  SessionTreeMessage,
  SessionTreeSession,
  SubagentBatchEntry,
  SubagentResultEntry,
  ThinkingLevelChangeEntry,
} from '../src/modules/netaclaw/session-tree/types.js';

describe('session tree snapshot builder', () => {
  const createSession = (leafEntryId: string | null): SessionTreeSession => ({
    sessionId: 'session_snapshot_001',
    provider: 'file',
    rootEntryId: 'root',
    leafEntryId,
  });

  const createMessageEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    message: SessionTreeMessage,
  ): SessionMessageEntry => ({
    id,
    parentId,
    timestamp,
    type: 'message',
    message,
  });

  const createLabelEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    targetId: string,
    label: string | undefined,
  ): LabelEntry => ({
    id,
    parentId,
    timestamp,
    type: 'label',
    targetId,
    label,
  });

  const createThinkingLevelEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    thinkingLevel: string,
  ): ThinkingLevelChangeEntry => ({
    id,
    parentId,
    timestamp,
    type: 'thinking_level_change',
    thinkingLevel,
  });

  const createModelChangeEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    provider: string,
    modelId: string,
  ): ModelChangeEntry => ({
    id,
    parentId,
    timestamp,
    type: 'model_change',
    provider,
    modelId,
  });

  const createSubagentBatchEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    status: 'running' | 'completed',
    metadata?: Record<string, unknown>,
  ): SubagentBatchEntry => ({
    id,
    parentId,
    timestamp,
    type: 'subagent_batch',
    content: {
      batchId: `${id}_batch`,
      mode: 'parallel',
      tasks: [
        {
          id: 'task_1',
          prompt: 'collect data',
        },
      ],
      status,
      parentEntryId: parentId,
    },
    metadata,
  });

  const createSubagentResultEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    metadata?: Record<string, unknown>,
  ): SubagentResultEntry => ({
    id,
    parentId,
    timestamp,
    type: 'subagent_result',
    content: {
      batchId: `${id}_batch`,
      results: [
        {
          id: 'subagent_1',
          status: 'completed',
          summary: 'found 2 images',
        },
      ],
      status: 'completed',
      parentEntryId: parentId,
    },
    metadata,
  });

  it('projects the active branch only and keeps the original session and entry references', () => {
    const rootMessage = { role: 'user', content: 'root' } satisfies SessionTreeMessage;
    const branchAMessage = { role: 'assistant', content: 'branch-a' } satisfies SessionTreeMessage;
    const branchBMessage = { role: 'assistant', content: 'branch-b' } satisfies SessionTreeMessage;
    const session = createSession('branch-a');
    const root = createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage);
    const branchA = createMessageEntry('branch-a', 'root', '2026-04-19T00:01:00.000Z', branchAMessage);
    const branchB = createMessageEntry('branch-b', 'root', '2026-04-19T00:02:00.000Z', branchBMessage);
    const entries: SessionTreeEntry[] = [root, branchA, branchB];

    const snapshot = buildSessionTreeSnapshot(session, entries);

    expect(snapshot.session).toBe(session);
    expect(snapshot.entries).toBe(entries);
    expect(snapshot.activePath).toEqual([root, branchA]);
    expect(snapshot.activePath).not.toContain(branchB);
    expect(snapshot.runtimeContext.messages).toEqual([rootMessage, branchAMessage]);
    expect(snapshot.runtimeContext.messages).not.toContain(branchBMessage);
  });

  it('groups children under ROOT_PARENT_KEY and real parent ids', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'root',
      }),
      createMessageEntry('child-a', 'root', '2026-04-19T00:01:00.000Z', {
        role: 'assistant',
        content: 'child-a',
      }),
      createMessageEntry('child-b', 'root', '2026-04-19T00:02:00.000Z', {
        role: 'assistant',
        content: 'child-b',
      }),
    ];

    const snapshot = buildSessionTreeSnapshot(createSession('child-b'), entries);

    expect(snapshot.childrenByParentId).toEqual({
      [ROOT_PARENT_KEY]: ['root'],
      root: ['child-a', 'child-b'],
    });
  });

  it('uses the latest label state and respects undefined clears', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'root',
      }),
      createMessageEntry('leaf', 'root', '2026-04-19T00:00:01.000Z', {
        role: 'assistant',
        content: 'leaf',
      }),
      createLabelEntry('label-root-old', 'leaf', '2026-04-19T00:00:02.000Z', 'root', 'old'),
      createLabelEntry('label-root-new', 'leaf', '2026-04-19T00:00:03.000Z', 'root', 'new'),
      createLabelEntry('label-leaf', 'leaf', '2026-04-19T00:00:04.000Z', 'leaf', 'keep'),
      createLabelEntry('label-leaf-clear', 'leaf', '2026-04-19T00:00:05.000Z', 'leaf', undefined),
    ];

    const snapshot = buildSessionTreeSnapshot(createSession('leaf'), entries);

    expect(snapshot.labelsByEntryId).toEqual({
      root: {
        label: 'new',
        timestamp: '2026-04-19T00:00:03.000Z',
        entryId: 'label-root-new',
      },
    });
  });

  it('returns an empty active path and empty runtime messages when leafEntryId is null', () => {
    const session = createSession(null);
    session.rootEntryId = null;
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'root',
      }),
      createThinkingLevelEntry('thinking', 'root', '2026-04-19T00:01:00.000Z', 'high'),
      createModelChangeEntry(
        'model',
        'thinking',
        '2026-04-19T00:02:00.000Z',
        'openai',
        'gpt-5',
      ),
      createMessageEntry('leaf', 'model', '2026-04-19T00:03:00.000Z', {
        role: 'assistant',
        content: 'must not leak',
      }),
    ];

    const snapshot = buildSessionTreeSnapshot(session, entries);

    expect(snapshot.activePath).toEqual([]);
    expect(snapshot.runtimeContext.messages).toEqual([]);
    expect(snapshot.runtimeContext.thinkingLevel).toBe('off');
    expect(snapshot.runtimeContext.model).toBeNull();
  });

  it('propagates missing parent errors from the active path resolution', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('orphan', 'missing-parent', '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'broken',
      }),
    ];

    expect(() => buildSessionTreeSnapshot(createSession('orphan'), entries)).toThrow(
      'Parent entry missing-parent not found',
    );
  });

  it('keeps subagent batch entries in the active path and snapshot entries but excludes them from runtime context', () => {
    const runtimeEvent = {
      sequence: 1,
      timestamp: '2026-04-19T00:01:05.000Z',
      event: {
        kind: 'result',
        status: 'completed',
        subagent: {
          id: 'subagent_1',
          summary: 'done',
        },
      },
    };

    const rootMessage = { role: 'user', content: 'run subagents' } satisfies SessionTreeMessage;
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage),
      createSubagentBatchEntry('subagent-batch', 'root', '2026-04-19T00:01:00.000Z', 'completed', {
        latestEvent: runtimeEvent,
        events: [runtimeEvent],
        finalResults: [
          {
            id: 'subagent_1',
            summary: 'done',
          },
        ],
      }),
    ];

    const snapshot = buildSessionTreeSnapshot(createSession('subagent-batch'), entries);

    expect(snapshot.activePath.map(entry => entry.id)).toEqual(['root', 'subagent-batch']);
    expect(snapshot.entries.find(entry => entry.id === 'subagent-batch')).toMatchObject({
      metadata: {
        latestEvent: runtimeEvent,
        events: [runtimeEvent],
        finalResults: [
          {
            id: 'subagent_1',
            summary: 'done',
          },
        ],
      },
    });
    expect(snapshot.runtimeContext.messages).toEqual([rootMessage]);
    expect(snapshot.runtimeContext.thinkingLevel).toBe('off');
    expect(snapshot.runtimeContext.model).toBeNull();
  });

  it('keeps subagent result evidence and process metadata in snapshots without leaking it into runtime context', () => {
    const evidenceSummary = {
      kind: 'file-count',
      title: '图片文件统计',
      count: 2,
      files: ['1.jpg', '2.png'],
      summary: ['根据工具结果统计，图片文件共 2 个。', '- 1.jpg', '- 2.png'].join('\n'),
    };
    const processEvent = {
      type: 'tool_call',
      runId: 'subagent-101',
      timestamp: '2026-04-19T00:01:02.000Z',
      name: 'find_files',
      toolCallId: 'tool-call-1',
    };
    const rootMessage = { role: 'user', content: 'run subagents' } satisfies SessionTreeMessage;
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage),
      createSubagentResultEntry('subagent-result', 'root', '2026-04-19T00:01:00.000Z', {
        evidenceSummaries: [evidenceSummary],
        processEvents: [processEvent],
      }),
    ];

    const snapshot = buildSessionTreeSnapshot(createSession('subagent-result'), entries);

    expect(snapshot.activePath.map(entry => entry.id)).toEqual(['root', 'subagent-result']);
    expect(snapshot.entries.find(entry => entry.id === 'subagent-result')).toMatchObject({
      type: 'subagent_result',
      metadata: {
        evidenceSummaries: [evidenceSummary],
        processEvents: [processEvent],
      },
    });
    expect(snapshot.runtimeContext.messages).toEqual([rootMessage]);
    expect(snapshot.runtimeContext.thinkingLevel).toBe('off');
    expect(snapshot.runtimeContext.model).toBeNull();
  });
});
