const { Constants, ForkOptions } = require('librechat-data-provider');

jest.mock('~/models', () => ({
  getConvo: jest.fn(),
  bulkSaveConvos: jest.fn(),
  getMessages: jest.fn(),
  bulkSaveMessages: jest.fn(),
  bulkIncrementTagCounts: jest.fn(),
  getSharedMessages: jest.fn(),
}));

jest.mock('~/server/controllers/ModelController', () => ({
  getModelsConfig: jest.fn().mockResolvedValue({ openAI: ['gpt-test'] }),
}));

jest.mock('~/server/services/Config', () => ({
  getAppConfig: jest.fn().mockResolvedValue({ interfaceConfig: {} }),
}));

let mockIdCounter = 0;
jest.mock('uuid', () => {
  return {
    v4: jest.fn(() => {
      mockIdCounter++;
      return mockIdCounter.toString();
    }),
  };
});

const {
  forkConversation,
  duplicateConversation,
  forkSharedConversation,
  splitAtTargetLevel,
  getAllMessagesUpToParent,
  getMessagesUpToTargetLevel,
  cloneMessagesWithTimestamps,
} = require('./fork');
const {
  bulkIncrementTagCounts,
  getConvo,
  bulkSaveConvos,
  getMessages,
  bulkSaveMessages,
  getSharedMessages,
} = require('~/models');
const { getModelsConfig } = require('~/server/controllers/ModelController');
const { createImportBatchBuilder } = require('./importBatchBuilder');
const BaseClient = require('~/app/clients/BaseClient');

/**
 *
 * @param {TMessage[]} messages - The list of messages to visualize.
 * @param {string | null} parentId - The parent message ID.
 * @param {string} prefix - The prefix to use for each line.
 * @returns
 */
function printMessageTree(messages, parentId = Constants.NO_PARENT, prefix = '') {
  let treeVisual = '';

  const childMessages = messages.filter((msg) => msg.parentMessageId === parentId);
  for (let index = 0; index < childMessages.length; index++) {
    const msg = childMessages[index];
    const isLast = index === childMessages.length - 1;
    const connector = isLast ? '└── ' : '├── ';

    treeVisual += `${prefix}${connector}[${msg.messageId}]: ${
      msg.parentMessageId !== Constants.NO_PARENT ? `Child of ${msg.parentMessageId}` : 'Root'
    }\n`;
    treeVisual += printMessageTree(messages, msg.messageId, prefix + (isLast ? '    ' : '|   '));
  }

  return treeVisual;
}

const mockMessages = [
  {
    messageId: '0',
    parentMessageId: Constants.NO_PARENT,
    text: 'Root message 1',
    createdAt: '2021-01-01',
  },
  {
    messageId: '1',
    parentMessageId: Constants.NO_PARENT,
    text: 'Root message 2',
    createdAt: '2021-01-01',
  },
  { messageId: '2', parentMessageId: '1', text: 'Child of 1', createdAt: '2021-01-02' },
  { messageId: '3', parentMessageId: '1', text: 'Child of 1', createdAt: '2021-01-03' },
  { messageId: '4', parentMessageId: '2', text: 'Child of 2', createdAt: '2021-01-04' },
  { messageId: '5', parentMessageId: '2', text: 'Child of 2', createdAt: '2021-01-05' },
  { messageId: '6', parentMessageId: '3', text: 'Child of 3', createdAt: '2021-01-06' },
  { messageId: '7', parentMessageId: '3', text: 'Child of 3', createdAt: '2021-01-07' },
  { messageId: '8', parentMessageId: '7', text: 'Child of 7', createdAt: '2021-01-07' },
];

const mockConversation = { convoId: 'abc123', title: 'Original Title' };

