import type {AppStateStatus} from 'react-native';

const mockAddEventListener = jest.fn();
const mockRemoveListener = jest.fn();
const mockProcessEvent = jest.fn();
const mockMarkConversationAsRead = jest.fn();
const mockCreateChannelDebounce = jest.fn();
const mockGetUlid = jest.fn();
const mockGetGroupsState = jest.fn();
const mockSentryError = jest.fn();

const mockAppState = {
  currentState: 'active' as AppStateStatus,
  addEventListener: (...args: unknown[]) => mockAddEventListener(...args),
};

let appStateChangeHandler: ((state: AppStateStatus) => void) | undefined;
let debouncedConversationMark: jest.Mock & {
  cancel: jest.Mock;
  flush: jest.Mock;
};
let debouncedMessageReceived: jest.Mock & {
  cancel: jest.Mock;
  flush: jest.Mock;
};

jest.mock('react-native', () => ({
  AppState: mockAppState,
}));

jest.mock('@config/synchroniser', () => ({
  localEventsHandler: {
    processEvent: (...args: unknown[]) => mockProcessEvent(...args),
  },
  LocalEventTypes: {
    CHATS2_READ_CHANNEL: 'CHATS2_READ_CHANNEL',
  },
}));

jest.mock('@shared/utils/texts', () => ({
  getUlid: (...args: unknown[]) => mockGetUlid(...args),
}));

jest.mock('react-native-ulid-jsi', () => ({
  ulid: (...args: unknown[]) => mockGetUlid(...args),
}));

jest.mock('@shared/utils/logs', () => ({
  sentryLogs: {
    error: (...args: unknown[]) => mockSentryError(...args),
  },
}));

jest.mock('@modules/chats/services', () => ({
  postConversationMarkReceived: jest.fn(),
}));

jest.mock('@modules/chats/utils/debounce', () => ({
  createChannelDebounce: (...args: unknown[]) =>
    mockCreateChannelDebounce(...args),
}));

jest.mock('@modules/chats/store/zustand/useGroups', () => ({
  useGroups: {
    getState: () => mockGetGroupsState(),
  },
}));

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

type ConversationMarkerModule = typeof import('../ConversationMarker');
let conversationMarker: ConversationMarkerModule['conversationMarker'];

const loadModule = () => {
  jest.resetModules();
  debouncedConversationMark = Object.assign(jest.fn(), {
    cancel: jest.fn(),
    flush: jest.fn(),
  });
  debouncedMessageReceived = Object.assign(jest.fn(), {
    cancel: jest.fn(),
    flush: jest.fn(),
  });
  let debounceCalls = 0;
  mockCreateChannelDebounce.mockImplementation(
    (fn: (...args: unknown[]) => void) => {
      debounceCalls += 1;
      const wrapper =
        debounceCalls === 1
          ? debouncedConversationMark
          : debouncedMessageReceived;
      wrapper.mockImplementation((channel: string, request: unknown) =>
        (fn as (channel: string, request: unknown) => unknown)(
          channel,
          request,
        ),
      );
      return wrapper;
    },
  );

  mockAddEventListener.mockImplementation((_event, handler) => {
    appStateChangeHandler = handler as (state: AppStateStatus) => void;
    return {remove: mockRemoveListener};
  });

  // eslint-disable-next-line @typescript-eslint/no-var-requires
  const module = require('../ConversationMarker') as ConversationMarkerModule;
  conversationMarker = module.conversationMarker;
  return module;
};

describe('ConversationMarker', () => {
  beforeEach(() => {
    mockAddEventListener.mockReset();
    mockRemoveListener.mockReset();
    mockProcessEvent.mockReset();
    mockMarkConversationAsRead.mockReset();
    mockCreateChannelDebounce.mockReset();
    mockGetUlid.mockReset();
    mockGetGroupsState.mockReset();
    mockSentryError.mockReset();
    mockProcessEvent.mockResolvedValue(undefined);
    mockMarkConversationAsRead.mockResolvedValue(undefined);
    mockAppState.currentState = 'active';
    appStateChangeHandler = undefined;
    loadModule();
  });

  it('marks conversation as read using provided timestamp', async () => {
    mockGetUlid.mockReturnValue('ulid-1');

    conversationMarker.markConversationAsRead('channel-1', 'ts-1');
    await Promise.all([
      mockMarkConversationAsRead.mock.results[0]?.value,
      mockProcessEvent.mock.results[0]?.value,
    ]);

    expect(mockMarkConversationAsRead).toHaveBeenCalledWith(
      'channel-1',
      'ts-1',
    );
    expect(mockProcessEvent).toHaveBeenCalledWith({
      data: {
        channel: 'channel-1',
        idempotencyKey: 'ulid-1',
        ts: 'ts-1',
      },
      id: 'channel-1',
      type: 'CHATS2_READ_CHANNEL',
      unique: true,
    });
  });

  it('uses latest message from store when timestamp is missing', async () => {
    mockGetUlid.mockReturnValue('ulid-2');
    mockGetGroupsState.mockReturnValue({
      groups: {
        'channel-1': {
          groupInfo: {
            latest: {ts: 'latest-ts'},
          },
        },
      },
    });

    conversationMarker.markConversationAsRead('channel-1');
    await Promise.all([
      mockMarkConversationAsRead.mock.results[0]?.value,
      mockProcessEvent.mock.results[0]?.value,
    ]);

    expect(mockMarkConversationAsRead).toHaveBeenCalledWith(
      'channel-1',
      'latest-ts',
    );
  });

  it('does nothing when no timestamp is available', () => {
    mockGetGroupsState.mockReturnValue({groups: {}});

    conversationMarker.markConversationAsRead('channel-1');

    expect(mockMarkConversationAsRead).not.toHaveBeenCalled();
    expect(mockProcessEvent).not.toHaveBeenCalled();
  });

  it('queues operations when app is inactive and flushes on active', async () => {
    mockGetUlid.mockReturnValue('ulid-3');
    mockAppState.currentState = 'background';

    conversationMarker.markConversationAsRead('channel-1', 'ts-1');

    expect(mockMarkConversationAsRead).not.toHaveBeenCalled();

    appStateChangeHandler?.('active');
    await Promise.all([
      mockMarkConversationAsRead.mock.results[0]?.value,
      mockProcessEvent.mock.results[0]?.value,
    ]);

    expect(mockMarkConversationAsRead).toHaveBeenCalledWith(
      'channel-1',
      'ts-1',
    );
  });

  it('cancels and flushes pending operations', () => {
    conversationMarker.cancelPendingOperations('channel-1');
    conversationMarker.flushPendingOperations();

    expect(debouncedConversationMark.cancel).toHaveBeenCalledWith('channel-1');
    expect(debouncedMessageReceived.cancel).toHaveBeenCalledWith('channel-1');
    expect(debouncedConversationMark.flush).toHaveBeenCalled();
    expect(debouncedMessageReceived.flush).toHaveBeenCalled();
  });
});
