const mockSearchMessages = jest.fn();
const mockMapSearchMessageMatchesFromServer = jest.fn();

const mockGetServices = () => ({
  searchMessages: mockSearchMessages,
});

const mockGetUtils = () => ({
  mapSearchMessageMatchesFromServer: mockMapSearchMessageMatchesFromServer,
});

jest.mock('@modules/chats/services', () => mockGetServices());
jest.mock('@modules/chats/utils', () => mockGetUtils());

const createMessagesSearchStore = () => {
  jest.resetModules();
  jest.mock('@modules/chats/services', () => mockGetServices());
  jest.mock('@modules/chats/utils', () => mockGetUtils());
  return require('../messagesSearch');
};

describe('messagesSearch store', () => {
  const CHANNEL_ID = 'channel-1';
  const SHORT_QUERY = 'a';
  const QUERY = 'hello';
  const MATCHES = [
    {ts: '1001', viewPosition: 0.3},
    {ts: '1002', viewPosition: 0.7},
  ];

  beforeEach(() => {
    mockSearchMessages.mockReset();
    mockMapSearchMessageMatchesFromServer.mockReset();
  });

  it('sets initial state when query is too short', async () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    await useMessagesSearch.getState().initSearch(CHANNEL_ID, SHORT_QUERY);

    const state = useMessagesSearch.getState();

    expect(mockSearchMessages).not.toHaveBeenCalled();
    expect(state.isSearchVisible).toBe(true);
    expect(state.channelId).toBe(CHANNEL_ID);
    expect(state.query).toBe('');
    expect(state.matchesCountLabel).toBe('0');
    expect(state.matches).toEqual([]);
    expect(state.currentSelectedMatch).toBeNull();
  });

  it('initializes search results and selects the first match', async () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    mockSearchMessages.mockResolvedValue({
      data: [{item: {ts: '1001'}}],
      nextCursor: null,
    });
    mockMapSearchMessageMatchesFromServer.mockReturnValue(MATCHES);

    await useMessagesSearch.getState().initSearch(CHANNEL_ID, QUERY);

    const state = useMessagesSearch.getState();

    expect(mockSearchMessages).toHaveBeenCalledWith(
      expect.objectContaining({
        channelId: CHANNEL_ID,
        query: QUERY,
        signal: expect.any(Object),
      }),
    );
    expect(state.isLoading).toBe(false);
    expect(state.matches).toEqual(MATCHES);
    expect(state.currentSelectedMatch).toEqual({
      index: 0,
      ts: MATCHES[0].ts,
      viewPosition: MATCHES[0].viewPosition,
    });
    expect(state.matchesCountLabel).toBe('2');
    expect(state.matchedMessagesTsIndexMap.get('1002')).toBe(1);
  });

  it('moves to the next and previous matches', () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    useMessagesSearch.setState({
      matches: MATCHES,
      currentSelectedMatch: {
        index: 0,
        ts: MATCHES[0].ts,
        viewPosition: MATCHES[0].viewPosition,
      },
    });

    const next = useMessagesSearch.getState().nextMatch();
    const prev = useMessagesSearch.getState().prevMatch();

    expect(next).toEqual({
      index: 1,
      ts: MATCHES[1].ts,
      viewPosition: MATCHES[1].viewPosition,
    });
    expect(prev).toEqual({
      index: 0,
      ts: MATCHES[0].ts,
      viewPosition: MATCHES[0].viewPosition,
    });
  });

  it('currentSelectedMatch uses viewPosition from the matched item', async () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    const matchesWithPositions = [
      {ts: '2001', viewPosition: 0.1},
      {ts: '2002', viewPosition: 0.9},
    ];

    mockSearchMessages.mockResolvedValue({
      data: [{item: {ts: '2001'}}, {item: {ts: '2002'}}],
      nextCursor: null,
    });
    mockMapSearchMessageMatchesFromServer.mockReturnValue(matchesWithPositions);

    await useMessagesSearch.getState().initSearch(CHANNEL_ID, QUERY);

    const {currentSelectedMatch} = useMessagesSearch.getState();
    expect(currentSelectedMatch?.viewPosition).toBe(0.1);
  });

  it('setCurrentSelectedMatch returns correct viewPosition for the selected index', () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    useMessagesSearch.setState({matches: MATCHES});

    const result = useMessagesSearch.getState().setCurrentSelectedMatch(1);

    expect(result).toEqual({
      index: 1,
      ts: MATCHES[1].ts,
      viewPosition: MATCHES[1].viewPosition,
    });
    expect(
      useMessagesSearch.getState().currentSelectedMatch?.viewPosition,
    ).toBe(MATCHES[1].viewPosition);
  });

  it('nextMatch carries viewPosition from the match item', () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    const matches = [
      {ts: 'a', viewPosition: 0},
      {ts: 'b', viewPosition: 0.5},
      {ts: 'c', viewPosition: 1},
    ];

    useMessagesSearch.setState({
      matches,
      currentSelectedMatch: {index: 0, ts: 'a', viewPosition: 0},
    });

    const result = useMessagesSearch.getState().nextMatch();
    expect(result?.viewPosition).toBe(0.5);

    const result2 = useMessagesSearch.getState().nextMatch();
    expect(result2?.viewPosition).toBe(1);
  });

  it('prevMatch carries viewPosition from the match item', () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    const matches = [
      {ts: 'a', viewPosition: 0.2},
      {ts: 'b', viewPosition: 0.8},
    ];

    useMessagesSearch.setState({
      matches,
      currentSelectedMatch: {index: 1, ts: 'b', viewPosition: 0.8},
    });

    const result = useMessagesSearch.getState().prevMatch();
    expect(result?.viewPosition).toBe(0.2);
  });

  it('uses viewPosition 0.5 as fallback when first match has no viewPosition', async () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    mockSearchMessages.mockResolvedValue({
      data: [{item: {ts: '3001'}}],
      nextCursor: null,
    });
    mockMapSearchMessageMatchesFromServer.mockReturnValue([{ts: '3001'}]);

    await useMessagesSearch.getState().initSearch(CHANNEL_ID, QUERY);

    expect(
      useMessagesSearch.getState().currentSelectedMatch?.viewPosition,
    ).toBe(0.5);
  });

  it('loads next cursor and appends new matches', async () => {
    const {useMessagesSearch} = createMessagesSearchStore();

    useMessagesSearch.setState({
      channelId: CHANNEL_ID,
      query: QUERY,
      nextCursor: 'cursor-1',
      matches: MATCHES,
      matchedMessagesTsIndexMap: new Map(
        MATCHES.map((match, index) => [match.ts, index]),
      ),
    });

    mockSearchMessages.mockResolvedValue({
      data: [{item: {ts: '1003'}}],
      nextCursor: 'cursor-2',
    });
    mockMapSearchMessageMatchesFromServer.mockReturnValue([
      {ts: '1002'},
      {ts: '1003'},
    ]);

    await useMessagesSearch.getState().loadNextCursor();

    const state = useMessagesSearch.getState();

    expect(mockSearchMessages).toHaveBeenCalledWith(
      expect.objectContaining({
        channelId: CHANNEL_ID,
        query: QUERY,
        cursor: 'cursor-1',
        signal: expect.any(Object),
      }),
    );
    expect(state.matches).toEqual([
      {ts: '1001', viewPosition: 0.3},
      {ts: '1002', viewPosition: 0.7},
      {ts: '1003'},
    ]);
    expect(state.matchedMessagesTsIndexMap.get('1003')).toBe(2);
    expect(state.nextCursor).toBe('cursor-2');
    expect(state.isLoading).toBe(false);
  });
});