describe('forkConversation', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockIdCounter = 0;
    getConvo.mockResolvedValue(mockConversation);
    getMessages.mockResolvedValue(mockMessages);
    bulkSaveConvos.mockResolvedValue(null);
    bulkSaveMessages.mockResolvedValue(null);
  });

  test('should fork conversation without branches', async () => {
    const result = await forkConversation({
      originalConvoId: 'abc123',
      targetMessageId: '3',
      requestUserId: 'user1',
      option: ForkOptions.DIRECT_PATH,
    });
    console.debug('forkConversation: direct path\n', printMessageTree(result.messages));

    // Reversed order due to setup in function
    const expectedMessagesTexts = ['Child of 1', 'Root message 2'];
    expect(getMessages).toHaveBeenCalled();
    expect(bulkSaveMessages).toHaveBeenCalledWith(
      expect.arrayContaining(
        expectedMessagesTexts.map((text) => expect.objectContaining({ text })),
      ),
      true,
    );
  });

  test('should fork conversation without branches (deeper)', async () => {
    const result = await forkConversation({
      originalConvoId: 'abc123',
      targetMessageId: '8',
      requestUserId: 'user1',
      option: ForkOptions.DIRECT_PATH,
    });
    console.debug('forkConversation: direct path (deeper)\n', printMessageTree(result.messages));

    const expectedMessagesTexts = ['Child of 7', 'Child of 3', 'Child of 1', 'Root message 2'];
    expect(getMessages).toHaveBeenCalled();
    expect(bulkSaveMessages).toHaveBeenCalledWith(
      expect.arrayContaining(
        expectedMessagesTexts.map((text) => expect.objectContaining({ text })),
      ),
      true,
    );
  });

  test('should fork conversation with branches', async () => {
    const result = await forkConversation({
      originalConvoId: 'abc123',
      targetMessageId: '3',
      requestUserId: 'user1',
      option: ForkOptions.INCLUDE_BRANCHES,
    });

    console.debug('forkConversation: include branches\n', printMessageTree(result.messages));

    const expectedMessagesTexts = ['Root message 2', 'Child of 1', 'Child of 1'];
    expect(getMessages).toHaveBeenCalled();
    expect(bulkSaveMessages).toHaveBeenCalledWith(
      expect.arrayContaining(
        expectedMessagesTexts.map((text) => expect.objectContaining({ text })),
      ),
      true,
    );
  });

  test('should fork conversation up to target level', async () => {
    const result = await forkConversation({
      originalConvoId: 'abc123',
      targetMessageId: '3',
      requestUserId: 'user1',
      option: ForkOptions.TARGET_LEVEL,
    });

    console.debug('forkConversation: target level\n', printMessageTree(result.messages));

    const expectedMessagesTexts = ['Root message 1', 'Root message 2', 'Child of 1', 'Child of 1'];
    expect(getMessages).toHaveBeenCalled();
    expect(bulkSaveMessages).toHaveBeenCalledWith(
      expect.arrayContaining(
        expectedMessagesTexts.map((text) => expect.objectContaining({ text })),
      ),
      true,
    );
  });

  test('should handle errors during message fetching', async () => {
    getMessages.mockRejectedValue(new Error('Failed to fetch messages'));

    await expect(
      forkConversation({
        originalConvoId: 'abc123',
        targetMessageId: '3',
        requestUserId: 'user1',
      }),
    ).rejects.toThrow('Failed to fetch messages');
  });

  test('should increment tag counts when forking conversation with tags', async () => {
    const mockConvoWithTags = {
      ...mockConversation,
      tags: ['bookmark1', 'bookmark2'],
    };
    getConvo.mockResolvedValue(mockConvoWithTags);

    await forkConversation({
      originalConvoId: 'abc123',
      targetMessageId: '3',
      requestUserId: 'user1',
      option: ForkOptions.DIRECT_PATH,
    });

    // Verify that bulkIncrementTagCounts was called with correct tags
    expect(bulkIncrementTagCounts).toHaveBeenCalledWith('user1', ['bookmark1', 'bookmark2']);
  });

  test('should handle conversation without tags when forking', async () => {
    const mockConvoWithoutTags = {
      ...mockConversation,
      // No tags field
    };
    getConvo.mockResolvedValue(mockConvoWithoutTags);

    await forkConversation({
      originalConvoId: 'abc123',
      targetMessageId: '3',
      requestUserId: 'user1',
      option: ForkOptions.DIRECT_PATH,
    });

    // bulkIncrementTagCounts will be called with array containing undefined
    expect(bulkIncrementTagCounts).toHaveBeenCalled();
  });

  test('should handle empty tags array when forking', async () => {
    const mockConvoWithEmptyTags = {
      ...mockConversation,
      tags: [],
    };
    getConvo.mockResolvedValue(mockConvoWithEmptyTags);

    await forkConversation({
      originalConvoId: 'abc123',
      targetMessageId: '3',
      requestUserId: 'user1',
      option: ForkOptions.DIRECT_PATH,
    });

    // bulkIncrementTagCounts will be called with empty array
    expect(bulkIncrementTagCounts).toHaveBeenCalledWith('user1', []);
  });
});

describe('duplicateConversation', () => {
  beforeEach(() => {
    jest.clearAllMocks();
    mockIdCounter = 0;
    getConvo.mockResolvedValue(mockConversation);
    getMessages.mockResolvedValue(mockMessages);
    bulkSaveConvos.mockResolvedValue(null);
    bulkSaveMessages.mockResolvedValue(null);
    bulkIncrementTagCounts.mockResolvedValue(null);
  });

  test('should duplicate conversation and increment tag counts', async () => {
    const mockConvoWithTags = {
      ...mockConversation,
      tags: ['important', 'work', 'project'],
    };
    getConvo.mockResolvedValue(mockConvoWithTags);

    await duplicateConversation({
      userId: 'user1',
      conversationId: 'abc123',
    });

    // Verify that bulkIncrementTagCounts was called with correct tags
    expect(bulkIncrementTagCounts).toHaveBeenCalledWith('user1', ['important', 'work', 'project']);
  });

  test('should duplicate conversation without tags', async () => {
    const mockConvoWithoutTags = {
      ...mockConversation,
      // No tags field
    };
    getConvo.mockResolvedValue(mockConvoWithoutTags);

    await duplicateConversation({
      userId: 'user1',
      conversationId: 'abc123',
    });

    // bulkIncrementTagCounts will be called with array containing undefined
    expect(bulkIncrementTagCounts).toHaveBeenCalled();
  });

  test('should handle empty tags array when duplicating', async () => {
    const mockConvoWithEmptyTags = {
      ...mockConversation,
      tags: [],
    };
    getConvo.mockResolvedValue(mockConvoWithEmptyTags);

    await duplicateConversation({
      userId: 'user1',
      conversationId: 'abc123',
    });

    // bulkIncrementTagCounts will be called with empty array
    expect(bulkIncrementTagCounts).toHaveBeenCalledWith('user1', []);
  });
});

