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

import { v4 as uuidv4, v7 as uuidv7 } from 'uuid';
import type { LabelEntry, SessionTreeEntry } from '../src/modules/netaclaw/session-tree/types.js';
import {
  createSessionTreeEntryId,
  createSessionTreeSessionId,
} from '../src/modules/netaclaw/session-tree/id.js';
import {
  ROOT_PARENT_KEY,
  buildEntryIndex,
  findCommonAncestorEntryId,
  getPathToLeaf,
  groupChildrenByParent,
  resolveLatestLabels,
} from '../src/modules/netaclaw/session-tree/path.js';

describe('session tree path helpers', () => {
  const mockedUuidV4 = uuidv4 as unknown as jest.Mock<string, []>;
  const mockedUuidV7 = uuidv7 as unknown as jest.Mock<string, []>;

  const createMessageEntry = (
    id: string,
    parentId: string | null,
    timestamp: string,
  ): SessionTreeEntry => ({
    id,
    parentId,
    timestamp,
    type: 'message',
    message: {
      role: 'user',
      content: `${id}-content`,
    },
  });

  beforeEach(() => {
    mockedUuidV4.mockReset();
    mockedUuidV7.mockReset();
  });

  it('createSessionTreeSessionId returns a non-empty string', () => {
    mockedUuidV7.mockReturnValue('session-v7-id');

    expect(createSessionTreeSessionId()).toBe('session-v7-id');
  });

  it('createSessionTreeEntryId returns a non-empty string', () => {
    mockedUuidV4.mockReturnValue('12345678-1234-1234-1234-1234567890ab');

    const entryId = createSessionTreeEntryId();

    expect(entryId).toBe('entry_123456781234');
    expect(entryId.length).toBeGreaterThan(0);
  });

  it('createSessionTreeEntryId retries when the generated id already exists', () => {
    mockedUuidV4
      .mockReturnValueOnce('12345678-1234-1234-1234-1234567890ab')
      .mockReturnValueOnce('abcdefab-cdef-abcd-efab-cdefabcdefab');

    const existingIds = new Set(['entry_123456781234']);

    const entryId = createSessionTreeEntryId(existingIds);

    expect(entryId).toBe('entry_abcdefabcdef');
    expect(existingIds.has(entryId)).toBe(false);
    expect(mockedUuidV4).toHaveBeenCalledTimes(2);
  });

  it('exports the canonical root parent key', () => {
    expect(ROOT_PARENT_KEY).toBe('__root__');
  });

  it('getPathToLeaf returns a root-to-leaf path', () => {
    const entries = [
      createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'),
      createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'),
      createMessageEntry('c', 'b', '2026-04-19T00:02:00.000Z'),
    ];

    expect(getPathToLeaf(entries, 'c').map(entry => entry.id)).toEqual(['a', 'b', 'c']);
  });

  it('getPathToLeaf throws when the leaf entry is missing', () => {
    const entries = [createMessageEntry('a', null, '2026-04-19T00:00:00.000Z')];

    expect(() => getPathToLeaf(entries, 'missing')).toThrow('Entry missing not found');
  });

  it('getPathToLeaf throws when a cycle is detected', () => {
    const entries = [
      createMessageEntry('a', 'c', '2026-04-19T00:00:00.000Z'),
      createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'),
      createMessageEntry('c', 'b', '2026-04-19T00:02:00.000Z'),
    ];

    expect(() => getPathToLeaf(entries, 'c')).toThrow('Cycle detected');
  });

  it('getPathToLeaf throws when a parent entry is missing', () => {
    const entries = [createMessageEntry('c', 'missing-parent', '2026-04-19T00:02:00.000Z')];

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

  it('findCommonAncestorEntryId returns the deepest common ancestor', () => {
    const entries = [
      createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'),
      createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'),
      createMessageEntry('c', 'b', '2026-04-19T00:02:00.000Z'),
      createMessageEntry('d', 'b', '2026-04-19T00:03:00.000Z'),
    ];

    expect(findCommonAncestorEntryId(entries, 'c', 'd')).toBe('b');
  });

  it('groupChildrenByParent collects root entries under ROOT_PARENT_KEY', () => {
    const entries = [
      createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'),
      createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'),
      createMessageEntry('c', null, '2026-04-19T00:02:00.000Z'),
    ];

    expect(groupChildrenByParent(entries)).toEqual({
      [ROOT_PARENT_KEY]: ['a', 'c'],
      a: ['b'],
    });
  });

  it('groupChildrenByParent throws when a real entry id matches ROOT_PARENT_KEY', () => {
    const entries = [
      createMessageEntry(ROOT_PARENT_KEY, null, '2026-04-19T00:00:00.000Z'),
      createMessageEntry('child', ROOT_PARENT_KEY, '2026-04-19T00:01:00.000Z'),
    ];

    expect(() => groupChildrenByParent(entries)).toThrow(
      `Entry id ${ROOT_PARENT_KEY} conflicts with ROOT_PARENT_KEY`,
    );
  });

  it('resolveLatestLabels keeps the later label for the same target', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'),
      {
        id: 'label-1',
        parentId: 'a',
        timestamp: '2026-04-19T00:01:00.000Z',
        type: 'label',
        targetId: 'a',
        label: 'initial',
      } satisfies LabelEntry,
      {
        id: 'label-2',
        parentId: 'a',
        timestamp: '2026-04-19T00:02:00.000Z',
        type: 'label',
        targetId: 'a',
        label: 'latest',
      } satisfies LabelEntry,
    ];

    expect(resolveLatestLabels(entries)).toEqual({
      a: {
        label: 'latest',
        timestamp: '2026-04-19T00:02:00.000Z',
        entryId: 'label-2',
      },
    });
  });

  it('resolveLatestLabels keeps the newest label when an older label appears later in the array', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'),
      {
        id: 'label-new',
        parentId: 'a',
        timestamp: '2026-04-19T00:02:00.000Z',
        type: 'label',
        targetId: 'a',
        label: 'latest',
      } satisfies LabelEntry,
      {
        id: 'label-old',
        parentId: 'a',
        timestamp: '2026-04-19T00:01:00.000Z',
        type: 'label',
        targetId: 'a',
        label: 'stale',
      } satisfies LabelEntry,
    ];

    expect(resolveLatestLabels(entries)).toEqual({
      a: {
        label: 'latest',
        timestamp: '2026-04-19T00:02:00.000Z',
        entryId: 'label-new',
      },
    });
  });

  it('resolveLatestLabels clears the target label when label is undefined', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'),
      {
        id: 'label-1',
        parentId: 'a',
        timestamp: '2026-04-19T00:01:00.000Z',
        type: 'label',
        targetId: 'a',
        label: 'initial',
      } satisfies LabelEntry,
      {
        id: 'label-3',
        parentId: 'a',
        timestamp: '2026-04-19T00:03:00.000Z',
        type: 'label',
        targetId: 'a',
        label: undefined,
      } satisfies LabelEntry,
    ];

    expect(resolveLatestLabels(entries)).toEqual({});
  });

  it('resolveLatestLabels ignores an older clear record that appears later in the array', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'),
      {
        id: 'label-new',
        parentId: 'a',
        timestamp: '2026-04-19T00:03:00.000Z',
        type: 'label',
        targetId: 'a',
        label: 'latest',
      } satisfies LabelEntry,
      {
        id: 'label-old-clear',
        parentId: 'a',
        timestamp: '2026-04-19T00:02:00.000Z',
        type: 'label',
        targetId: 'a',
        label: undefined,
      } satisfies LabelEntry,
    ];

    expect(resolveLatestLabels(entries)).toEqual({
      a: {
        label: 'latest',
        timestamp: '2026-04-19T00:03:00.000Z',
        entryId: 'label-new',
      },
    });
  });

  it('buildEntryIndex returns an id keyed map', () => {
    const entries = [
      createMessageEntry('a', null, '2026-04-19T00:00:00.000Z'),
      createMessageEntry('b', 'a', '2026-04-19T00:01:00.000Z'),
    ];

    expect(buildEntryIndex(entries)).toEqual({
      a: entries[0],
      b: entries[1],
    });
  });

  it('buildEntryIndex stores __proto__ and constructor ids without prototype pollution', () => {
    const entries = [
      createMessageEntry('__proto__', null, '2026-04-19T00:00:00.000Z'),
      createMessageEntry('constructor', null, '2026-04-19T00:01:00.000Z'),
    ];

    const index = buildEntryIndex(entries);

    expect(index.__proto__).toBe(entries[0]);
    expect(index.constructor).toBe(entries[1]);
    expect(Object.getPrototypeOf(index)).toBeNull();
  });

  it('groupChildrenByParent stores __proto__ and constructor parent ids without prototype pollution', () => {
    const entries = [
      createMessageEntry('__proto__', null, '2026-04-19T00:00:00.000Z'),
      createMessageEntry('constructor', '__proto__', '2026-04-19T00:01:00.000Z'),
      createMessageEntry('leaf', 'constructor', '2026-04-19T00:02:00.000Z'),
    ];

    const groups = groupChildrenByParent(entries);

    expect(groups[ROOT_PARENT_KEY]).toEqual(['__proto__']);
    expect(groups.__proto__).toEqual(['constructor']);
    expect(groups.constructor).toEqual(['leaf']);
    expect(Object.getPrototypeOf(groups)).toBeNull();
  });

  it('resolveLatestLabels stores __proto__ and constructor target ids without prototype pollution', () => {
    const entries: SessionTreeEntry[] = [
      createMessageEntry('__proto__', null, '2026-04-19T00:00:00.000Z'),
      createMessageEntry('constructor', null, '2026-04-19T00:00:01.000Z'),
      {
        id: 'label-proto',
        parentId: '__proto__',
        timestamp: '2026-04-19T00:00:02.000Z',
        type: 'label',
        targetId: '__proto__',
        label: 'proto-label',
      } satisfies LabelEntry,
      {
        id: 'label-constructor',
        parentId: 'constructor',
        timestamp: '2026-04-19T00:00:03.000Z',
        type: 'label',
        targetId: 'constructor',
        label: 'constructor-label',
      } satisfies LabelEntry,
    ];

    const labels = resolveLatestLabels(entries);

    expect(labels.__proto__).toEqual({
      label: 'proto-label',
      timestamp: '2026-04-19T00:00:02.000Z',
      entryId: 'label-proto',
    });
    expect(labels.constructor).toEqual({
      label: 'constructor-label',
      timestamp: '2026-04-19T00:00:03.000Z',
      entryId: 'label-constructor',
    });
    expect(Object.getPrototypeOf(labels)).toBeNull();
  });
});
