import { buildSessionContext } from '../src/modules/netaclaw/session-tree/context_builder.js';
import type {
  BranchSummaryEntry,
  CompactionEntry,
  CustomEntry,
  CustomMessageEntry,
  LabelEntry,
  ModelChangeEntry,
  SessionInfoEntry,
  SessionMessageEntry,
  SessionTreeEntry,
  SessionTreeMessage,
  ThinkingLevelChangeEntry,
} from '../src/modules/netaclaw/session-tree/types.js';

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

  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 createCompactionEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    summary: string,
    firstKeptEntryId: string,
    tokensBefore: number,
  ): CompactionEntry => ({
    id,
    parentId,
    timestamp,
    type: 'compaction',
    summary,
    firstKeptEntryId,
    tokensBefore,
  });

  const createBranchSummaryEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    fromId: string,
    summary: string,
  ): BranchSummaryEntry => ({
    id,
    parentId,
    timestamp,
    type: 'branch_summary',
    fromId,
    summary,
  });

  const createCustomEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    customType: string,
  ): CustomEntry => ({
    id,
    parentId,
    timestamp,
    type: 'custom',
    customType,
  });

  const createCustomMessageEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    customType: string,
    content: unknown,
    display: boolean,
    details?: Record<string, unknown>,
  ): CustomMessageEntry => ({
    id,
    parentId,
    timestamp,
    type: 'custom_message',
    customType,
    content,
    display,
    details,
  });

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

  const createSessionInfoEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
    name: string,
  ): SessionInfoEntry => ({
    id,
    parentId,
    timestamp,
    type: 'session_info',
    name,
  });

  it('returns an empty context when leafId is null', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'ignored',
      }),
    ];

    expect(buildSessionContext(entries, null)).toEqual({
      messages: [],
      thinkingLevel: 'off',
      model: null,
    });
  });

  it('projects only the active path messages and ignores sibling branches', () => {
    const rootMessage = { role: 'user', content: 'root' } satisfies SessionTreeMessage;
    const mainLeafMessage = { role: 'assistant', content: 'main leaf' } satisfies SessionTreeMessage;
    const sideLeafMessage = { role: 'assistant', content: 'side leaf' } satisfies SessionTreeMessage;

    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage),
      createMessageEntry('main', 'root', '2026-04-19T00:01:00.000Z', mainLeafMessage),
      createMessageEntry('side', 'root', '2026-04-19T00:02:00.000Z', sideLeafMessage),
    ];

    const context = buildSessionContext(entries, 'main');

    expect(context.messages).toEqual([rootMessage, mainLeafMessage]);
    expect(context.messages).not.toContain(sideLeafMessage);
  });

  it('falls back to the last entry when leafId is undefined or missing', () => {
    const rootMessage = { role: 'user', content: 'root' } satisfies SessionTreeMessage;
    const mainLeafMessage = { role: 'assistant', content: 'main leaf' } satisfies SessionTreeMessage;
    const lastLeafMessage = { role: 'assistant', content: 'last leaf' } satisfies SessionTreeMessage;
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage),
      createMessageEntry('main', 'root', '2026-04-19T00:01:00.000Z', mainLeafMessage),
      createMessageEntry('last', 'root', '2026-04-19T00:02:00.000Z', lastLeafMessage),
    ];

    expect(buildSessionContext(entries).messages).toEqual([rootMessage, lastLeafMessage]);
    expect(buildSessionContext(entries, 'missing').messages).toEqual([rootMessage, lastLeafMessage]);
  });

  it('uses a Map index to resolve the leaf and parent path when entries are incomplete', () => {
    const rootMessage = { role: 'user', content: 'root from index' } satisfies SessionTreeMessage;
    const parentMessage = { role: 'assistant', content: 'parent from index' } satisfies SessionTreeMessage;
    const leafMessage = { role: 'user', content: 'leaf from entries' } satisfies SessionTreeMessage;

    const root = createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage);
    const parent = createMessageEntry('parent', 'root', '2026-04-19T00:01:00.000Z', parentMessage);
    const leaf = createMessageEntry('leaf', 'parent', '2026-04-19T00:02:00.000Z', leafMessage);
    const entries: SessionTreeEntry[] = [leaf];
    const byId = new Map<string, SessionTreeEntry>([
      [root.id, root],
      [parent.id, parent],
      [leaf.id, leaf],
    ]);

    expect(buildSessionContext(entries, 'leaf', byId).messages).toEqual([
      rootMessage,
      parentMessage,
      leafMessage,
    ]);
  });

  it('uses a Record index to resolve parent paths when entries are incomplete', () => {
    const rootMessage = { role: 'user', content: 'root from record' } satisfies SessionTreeMessage;
    const leafMessage = { role: 'assistant', content: 'leaf from entries' } satisfies SessionTreeMessage;
    const root = createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage);
    const leaf = createMessageEntry('leaf', 'root', '2026-04-19T00:01:00.000Z', leafMessage);
    const entries: SessionTreeEntry[] = [leaf];
    const byId: Record<string, SessionTreeEntry> = {
      [root.id]: root,
      [leaf.id]: leaf,
    };

    expect(buildSessionContext(entries, 'leaf', byId).messages).toEqual([
      rootMessage,
      leafMessage,
    ]);
  });

  it('falls back to the last entry through the same normalized index when byId is incomplete', () => {
    const rootMessage = { role: 'user', content: 'root from entries' } satisfies SessionTreeMessage;
    const lastLeafMessage = { role: 'assistant', content: 'last leaf from entries' } satisfies SessionTreeMessage;
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage),
      createMessageEntry('last', 'root', '2026-04-19T00:01:00.000Z', lastLeafMessage),
    ];
    const byId = new Map<string, SessionTreeEntry>();

    expect(buildSessionContext(entries, 'missing', byId).messages).toEqual([
      rootMessage,
      lastLeafMessage,
    ]);
  });

  it('does not resolve missing leaf ids from Record prototype keys', () => {
    const rootMessage = { role: 'user', content: 'root' } satisfies SessionTreeMessage;
    const lastLeafMessage = { role: 'assistant', content: 'last leaf' } satisfies SessionTreeMessage;
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage),
      createMessageEntry('last', 'root', '2026-04-19T00:01:00.000Z', lastLeafMessage),
    ];
    const byId: Record<string, SessionTreeEntry> = {
      root: entries[0],
      last: entries[1],
    };

    expect(buildSessionContext(entries, 'constructor', byId).messages).toEqual([
      rootMessage,
      lastLeafMessage,
    ]);
  });

  it('does not resolve missing parent ids from Record prototype keys', () => {
    const leaf = createMessageEntry('leaf', 'constructor', '2026-04-19T00:00:00.000Z', {
      role: 'user',
      content: 'broken path',
    });
    const byId: Record<string, SessionTreeEntry> = {
      [leaf.id]: leaf,
    };

    expect(() => buildSessionContext([leaf], 'leaf', byId)).toThrow(
      'Parent entry constructor not found',
    );
  });

  it('updates runtime settings from thinking and model changes without emitting them as messages', () => {
    const rootMessage = { role: 'user', content: 'root' } satisfies SessionTreeMessage;
    const leafMessage = { role: 'user', content: 'leaf' } satisfies SessionTreeMessage;

    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage),
      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', leafMessage),
    ];

    expect(buildSessionContext(entries, 'leaf')).toEqual({
      messages: [rootMessage, leafMessage],
      thinkingLevel: 'high',
      model: {
        provider: 'openai',
        modelId: 'gpt-5',
      },
    });
  });

  it('updates model from assistant messages and preserves the original message object', () => {
    const userMessage = { role: 'user', content: 'hello' } satisfies SessionTreeMessage;
    const assistantMessage = {
      role: 'assistant',
      content: 'world',
      provider: 'anthropic',
      model: 'claude-sonnet-4-20250514',
    } satisfies SessionTreeMessage;

    const entries: SessionTreeEntry[] = [
      createMessageEntry('user', null, '2026-04-19T00:00:00.000Z', userMessage),
      createMessageEntry('assistant', 'user', '2026-04-19T00:01:00.000Z', assistantMessage),
    ];

    const context = buildSessionContext(entries, 'assistant');

    expect(context.messages).toHaveLength(2);
    expect(context.messages[1]).toBe(assistantMessage);
    expect(context.model).toEqual({
      provider: 'anthropic',
      modelId: 'claude-sonnet-4-20250514',
    });
  });

  it('includes branch summaries and custom messages but excludes non-message entry types', () => {
    const rootMessage = { role: 'user', content: 'root' } satisfies SessionTreeMessage;
    const leafMessage = { role: 'assistant', content: 'leaf' } satisfies SessionTreeMessage;

    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', rootMessage),
      createCustomEntry('custom', 'root', '2026-04-19T00:01:00.000Z', 'agent_event'),
      createBranchSummaryEntry(
        'branch',
        'custom',
        '2026-04-19T00:02:00.000Z',
        'root',
        'branched away and returned',
      ),
      createCustomMessageEntry(
        'custom-message',
        'branch',
        '2026-04-19T00:03:00.000Z',
        'notice',
        'custom content',
        true,
        { severity: 'info' },
      ),
      createLabelEntry('label', 'custom-message', '2026-04-19T00:04:00.000Z', 'root', 'bookmark'),
      createSessionInfoEntry('session-info', 'label', '2026-04-19T00:05:00.000Z', 'renamed session'),
      createMessageEntry('leaf', 'session-info', '2026-04-19T00:06:00.000Z', leafMessage),
    ];

    expect(buildSessionContext(entries, 'leaf').messages).toEqual([
      rootMessage,
      {
        role: 'branchSummary',
        summary: 'branched away and returned',
        fromId: 'root',
        timestamp: new Date('2026-04-19T00:02:00.000Z').getTime(),
      },
      {
        role: 'custom',
        customType: 'notice',
        content: 'custom content',
        display: true,
        details: { severity: 'info' },
        timestamp: new Date('2026-04-19T00:03:00.000Z').getTime(),
      },
      leafMessage,
    ]);
  });

  it('uses compaction summary plus kept entries and post-compaction messages', () => {
    const droppedMessage = { role: 'user', content: 'drop me' } satisfies SessionTreeMessage;
    const keptMessage = { role: 'assistant', content: 'keep me' } satisfies SessionTreeMessage;
    const afterMessage = { role: 'assistant', content: 'after compaction' } satisfies SessionTreeMessage;

    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'very old root',
      }),
      createMessageEntry('drop', 'root', '2026-04-19T00:01:00.000Z', droppedMessage),
      createMessageEntry('keep', 'drop', '2026-04-19T00:02:00.000Z', keptMessage),
      createCompactionEntry(
        'compaction',
        'keep',
        '2026-04-19T00:03:00.000Z',
        'summary text',
        'keep',
        4096,
      ),
      createBranchSummaryEntry(
        'branch',
        'compaction',
        '2026-04-19T00:04:00.000Z',
        'drop',
        'branch after compaction',
      ),
      createCustomMessageEntry(
        'custom-message',
        'branch',
        '2026-04-19T00:05:00.000Z',
        'notice',
        'after custom',
        false,
      ),
      createMessageEntry('after', 'custom-message', '2026-04-19T00:06:00.000Z', afterMessage),
    ];

    expect(buildSessionContext(entries, 'after').messages).toEqual([
      {
        role: 'compactionSummary',
        summary: 'summary text',
        tokensBefore: 4096,
        timestamp: new Date('2026-04-19T00:03:00.000Z').getTime(),
      },
      keptMessage,
      {
        role: 'branchSummary',
        summary: 'branch after compaction',
        fromId: 'drop',
        timestamp: new Date('2026-04-19T00:04:00.000Z').getTime(),
      },
      {
        role: 'custom',
        customType: 'notice',
        content: 'after custom',
        display: false,
        details: undefined,
        timestamp: new Date('2026-04-19T00:05:00.000Z').getTime(),
      },
      afterMessage,
    ]);
  });

  it('uses the latest compaction on the active path', () => {
    const firstKeptMessage = { role: 'user', content: 'first kept' } satisfies SessionTreeMessage;
    const secondKeptMessage = { role: 'assistant', content: 'second kept' } satisfies SessionTreeMessage;
    const finalMessage = { role: 'assistant', content: 'final' } satisfies SessionTreeMessage;

    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'old root',
      }),
      createMessageEntry('first-kept', 'root', '2026-04-19T00:01:00.000Z', firstKeptMessage),
      createCompactionEntry(
        'compaction-1',
        'first-kept',
        '2026-04-19T00:02:00.000Z',
        'old summary',
        'first-kept',
        100,
      ),
      createMessageEntry('second-kept', 'compaction-1', '2026-04-19T00:03:00.000Z', secondKeptMessage),
      createCompactionEntry(
        'compaction-2',
        'second-kept',
        '2026-04-19T00:04:00.000Z',
        'new summary',
        'second-kept',
        200,
      ),
      createMessageEntry('final', 'compaction-2', '2026-04-19T00:05:00.000Z', finalMessage),
    ];

    expect(buildSessionContext(entries, 'final').messages).toEqual([
      {
        role: 'compactionSummary',
        summary: 'new summary',
        tokensBefore: 200,
        timestamp: new Date('2026-04-19T00:04:00.000Z').getTime(),
      },
      secondKeptMessage,
      finalMessage,
    ]);
  });

  it('throws when a compaction first kept entry is missing from the active path', () => {
    const finalMessage = { role: 'assistant', content: 'final' } satisfies SessionTreeMessage;
    const entries: SessionTreeEntry[] = [
      createMessageEntry('root', null, '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'root',
      }),
      createCompactionEntry(
        'compaction-missing-kept',
        'root',
        '2026-04-19T00:01:00.000Z',
        'summary text',
        'not-on-active-path',
        100,
      ),
      createMessageEntry('final', 'compaction-missing-kept', '2026-04-19T00:02:00.000Z', finalMessage),
    ];

    expect(() => buildSessionContext(entries, 'final')).toThrow(
      'Compaction compaction-missing-kept firstKeptEntryId not-on-active-path not found on active path',
    );
  });

  it('throws when an active path parent is missing', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('orphan', 'missing-parent', '2026-04-19T00:00:00.000Z', {
        role: 'user',
        content: 'broken path',
      }),
    ];

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