describe('forkSharedConversation', () => {
  const mockSharedMessages = [
    {
      messageId: 'msg_a',
      parentMessageId: Constants.NO_PARENT,
      text: 'Shared root',
      isCreatedByUser: true,
      createdAt: '2021-01-01',
    },
    {
      messageId: 'msg_b',
      parentMessageId: 'msg_a',
      text: 'Shared reply',
      isCreatedByUser: false,
      createdAt: '2021-01-02',
    },
  ];

  const mockShare = {
    shareId: 'share123',
    conversationId: 'convo_anon',
    title: 'Shared Title',
    messages: mockSharedMessages,
  };

  beforeEach(() => {
    jest.clearAllMocks();
    mockIdCounter = 0;
    getSharedMessages.mockResolvedValue(mockShare);
    getConvo.mockResolvedValue(mockConversation);
    getMessages.mockResolvedValue(mockSharedMessages);
    bulkSaveConvos.mockResolvedValue(null);
    bulkSaveMessages.mockResolvedValue(null);
    bulkIncrementTagCounts.mockResolvedValue(null);
  });

  test('should clone shared messages into a conversation owned by the requesting user', async () => {
    const result = await forkSharedConversation({
      shareId: 'share123',
      shareResourceId: 'resource123',
      requestUserId: 'user1',
    });

    expect(getSharedMessages).toHaveBeenCalledWith('share123', 'resource123', {
      snapshotFiles: undefined,
    });

    const savedMessages = bulkSaveMessages.mock.calls[0][0];
    expect(savedMessages).toHaveLength(2);
    const [root, reply] = savedMessages;
    expect(root).toMatchObject({
      text: 'Shared root',
      user: 'user1',
      endpoint: 'openAI',
      parentMessageId: Constants.NO_PARENT,
    });
    expect(reply).toMatchObject({
      text: 'Shared reply',
      user: 'user1',
      parentMessageId: root.messageId,
    });
    expect(root.messageId).not.toBe('msg_a');
    expect(reply.messageId).not.toBe('msg_b');

    const savedConvos = bulkSaveConvos.mock.calls[0][0];
    expect(savedConvos[0]).toMatchObject({
      user: 'user1',
      title: 'Shared Title',
      endpoint: 'openAI',
      model: 'gpt-test',
    });

    expect(getConvo).toHaveBeenCalledWith('user1', savedConvos[0].conversationId);
    expect(result).toMatchObject({ conversation: mockConversation, messages: mockSharedMessages });
  });

  test('should use an available endpoint when the deployment does not expose OpenAI', async () => {
    getModelsConfig.mockResolvedValueOnce({ anthropic: ['claude-test'] });

    await forkSharedConversation({
      shareId: 'share123',
      shareResourceId: 'resource123',
      requestUserId: 'user1',
    });

    const savedConvos = bulkSaveConvos.mock.calls[0][0];
    expect(savedConvos[0]).toMatchObject({ endpoint: 'anthropic', model: 'claude-test' });

    const savedMessages = bulkSaveMessages.mock.calls[0][0];
    expect(savedMessages.every((message) => message.endpoint === 'anthropic')).toBe(true);
  });

  test('should return null when the share is not found', async () => {
    getSharedMessages.mockResolvedValue(null);

    const result = await forkSharedConversation({
      shareId: 'missing',
      requestUserId: 'user1',
    });

    expect(result).toBeNull();
    expect(bulkSaveMessages).not.toHaveBeenCalled();
  });

  test('should return null when the share has no messages', async () => {
    getSharedMessages.mockResolvedValue({ ...mockShare, messages: [] });

    const result = await forkSharedConversation({
      shareId: 'share123',
      requestUserId: 'user1',
    });

    expect(result).toBeNull();
    expect(bulkSaveMessages).not.toHaveBeenCalled();
  });

  test('should normalize orphaned parentMessageId references to NO_PARENT', async () => {
    getSharedMessages.mockResolvedValue({
      ...mockShare,
      messages: [
        {
          messageId: 'msg_orphan',
          parentMessageId: 'msg_deleted',
          text: 'Orphaned message',
          createdAt: '2021-01-01',
        },
      ],
    });

    await forkSharedConversation({
      shareId: 'share123',
      requestUserId: 'user1',
    });

    const savedMessages = bulkSaveMessages.mock.calls[0][0];
    expect(savedMessages[0].parentMessageId).toBe(Constants.NO_PARENT);
  });

  test('should forward snapshotFiles to getSharedMessages so the kill switch is honored', async () => {
    await forkSharedConversation({
      shareId: 'share123',
      shareResourceId: 'resource123',
      requestUserId: 'user1',
      snapshotFiles: false,
    });

    expect(getSharedMessages).toHaveBeenCalledWith('share123', 'resource123', {
      snapshotFiles: false,
    });
  });

  test('should strip anonymized model identifiers from cloned messages', async () => {
    getSharedMessages.mockResolvedValue({
      ...mockShare,
      messages: [
        {
          messageId: 'msg_a',
          parentMessageId: Constants.NO_PARENT,
          text: 'Assistant message',
          model: 'a_anon123',
          createdAt: '2021-01-01',
        },
      ],
    });

    await forkSharedConversation({
      shareId: 'share123',
      requestUserId: 'user1',
    });

    const savedMessages = bulkSaveMessages.mock.calls[0][0];
    expect(savedMessages[0].model).not.toBe('a_anon123');
  });

  test('should strip file_id from cloned files and attachments', async () => {
    getSharedMessages.mockResolvedValue({
      ...mockShare,
      messages: [
        {
          messageId: 'msg_a',
          parentMessageId: Constants.NO_PARENT,
          text: 'Message with files',
          isCreatedByUser: true,
          createdAt: '2021-01-01',
          files: [{ file_id: 'owner-file-1', filepath: '/images/owner/a.png' }],
          attachments: [
            { file_id: 'owner-file-2', toolCallId: 'tool_1', filepath: '/images/owner/b.png' },
          ],
        },
      ],
    });

    await forkSharedConversation({
      shareId: 'share123',
      requestUserId: 'user1',
    });

    const savedMessages = bulkSaveMessages.mock.calls[0][0];
    const [message] = savedMessages;
    expect(message.files[0]).not.toHaveProperty('file_id');
    expect(message.attachments[0]).not.toHaveProperty('file_id');
    // Render-only metadata is preserved
    expect(message.files[0].filepath).toBe('/images/owner/a.png');
    expect(message.attachments[0].toolCallId).toBe('tool_1');
  });

  test('should resolve interfaceConfig from the app config and pass it to the builder', async () => {
    const interfaceConfig = { retentionMode: 'all', retention: { days: 30 } };
    const loadAppConfig = jest.fn().mockResolvedValue({ interfaceConfig });
    const builderFactory = jest.fn((userId, config) => createImportBatchBuilder(userId, config));

    await forkSharedConversation({
      shareId: 'share123',
      requestUserId: 'user1',
      userRole: 'USER',
      userTenantId: 'tenant-viewer',
      loadAppConfig,
      builderFactory,
    });

    expect(loadAppConfig).toHaveBeenCalledWith({
      role: 'USER',
      userId: 'user1',
      tenantId: 'tenant-viewer',
    });
    expect(builderFactory).toHaveBeenCalledWith('user1', interfaceConfig);
  });

  test('should resolve the app config under the requesting user tenant', async () => {
    const { tenantStorage, getTenantId } = require('@librechat/data-schemas');
    let tenantDuringConfigLoad;
    const loadAppConfig = jest.fn(async () => {
      tenantDuringConfigLoad = getTenantId();
      return { interfaceConfig: {} };
    });

    await tenantStorage.run({ tenantId: 'tenant-share-owner' }, () =>
      forkSharedConversation({
        shareId: 'share123',
        requestUserId: 'user1',
        userTenantId: 'tenant-viewer',
        loadAppConfig,
      }),
    );

    expect(tenantDuringConfigLoad).toBe('tenant-viewer');
  });

  test('should clone only the active branch path when targetMessageIndex is provided', async () => {
    getSharedMessages.mockResolvedValue({
      ...mockShare,
      messages: [
        {
          messageId: 'msg_root',
          parentMessageId: Constants.NO_PARENT,
          text: 'Root',
          createdAt: '2021-01-01T00:00:00.000Z',
        },
        {
          messageId: 'msg_branch_a',
          parentMessageId: 'msg_root',
          text: 'Branch A (shared)',
          createdAt: '2021-01-02T00:00:00.000Z',
        },
        {
          messageId: 'msg_branch_b',
          parentMessageId: 'msg_root',
          text: 'Branch B (newer sibling)',
          createdAt: '2021-01-03T00:00:00.000Z',
        },
      ],
    });

    // Index 1 = the "Branch A" tip the viewer had active.
    await forkSharedConversation({
      shareId: 'share123',
      requestUserId: 'user1',
      targetMessageIndex: 1,
    });

    const savedTexts = bulkSaveMessages.mock.calls[0][0].map((message) => message.text);
    expect(savedTexts).toEqual(['Root', 'Branch A (shared)']);
    expect(savedTexts).not.toContain('Branch B (newer sibling)');
  });

  test('should select the correct branch even when siblings share a createdAt', async () => {
    getSharedMessages.mockResolvedValue({
      ...mockShare,
      messages: [
        {
          messageId: 'msg_root',
          parentMessageId: Constants.NO_PARENT,
          text: 'Root',
          createdAt: '2021-01-01T00:00:00.000Z',
        },
        {
          messageId: 'msg_sib_a',
          parentMessageId: 'msg_root',
          text: 'Sibling A',
          createdAt: '2021-01-02T00:00:00.000Z',
        },
        {
          messageId: 'msg_sib_b',
          parentMessageId: 'msg_root',
          text: 'Sibling B (same timestamp)',
          createdAt: '2021-01-02T00:00:00.000Z',
        },
      ],
    });

    // Index 2 unambiguously targets Sibling B despite the shared createdAt.
    await forkSharedConversation({
      shareId: 'share123',
      requestUserId: 'user1',
      targetMessageIndex: 2,
    });

    const savedTexts = bulkSaveMessages.mock.calls[0][0].map((message) => message.text);
    expect(savedTexts).toEqual(['Root', 'Sibling B (same timestamp)']);
    expect(savedTexts).not.toContain('Sibling A');
  });

  test('should fall back to the full set when targetMessageIndex is out of range', async () => {
    await forkSharedConversation({
      shareId: 'share123',
      requestUserId: 'user1',
      targetMessageIndex: 999,
    });

    expect(bulkSaveMessages.mock.calls[0][0]).toHaveLength(mockSharedMessages.length);
  });

  test('should persist under the requesting user tenant, not the share tenant', async () => {
    const { tenantStorage, getTenantId } = require('@librechat/data-schemas');
    let tenantDuringSave;
    bulkSaveConvos.mockImplementation(async () => {
      tenantDuringSave = getTenantId();
    });

    // Simulate the handler running inside the share owner's tenant context
    // (as `canAccessSharedLink` does) and ensure the write switches to the viewer's.
    await tenantStorage.run({ tenantId: 'tenant-share-owner' }, () =>
      forkSharedConversation({
        shareId: 'share123',
        requestUserId: 'user1',
        userTenantId: 'tenant-viewer',
      }),
    );

    expect(tenantDuringSave).toBe('tenant-viewer');
  });
});

