import type {BlockMessageForwarded, Message} from '@modules/chats/interfaces';
import {BlockType, MessageStatus} from '@modules/chats/interfaces';

const ApiErrors = {
  NETWORK_ERROR: 'NETWORK_ERROR',
};

const mockGetPendingMessages = jest.fn();
const mockPostMessage = jest.fn();
const mockForwardMessage = jest.fn();
const mockMarkConversationAsRead = jest.fn();
const mockSaveNewMessage = jest.fn();
const mockSetMessage = jest.fn();

jest.mock('@modules/chats/db', () => ({
  getPendingMessages: (...args: unknown[]) => mockGetPendingMessages(...args),
}));

jest.mock('@modules/chats/services', () => ({
  postMessage: (...args: unknown[]) => mockPostMessage(...args),
  forwardMessage: (...args: unknown[]) => mockForwardMessage(...args),
}));

jest.mock('@modules/chats/instances/FileHandler', () => ({
  fileHandler: {
    updateAsset: jest.fn(),
  },
}));

jest.mock(
  '@modules/chats/instances/updaters/OrchestratorUpdaterService',
  () => ({
    OrchestratorUpdaterService: {
      processMessage: (...args: unknown[]) => mockSaveNewMessage(...args),
    },
  }),
);

jest.mock('@modules/chats/instances/PendingAssets', () => ({
  PendingAssets: {
    setMessage: (...args: unknown[]) => mockSetMessage(...args),
  },
}));

jest.mock('@modules/chats/instances/ConversationMarker', () => ({
  conversationMarker: {
    markConversationAsRead: (...args: unknown[]) =>
      mockMarkConversationAsRead(...args),
  },
}));

jest.mock('@modules/chats/utils/files', () => ({
  uploadAudioFile: jest.fn(),
}));

jest.mock('@config/api', () => ({
  ApiErrors: {
    NETWORK_ERROR: 'NETWORK_ERROR',
  },
}));

jest.mock('@modules/chats/utils/mappers/assetMapper', () => ({
  mapMediaToService: jest.fn(),
}));

type MessagesQueueModule = typeof import('../MessagesQueue');
let messagesQueue: MessagesQueueModule['messagesQueue'];

const loadModule = () => {
  jest.resetModules();
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const module = require('../MessagesQueue') as MessagesQueueModule;
  messagesQueue = module.messagesQueue;
  return module;
};

const createMessage = (overrides: Partial<Message> = {}): Message => ({
  text: 'hello',
  channel: 'channel-1',
  ts: 'ts-1',
  user: 'user-1',
  client_msg_id: 'client-1',
  username: 'User',
  version: 1,
  height: 1,
  highlighted_spans: [],
  formatted_text: 'hello',
  ...overrides,
});

describe('MessagesQueue', () => {
  beforeEach(async () => {
    mockGetPendingMessages.mockReset();
    mockPostMessage.mockReset();
    mockForwardMessage.mockReset();
    mockMarkConversationAsRead.mockReset();
    mockSaveNewMessage.mockReset();
    mockSetMessage.mockReset();
    mockGetPendingMessages.mockResolvedValue([]);
    loadModule();
    await messagesQueue.sendPendingMessages();
  });

  it('sends messages and updates status to sent', async () => {
    mockPostMessage.mockResolvedValue({
      data: {ts: 'server-ts'},
    });

    messagesQueue.addMessage(createMessage());

    await mockPostMessage.mock.results[0]?.value;

    expect(mockPostMessage).toHaveBeenCalledWith(
      expect.objectContaining({
        channel: 'channel-1',
        text: 'hello',
        idempotencyKey: 'client-1',
        messageId: 'client-1',
      }),
    );
    expect(mockMarkConversationAsRead).toHaveBeenCalledWith(
      'channel-1',
      'server-ts',
    );
    expect(mockSaveNewMessage).toHaveBeenCalledWith({
      ts: 'server-ts',
      status: MessageStatus.Sent,
    });
  });

  it('marks message as error on unknown failure', async () => {
    const savePromise = new Promise<void>(resolve => {
      mockSaveNewMessage.mockImplementation(() => resolve());
    });
    mockPostMessage.mockImplementation(() => Promise.reject(new Error('test')));

    const message = createMessage();
    messagesQueue.addMessage(message);

    await savePromise;

    expect(mockSaveNewMessage).toHaveBeenCalledWith({
      ...message,
      status: MessageStatus.Error,
    });
  });

  it('does not update status on network error', async () => {
    const postPromise = Promise.reject(new Error(ApiErrors.NETWORK_ERROR));
    mockPostMessage.mockReturnValue(postPromise);

    messagesQueue.addMessage(createMessage());

    await postPromise.catch(() => undefined);

    expect(mockSaveNewMessage).not.toHaveBeenCalled();
  });

  it('forwards messages with forward blocks', async () => {
    mockForwardMessage.mockResolvedValue({
      data: [{ts: 'server-ts'}],
    });

    const forwardBlock: BlockMessageForwarded = {
      type: BlockType.MessageForwarded,
      original_channel_ts: 'origin-channel',
      original_message_ts: 'origin-message',
    };

    const message = createMessage({
      ts: 'local-message-ts',
      blocks: [forwardBlock],
    });

    messagesQueue.addMessage(message);

    await mockForwardMessage.mock.results[0]?.value;

    expect(mockForwardMessage).toHaveBeenCalledWith({
      channel: 'origin-channel',
      messageId: 'origin-message',
      destinationChannelIds: ['channel-1'],
      destinationUserIds: [],
      idempotencyKey: 'client-1',
      text: 'hello',
      attachments: undefined,
    });
    expect(mockSaveNewMessage).toHaveBeenCalledWith({
      ts: 'local-message-ts',
      client_msg_id: 'client-1',
      status: MessageStatus.Sent,
    });
  });

  it('sends pending messages from the database', async () => {
    const pending1 = createMessage({client_msg_id: 'client-1', ts: 'ts-1'});
    const pending2 = createMessage({client_msg_id: 'client-2', ts: 'ts-2'});

    mockGetPendingMessages.mockResolvedValue([pending1, pending2]);
    mockSetMessage.mockImplementation((message: Message) => message);
    mockPostMessage.mockResolvedValue({data: {ts: 'server-ts'}});

    await messagesQueue.sendPendingMessages();

    await Promise.all(mockPostMessage.mock.results.map(result => result.value));

    expect(mockGetPendingMessages).toHaveBeenCalled();
    expect(mockSetMessage).toHaveBeenCalledTimes(2);
    expect(mockPostMessage).toHaveBeenCalledTimes(2);
  });
});