const mockMessagesComplex = [
  { messageId: '7', parentMessageId: Constants.NO_PARENT, text: 'Message 7' },
  { messageId: '8', parentMessageId: Constants.NO_PARENT, text: 'Message 8' },
  { messageId: '5', parentMessageId: '7', text: 'Message 5' },
  { messageId: '6', parentMessageId: '7', text: 'Message 6' },
  { messageId: '9', parentMessageId: '8', text: 'Message 9' },
  { messageId: '2', parentMessageId: '5', text: 'Message 2' },
  { messageId: '3', parentMessageId: '5', text: 'Message 3' },
  { messageId: '1', parentMessageId: '6', text: 'Message 1' },
  { messageId: '4', parentMessageId: '6', text: 'Message 4' },
  { messageId: '10', parentMessageId: '3', text: 'Message 10' },
];

describe('getMessagesUpToTargetLevel', () => {
  test('should get all messages up to target level', async () => {
    const result = getMessagesUpToTargetLevel(mockMessagesComplex, '5');
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getMessagesUpToTargetLevel] should get all messages up to target level\n',
      mappedResult,
    );
    console.debug('mockMessages\n', printMessageTree(mockMessagesComplex));
    console.debug('result\n', printMessageTree(result));
    expect(mappedResult).toEqual(['7', '8', '5', '6', '9']);
  });

  test('should get all messages if target is deepest level', async () => {
    const result = getMessagesUpToTargetLevel(mockMessagesComplex, '10');
    expect(result.length).toEqual(mockMessagesComplex.length);
  });

  test('should return target if only message', async () => {
    const result = getMessagesUpToTargetLevel(
      [mockMessagesComplex[mockMessagesComplex.length - 1]],
      '10',
    );
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getMessagesUpToTargetLevel] should return target if only message\n',
      mappedResult,
    );
    console.debug('mockMessages\n', printMessageTree(mockMessages));
    console.debug('result\n', printMessageTree(result));
    expect(mappedResult).toEqual(['10']);
  });

  test('should return empty array if target message ID does not exist', async () => {
    const result = getMessagesUpToTargetLevel(mockMessagesComplex, '123');
    expect(result).toEqual([]);
  });

  test('should return correct messages when target is a root message', async () => {
    const result = getMessagesUpToTargetLevel(mockMessagesComplex, '7');
    const mappedResult = result.map((msg) => msg.messageId);
    expect(mappedResult).toEqual(['7', '8']);
  });

  test('should correctly handle single message with non-matching ID', async () => {
    const singleMessage = [
      { messageId: '30', parentMessageId: Constants.NO_PARENT, text: 'Message 30' },
    ];
    const result = getMessagesUpToTargetLevel(singleMessage, '31');
    expect(result).toEqual([]);
  });

  test('should correctly handle case with circular dependencies', async () => {
    const circularMessages = [
      { messageId: '40', parentMessageId: '42', text: 'Message 40' },
      { messageId: '41', parentMessageId: '40', text: 'Message 41' },
      { messageId: '42', parentMessageId: '41', text: 'Message 42' },
    ];
    const result = getMessagesUpToTargetLevel(circularMessages, '40');
    const mappedResult = result.map((msg) => msg.messageId);
    expect(new Set(mappedResult)).toEqual(new Set(['40', '41', '42']));
  });

  test('should return all messages when all are interconnected and target is deep in hierarchy', async () => {
    const interconnectedMessages = [
      { messageId: '50', parentMessageId: Constants.NO_PARENT, text: 'Root Message' },
      { messageId: '51', parentMessageId: '50', text: 'Child Level 1' },
      { messageId: '52', parentMessageId: '51', text: 'Child Level 2' },
      { messageId: '53', parentMessageId: '52', text: 'Child Level 3' },
    ];
    const result = getMessagesUpToTargetLevel(interconnectedMessages, '53');
    const mappedResult = result.map((msg) => msg.messageId);
    expect(mappedResult).toEqual(['50', '51', '52', '53']);
  });
});

describe('getAllMessagesUpToParent', () => {
  const mockMessages = [
    { messageId: '11', parentMessageId: Constants.NO_PARENT, text: 'Message 11' },
    { messageId: '12', parentMessageId: Constants.NO_PARENT, text: 'Message 12' },
    { messageId: '13', parentMessageId: '11', text: 'Message 13' },
    { messageId: '14', parentMessageId: '12', text: 'Message 14' },
    { messageId: '15', parentMessageId: '13', text: 'Message 15' },
    { messageId: '16', parentMessageId: '13', text: 'Message 16' },
    { messageId: '21', parentMessageId: '13', text: 'Message 21' },
    { messageId: '17', parentMessageId: '14', text: 'Message 17' },
    { messageId: '18', parentMessageId: '16', text: 'Message 18' },
    { messageId: '19', parentMessageId: '18', text: 'Message 19' },
    { messageId: '20', parentMessageId: '19', text: 'Message 20' },
  ];

  test('should handle empty message list', async () => {
    const result = getAllMessagesUpToParent([], '10');
    expect(result).toEqual([]);
  });

  test('should handle target message not found', async () => {
    const result = getAllMessagesUpToParent(mockMessages, 'invalid-id');
    expect(result).toEqual([]);
  });

  test('should handle single level tree (no parents)', async () => {
    const result = getAllMessagesUpToParent(
      [
        { messageId: '11', parentMessageId: Constants.NO_PARENT, text: 'Message 11' },
        { messageId: '12', parentMessageId: Constants.NO_PARENT, text: 'Message 12' },
      ],
      '11',
    );
    const mappedResult = result.map((msg) => msg.messageId);
    expect(mappedResult).toEqual(['11']);
  });

  test('should correctly retrieve messages in a deeply nested structure', async () => {
    const result = getAllMessagesUpToParent(mockMessages, '20');
    const mappedResult = result.map((msg) => msg.messageId);
    expect(mappedResult).toContain('11');
    expect(mappedResult).toContain('13');
    expect(mappedResult).toContain('16');
    expect(mappedResult).toContain('18');
    expect(mappedResult).toContain('19');
    expect(mappedResult).toContain('20');
  });

  test('should return only the target message if it has no parent', async () => {
    const result = getAllMessagesUpToParent(mockMessages, '11');
    const mappedResult = result.map((msg) => msg.messageId);
    expect(mappedResult).toEqual(['11']);
  });

  test('should handle messages without a parent ID defined', async () => {
    const additionalMessages = [
      ...mockMessages,
      { messageId: '22', text: 'Message 22' }, // No parentMessageId field
    ];
    const result = getAllMessagesUpToParent(additionalMessages, '22');
    const mappedResult = result.map((msg) => msg.messageId);
    expect(mappedResult).toEqual(['22']);
  });

  test('should retrieve all messages from the target to the root (including indirect ancestors)', async () => {
    const result = getAllMessagesUpToParent(mockMessages, '18');
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getAllMessagesUpToParent] should retrieve all messages from the target to the root\n',
      mappedResult,
    );
    console.debug('mockMessages\n', printMessageTree(mockMessages));
    console.debug('result\n', printMessageTree(result));
    expect(mappedResult).toEqual(['11', '13', '15', '16', '21', '18']);
  });

  test('should handle circular dependencies gracefully', () => {
    const mockMessages = [
      { messageId: '1', parentMessageId: '2' },
      { messageId: '2', parentMessageId: '3' },
      { messageId: '3', parentMessageId: '1' },
    ];

    const targetMessageId = '1';
    const result = getAllMessagesUpToParent(mockMessages, targetMessageId);

    const uniqueIds = new Set(result.map((msg) => msg.messageId));
    expect(uniqueIds.size).toBe(result.length);
    expect(result.map((msg) => msg.messageId).sort()).toEqual(['1', '2', '3'].sort());
  });

  test('should return target if only message', async () => {
    const result = getAllMessagesUpToParent([mockMessages[mockMessages.length - 1]], '20');
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getAllMessagesUpToParent] should return target if only message\n',
      mappedResult,
    );
    console.debug('mockMessages\n', printMessageTree(mockMessages));
    console.debug('result\n', printMessageTree(result));
    expect(mappedResult).toEqual(['20']);
  });
});

describe('getMessagesForConversation', () => {
  const mockMessages = [
    { messageId: '11', parentMessageId: Constants.NO_PARENT, text: 'Message 11' },
    { messageId: '12', parentMessageId: Constants.NO_PARENT, text: 'Message 12' },
    { messageId: '13', parentMessageId: '11', text: 'Message 13' },
    { messageId: '14', parentMessageId: '12', text: 'Message 14' },
    { messageId: '15', parentMessageId: '13', text: 'Message 15' },
    { messageId: '16', parentMessageId: '13', text: 'Message 16' },
    { messageId: '21', parentMessageId: '13', text: 'Message 21' },
    { messageId: '17', parentMessageId: '14', text: 'Message 17' },
    { messageId: '18', parentMessageId: '16', text: 'Message 18' },
    { messageId: '19', parentMessageId: '18', text: 'Message 19' },
    { messageId: '20', parentMessageId: '19', text: 'Message 20' },
  ];

  test('should provide the direct path to the target without branches', async () => {
    const result = BaseClient.getMessagesForConversation({
      messages: mockMessages,
      parentMessageId: '18',
    });
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getMessagesForConversation] should provide the direct path to the target without branches\n',
      mappedResult,
    );
    console.debug('mockMessages\n', printMessageTree(mockMessages));
    console.debug('result\n', printMessageTree(result));
    expect(new Set(mappedResult)).toEqual(new Set(['11', '13', '16', '18']));
  });

  test('should return target if only message', async () => {
    const result = BaseClient.getMessagesForConversation({
      messages: [mockMessages[mockMessages.length - 1]],
      parentMessageId: '20',
    });
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getMessagesForConversation] should return target if only message\n',
      mappedResult,
    );
    console.debug('mockMessages\n', printMessageTree(mockMessages));
    console.debug('result\n', printMessageTree(result));
    expect(new Set(mappedResult)).toEqual(new Set(['20']));
  });

  test('should break on detecting a circular dependency', async () => {
    const mockMessagesWithCycle = [
      ...mockMessagesComplex,
      { messageId: '100', parentMessageId: '101', text: 'Message 100' },
      { messageId: '101', parentMessageId: '100', text: 'Message 101' }, // introduces circular dependency
    ];

    const result = BaseClient.getMessagesForConversation({
      messages: mockMessagesWithCycle,
      parentMessageId: '100',
    });
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getMessagesForConversation] should break on detecting a circular dependency\n',
      mappedResult,
    );
    expect(mappedResult).toEqual(['101', '100']);
  });

  // Testing with mockMessagesComplex
  test('should correctly find the conversation path including root messages', async () => {
    const result = BaseClient.getMessagesForConversation({
      messages: mockMessagesComplex,
      parentMessageId: '2',
    });
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getMessagesForConversation] should correctly find the conversation path including root messages\n',
      mappedResult,
    );
    expect(new Set(mappedResult)).toEqual(new Set(['7', '5', '2']));
  });

  // Testing summary feature
  test('should stop at summary if option is enabled', async () => {
    const messagesWithSummary = [
      ...mockMessagesComplex,
      { messageId: '11', parentMessageId: '7', text: 'Message 11', summary: 'Summary for 11' },
    ];

    const result = BaseClient.getMessagesForConversation({
      messages: messagesWithSummary,
      parentMessageId: '11',
      summary: true,
    });
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getMessagesForConversation] should stop at summary if option is enabled\n',
      mappedResult,
    );
    expect(mappedResult).toEqual(['11']); // Should include only the summarizing message
  });

  // Testing no parent condition
  test('should return only the root message if no parent exists', async () => {
    const result = BaseClient.getMessagesForConversation({
      messages: mockMessagesComplex,
      parentMessageId: '8',
    });
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      '[getMessagesForConversation] should return only the root message if no parent exists\n',
      mappedResult,
    );
    expect(mappedResult).toEqual(['8']); // The message with no parent in the thread
  });
});

describe('splitAtTargetLevel', () => {
  /* const mockMessagesComplex = [
    { messageId: '7', parentMessageId: Constants.NO_PARENT, text: 'Message 7' },
    { messageId: '8', parentMessageId: Constants.NO_PARENT, text: 'Message 8' },
    { messageId: '5', parentMessageId: '7', text: 'Message 5' },
    { messageId: '6', parentMessageId: '7', text: 'Message 6' },
    { messageId: '9', parentMessageId: '8', text: 'Message 9' },
    { messageId: '2', parentMessageId: '5', text: 'Message 2' },
    { messageId: '3', parentMessageId: '5', text: 'Message 3' },
    { messageId: '1', parentMessageId: '6', text: 'Message 1' },
    { messageId: '4', parentMessageId: '6', text: 'Message 4' },
    { messageId: '10', parentMessageId: '3', text: 'Message 10' },
  ];

     mockMessages
    ├── [7]: Root
    |   ├── [5]: Child of 7
    |   |   ├── [2]: Child of 5
    |   |   └── [3]: Child of 5
    |   |       └── [10]: Child of 3
    |   └── [6]: Child of 7
    |       ├── [1]: Child of 6
    |       └── [4]: Child of 6
    └── [8]: Root
        └── [9]: Child of 8
  */
  test('should include target message level and all descendants (1/2)', () => {
    console.debug('splitAtTargetLevel: mockMessages\n', printMessageTree(mockMessagesComplex));
    const result = splitAtTargetLevel(mockMessagesComplex, '2');
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      'splitAtTargetLevel: include target message level and all descendants (1/2)\n',
      printMessageTree(result),
    );
    expect(mappedResult).toEqual(['2', '3', '1', '4', '10']);
  });

  test('should include target message level and all descendants (2/2)', () => {
    console.debug('splitAtTargetLevel: mockMessages\n', printMessageTree(mockMessagesComplex));
    const result = splitAtTargetLevel(mockMessagesComplex, '5');
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      'splitAtTargetLevel: include target message level and all descendants (2/2)\n',
      printMessageTree(result),
    );
    expect(mappedResult).toEqual(['5', '6', '9', '2', '3', '1', '4', '10']);
  });

  test('should handle when target message is root', () => {
    const result = splitAtTargetLevel(mockMessagesComplex, '7');
    console.debug('splitAtTargetLevel: target level is root message\n', printMessageTree(result));
    expect(result.length).toBe(mockMessagesComplex.length);
  });

  test('should handle when target message is deepest, lonely child', () => {
    const result = splitAtTargetLevel(mockMessagesComplex, '10');
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      'splitAtTargetLevel: target message is deepest, lonely child\n',
      printMessageTree(result),
    );
    expect(mappedResult).toEqual(['10']);
  });

  test('should handle when target level is last with many neighbors', () => {
    const mockMessages = [
      ...mockMessagesComplex,
      { messageId: '11', parentMessageId: '10', text: 'Message 11' },
      { messageId: '12', parentMessageId: '10', text: 'Message 12' },
      { messageId: '13', parentMessageId: '10', text: 'Message 13' },
      { messageId: '14', parentMessageId: '10', text: 'Message 14' },
      { messageId: '15', parentMessageId: '4', text: 'Message 15' },
      { messageId: '16', parentMessageId: '15', text: 'Message 15' },
    ];
    const result = splitAtTargetLevel(mockMessages, '11');
    const mappedResult = result.map((msg) => msg.messageId);
    console.debug(
      'splitAtTargetLevel: should handle when target level is last with many neighbors\n',
      printMessageTree(result),
    );
    expect(mappedResult).toEqual(['11', '12', '13', '14', '16']);
  });

  test('should handle non-existent target message', () => {
    // Non-existent message ID
    const result = splitAtTargetLevel(mockMessagesComplex, '99');
    expect(result.length).toBe(0);
  });
});

describe('cloneMessagesWithTimestamps', () => {
  test('should maintain proper timestamp order between parent and child messages', () => {
    // Create messages with out-of-order timestamps
    const messagesToClone = [
      {
        messageId: 'parent',
        parentMessageId: Constants.NO_PARENT,
        text: 'Parent Message',
        createdAt: '2023-01-01T00:02:00Z', // Later timestamp
      },
      {
        messageId: 'child1',
        parentMessageId: 'parent',
        text: 'Child Message 1',
        createdAt: '2023-01-01T00:01:00Z', // Earlier timestamp
      },
      {
        messageId: 'child2',
        parentMessageId: 'parent',
        text: 'Child Message 2',
        createdAt: '2023-01-01T00:03:00Z',
      },
    ];

    const importBatchBuilder = createImportBatchBuilder('testUser');
    importBatchBuilder.startConversation();

    cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);

    // Verify timestamps are properly ordered
    const clonedMessages = importBatchBuilder.messages;
    expect(clonedMessages.length).toBe(3);

    // Find cloned messages (they'll have new IDs)
    const parent = clonedMessages.find((msg) => msg.parentMessageId === Constants.NO_PARENT);
    const children = clonedMessages.filter((msg) => msg.parentMessageId === parent.messageId);

    // Verify parent timestamp is earlier than all children
    children.forEach((child) => {
      expect(new Date(child.createdAt).getTime()).toBeGreaterThan(
        new Date(parent.createdAt).getTime(),
      );
    });
  });

  test('should handle multi-level message chains', () => {
    const messagesToClone = [
      {
        messageId: 'root',
        parentMessageId: Constants.NO_PARENT,
        text: 'Root',
        createdAt: '2023-01-01T00:03:00Z', // Latest
      },
      {
        messageId: 'parent',
        parentMessageId: 'root',
        text: 'Parent',
        createdAt: '2023-01-01T00:01:00Z', // Earliest
      },
      {
        messageId: 'child',
        parentMessageId: 'parent',
        text: 'Child',
        createdAt: '2023-01-01T00:02:00Z', // Middle
      },
    ];

    const importBatchBuilder = createImportBatchBuilder('testUser');
    importBatchBuilder.startConversation();

    cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);

    const clonedMessages = importBatchBuilder.messages;
    expect(clonedMessages.length).toBe(3);

    // Verify the chain of timestamps
    const root = clonedMessages.find((msg) => msg.parentMessageId === Constants.NO_PARENT);
    const parent = clonedMessages.find((msg) => msg.parentMessageId === root.messageId);
    const child = clonedMessages.find((msg) => msg.parentMessageId === parent.messageId);

    expect(new Date(parent.createdAt).getTime()).toBeGreaterThan(
      new Date(root.createdAt).getTime(),
    );
    expect(new Date(child.createdAt).getTime()).toBeGreaterThan(
      new Date(parent.createdAt).getTime(),
    );
  });

  test('should handle messages with identical timestamps', () => {
    const sameTimestamp = '2023-01-01T00:00:00Z';
    const messagesToClone = [
      {
        messageId: 'parent',
        parentMessageId: Constants.NO_PARENT,
        text: 'Parent',
        createdAt: sameTimestamp,
      },
      {
        messageId: 'child',
        parentMessageId: 'parent',
        text: 'Child',
        createdAt: sameTimestamp,
      },
    ];

    const importBatchBuilder = createImportBatchBuilder('testUser');
    importBatchBuilder.startConversation();

    cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);

    const clonedMessages = importBatchBuilder.messages;
    const parent = clonedMessages.find((msg) => msg.parentMessageId === Constants.NO_PARENT);
    const child = clonedMessages.find((msg) => msg.parentMessageId === parent.messageId);

    expect(new Date(child.createdAt).getTime()).toBeGreaterThan(
      new Date(parent.createdAt).getTime(),
    );
  });

  test('should preserve original timestamps when already properly ordered', () => {
    const messagesToClone = [
      {
        messageId: 'parent',
        parentMessageId: Constants.NO_PARENT,
        text: 'Parent',
        createdAt: '2023-01-01T00:00:00Z',
      },
      {
        messageId: 'child',
        parentMessageId: 'parent',
        text: 'Child',
        createdAt: '2023-01-01T00:01:00Z',
      },
    ];

    const importBatchBuilder = createImportBatchBuilder('testUser');
    importBatchBuilder.startConversation();

    cloneMessagesWithTimestamps(messagesToClone, importBatchBuilder);

    const clonedMessages = importBatchBuilder.messages;
    const parent = clonedMessages.find((msg) => msg.parentMessageId === Constants.NO_PARENT);
    const child = clonedMessages.find((msg) => msg.parentMessageId === parent.messageId);

    expect(parent.createdAt).toEqual(new Date(messagesToClone[0].createdAt));
    expect(child.createdAt).toEqual(new Date(messagesToClone[1].createdAt));
  });

  test('should handle complex multi-branch scenario with out-of-order timestamps', () => {
    const complexMessages = [
      // Branch 1: Root -> A -> (B, C) -> D
      {
        messageId: 'root1',
        parentMessageId: Constants.NO_PARENT,
        text: 'Root 1',
        createdAt: '2023-01-01T00:05:00Z', // Root is later than children
      },
      {
        messageId: 'A1',
        parentMessageId: 'root1',
        text: 'A1',
        createdAt: '2023-01-01T00:02:00Z',
      },
      {
        messageId: 'B1',
        parentMessageId: 'A1',
        text: 'B1',
        createdAt: '2023-01-01T00:01:00Z', // Earlier than parent
      },
      {
        messageId: 'C1',
        parentMessageId: 'A1',
        text: 'C1',
        createdAt: '2023-01-01T00:03:00Z',
      },
      {
        messageId: 'D1',
        parentMessageId: 'B1',
        text: 'D1',
        createdAt: '2023-01-01T00:04:00Z',
      },

      // Branch 2: Root -> (X, Y, Z) where Z has children but X is latest
      {
        messageId: 'root2',
        parentMessageId: Constants.NO_PARENT,
        text: 'Root 2',
        createdAt: '2023-01-01T00:06:00Z',
      },
      {
        messageId: 'X2',
        parentMessageId: 'root2',
        text: 'X2',
        createdAt: '2023-01-01T00:09:00Z', // Latest of siblings
      },
      {
        messageId: 'Y2',
        parentMessageId: 'root2',
        text: 'Y2',
        createdAt: '2023-01-01T00:07:00Z',
      },
      {
        messageId: 'Z2',
        parentMessageId: 'root2',
        text: 'Z2',
        createdAt: '2023-01-01T00:08:00Z',
      },
      {
        messageId: 'Z2Child',
        parentMessageId: 'Z2',
        text: 'Z2 Child',
        createdAt: '2023-01-01T00:04:00Z', // Earlier than all parents
      },

      // Branch 3: Root with alternating early/late timestamps
      {
        messageId: 'root3',
        parentMessageId: Constants.NO_PARENT,
        text: 'Root 3',
        createdAt: '2023-01-01T00:15:00Z', // Latest of all
      },
      {
        messageId: 'E3',
        parentMessageId: 'root3',
        text: 'E3',
        createdAt: '2023-01-01T00:10:00Z',
      },
      {
        messageId: 'F3',
        parentMessageId: 'E3',
        text: 'F3',
        createdAt: '2023-01-01T00:14:00Z', // Later than parent
      },
      {
        messageId: 'G3',
        parentMessageId: 'F3',
        text: 'G3',
        createdAt: '2023-01-01T00:11:00Z', // Earlier than parent
      },
      {
        messageId: 'H3',
        parentMessageId: 'G3',
        text: 'H3',
        createdAt: '2023-01-01T00:13:00Z',
      },
    ];

    const importBatchBuilder = createImportBatchBuilder('testUser');
    importBatchBuilder.startConversation();

    cloneMessagesWithTimestamps(complexMessages, importBatchBuilder);

    const clonedMessages = importBatchBuilder.messages;
    console.debug(
      'Complex multi-branch scenario\nOriginal messages:\n',
      printMessageTree(complexMessages),
    );
    console.debug('Cloned messages:\n', printMessageTree(clonedMessages));

    // Helper function to verify timestamp order
    const verifyTimestampOrder = (parentId, messages) => {
      const parent = messages.find((msg) => msg.messageId === parentId);
      const children = messages.filter((msg) => msg.parentMessageId === parentId);

      children.forEach((child) => {
        const parentTime = new Date(parent.createdAt).getTime();
        const childTime = new Date(child.createdAt).getTime();
        expect(childTime).toBeGreaterThan(parentTime);
        // Recursively verify child's children
        verifyTimestampOrder(child.messageId, messages);
      });
    };

    // Verify each branch
    const roots = clonedMessages.filter((msg) => msg.parentMessageId === Constants.NO_PARENT);
    roots.forEach((root) => verifyTimestampOrder(root.messageId, clonedMessages));

    // Additional specific checks
    const getMessageByText = (text) => clonedMessages.find((msg) => msg.text === text);

    // Branch 1 checks
    const root1 = getMessageByText('Root 1');
    const b1 = getMessageByText('B1');
    const d1 = getMessageByText('D1');
    expect(new Date(b1.createdAt).getTime()).toBeGreaterThan(new Date(root1.createdAt).getTime());
    expect(new Date(d1.createdAt).getTime()).toBeGreaterThan(new Date(b1.createdAt).getTime());

    // Branch 2 checks
    const root2 = getMessageByText('Root 2');
    const x2 = getMessageByText('X2');
    const z2Child = getMessageByText('Z2 Child');
    const z2 = getMessageByText('Z2');
    expect(new Date(x2.createdAt).getTime()).toBeGreaterThan(new Date(root2.createdAt).getTime());
    expect(new Date(z2Child.createdAt).getTime()).toBeGreaterThan(new Date(z2.createdAt).getTime());

    // Branch 3 checks
    const f3 = getMessageByText('F3');
    const g3 = getMessageByText('G3');
    expect(new Date(g3.createdAt).getTime()).toBeGreaterThan(new Date(f3.createdAt).getTime());

    // Verify all messages are present
    expect(clonedMessages.length).toBe(complexMessages.length);
  });
});
