import {ulid} from 'ulid';

jest.mock('react-native-mmkv', () => {
  // Storage per MMKV instance id
  const storesById: Record<string, Record<string, unknown>> = {};

  const getStore = (id: string) => {
    if (!storesById[id]) {
      storesById[id] = {};
    }
    return storesById[id];
  };

  class MMKVMock {
    private id: string;

    constructor(config?: {id?: string}) {
      this.id = config?.id ?? 'default';
    }

    // String
    getString(key: string) {
      const store = getStore(this.id);
      const v = store[key];
      return typeof v === 'string' ? v : undefined;
    }

    set(key: string, value: unknown) {
      // MMKV has set(key, value) that accepts string/number/bool (and more depending on version)
      const store = getStore(this.id);
      store[key] = value;
    }

    // Number
    getNumber(key: string) {
      const store = getStore(this.id);
      const v = store[key];
      return typeof v === 'number' ? v : undefined;
    }

    setNumber(key: string, value: number) {
      const store = getStore(this.id);
      store[key] = value;
    }

    // Boolean
    getBoolean(key: string) {
      const store = getStore(this.id);
      const v = store[key];
      return typeof v === 'boolean' ? v : undefined;
    }

    setBoolean(key: string, value: boolean) {
      const store = getStore(this.id);
      store[key] = value;
    }

    remove(key: string) {
      const store = getStore(this.id);
      delete store[key];
    }

    contains(key: string) {
      const store = getStore(this.id);
      return Object.prototype.hasOwnProperty.call(store, key);
    }

    delete(key: string) {
      const store = getStore(this.id);
      delete store[key];
    }

    getAllKeys() {
      const store = getStore(this.id);
      return Object.keys(store);
    }

    clearAll() {
      const store = getStore(this.id);
      Object.keys(store).forEach(k => delete store[k]);
    }

    get size() {
      return Object.keys(getStore(this.id)).length;
    }
  }

  const createMMKV = jest.fn((config?: {id?: string}) => new MMKVMock(config));

  return {
    createMMKV,
    MMKV: jest
      .fn()
      .mockImplementation((config?: {id?: string}) => new MMKVMock(config)),

    __getStoreById: (id: string) => getStore(id),
    __clearAllStores: () => {
      Object.keys(storesById).forEach(id => delete storesById[id]);
    },
  };
});

// Mock getConversations service
const mockGetConversations = jest.fn();
jest.mock('@modules/chats/services', () => ({
  getConversations: (...args: unknown[]) => mockGetConversations(...args),
}));

// Mock sentry logs and getUlid (skip the @shared/utils barrel — only these symbols are used).
jest.mock('@shared/utils', () => {
  const {ulid: mockUlid} = jest.requireActual('ulid');

  return {
    sentryLogs: {
      error: jest.fn(),
      warn: jest.fn(),
      info: jest.fn(),
      debug: jest.fn(),
    },
    getUlid: () => mockUlid(),
    wait: (ms: number) => new Promise(resolve => setTimeout(resolve, ms)),
  };
});

// Skip @modules/chats/store barrel — the service only consumes unreadsStore.
jest.mock('@modules/chats/store', () => ({
  unreadsStore: jest.requireActual('@modules/chats/store/zustand/unreads')
    .unreadsStore,
}));

type Deferred<T> = {
  promise: Promise<T>;
  resolve: (value: T) => void;
  reject: (reason?: unknown) => void;
};

function deferred<T>(): Deferred<T> {
  let resolve!: (value: T) => void;
  let reject!: (reason?: unknown) => void;

  const promise = new Promise<T>((res, rej) => {
    resolve = res;
    reject = rej;
  });

  return {promise, resolve, reject};
}

async function waitFor(
  fn: () => boolean,
  {timeoutMs = 200, stepMs = 0}: {timeoutMs?: number; stepMs?: number} = {},
) {
  const start = Date.now();
  while (!fn()) {
    if (Date.now() - start > timeoutMs) {
      throw new Error('waitFor timeout');
    }
    if (stepMs > 0) {
      await new Promise(r => setTimeout(r, stepMs));
    } else {
      await Promise.resolve();
    }
  }
}

// Helper to generate ULIDs with controlled timestamps
const generateUlid = (timestamp: number) => ulid(timestamp);

let UnreadsUpdaterService: typeof import('../updaters/UnreadsUpdaterService').UnreadsUpdaterService;
let UNREAD_HANDLER_STORES: typeof import('../updaters/UnreadsUpdaterService').UNREAD_HANDLER_STORES;
let unreadsStore: typeof import('@modules/chats/store').unreadsStore;

let __getStoreById: (id: string) => Record<string, unknown>;
let __clearAllStores: () => void;

const getStoreState = () => unreadsStore.getState();

const CHANNEL_ID = 'channel_01';
const CHANNEL_ID2 = 'channel_02';
const USER_ID_1 = 100;
const USER_ID_2 = 200;

type StoreNamespaceIds = {
  counts: string;
  latestMsg: string;
  lastRead: string;
};

const getStoreIdsByUser = (user_id: number = USER_ID_1) => {
  const storeIds = (UnreadsUpdaterService as any).getStoreIdsByUser(user_id);
  return storeIds as StoreNamespaceIds;
};

const getNamespacesStore = () => {
  return __getStoreById(UNREAD_HANDLER_STORES.storeNamespaces) as Record<
    string,
    string //{counts: string; latestMsg: string; lastRead: string}
  >;
};

const getUnreadsCountStore = (user_id: number = USER_ID_1) => {
  const storeIds = getStoreIdsByUser(user_id);
  return __getStoreById(storeIds.counts) as Record<string, number>;
};

const getUnreadsLatestStore = (user_id: number = USER_ID_1) => {
  const storeIds = getStoreIdsByUser(user_id);
  return __getStoreById(storeIds.latestMsg) as Record<string, string>;
};

const getResponseAsLocal = (
  storageCounts: Record<string, number>,
  storageLatests: Record<string, string>,
) => {
  return [
    {
      id: CHANNEL_ID,
      latest: {
        ts: storageLatests[CHANNEL_ID],
      },
      readMembership: {
        unread_count: storageCounts[CHANNEL_ID],
      },
    },
    {
      id: CHANNEL_ID2,
      latest: {
        ts: storageLatests[CHANNEL_ID2],
      },
      readMembership: {
        unread_count: storageCounts[CHANNEL_ID2],
      },
    },
  ];
};

describe('UnreadsHandler', () => {
  // Base timestamp for generating ULIDs
  let baseTimestamp: number;

  const clearAllState = () => {
    __clearAllStores();

    // Clear the handler
    UnreadsUpdaterService.cleanup();

    // Mark the one-time storage reset as already done so init() doesn't wipe test data
    const namespacesStore = __getStoreById(
      UNREAD_HANDLER_STORES.storeNamespaces,
    );
    namespacesStore.__UNREADS_RESET_VERSION__ = 1;

    // Clear mocks
    mockGetConversations.mockClear();
  };

  beforeEach(() => {
    jest.resetModules();

    ({__getStoreById, __clearAllStores} = jest.requireMock(
      'react-native-mmkv',
    ) as {
      __getStoreById: (id: string) => Record<string, unknown>;
      __clearAllStores: () => void;
    });

    ({
      UNREAD_HANDLER_STORES,
      UnreadsUpdaterService,
    } = require('../updaters/UnreadsUpdaterService'));
    ({unreadsStore} = require('@modules/chats/store'));

    clearAllState();
    baseTimestamp = Date.now();
  });
  describe('Initialization', () => {
    it('should load unreads from storage on init', async () => {
      const runSyncSpy = jest
        .spyOn(UnreadsUpdaterService, 'runSync')
        .mockResolvedValue(undefined);

      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(8);

      runSyncSpy.mockRestore();
    });

    it('should handle init with empty storage', async () => {
      mockGetConversations.mockResolvedValue({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: generateUlid(baseTimestamp),
            },
            readMembership: {
              unread_count: 5,
            },
          },
          {
            id: CHANNEL_ID2,
            latest: {
              ts: generateUlid(baseTimestamp),
            },
            readMembership: {
              unread_count: 2,
            },
          },
        ],
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(2);
      expect(getStoreState().totalUnreadCount).toBe(7);
    });
  });

  describe('Server synchronization', () => {
    it('should update unreads from server and override local values', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      // Set up unreads in separate stores
      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      mockGetConversations.mockResolvedValue({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: generateUlid(baseTimestamp + 1000),
            },
            readMembership: {
              unread_count: 7,
            },
          },
          {
            id: CHANNEL_ID2,
            latest: {
              ts: generateUlid(baseTimestamp + 2000),
            },
            readMembership: {
              unread_count: 3,
            },
          },
        ],
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(7);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(10);
    });

    it('should merge server data with pending socket events', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      // Set up unreads in separate stores
      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      // + 1
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 2500),
      );
      // + 1
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 3000),
      );

      // + 1
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID2,
        generateUlid(baseTimestamp + 2500),
      );
      // + 1
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID2,
        generateUlid(baseTimestamp + 3000),
      );

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(7);

      d.resolve({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: generateUlid(baseTimestamp + 1000),
            },
            readMembership: {
              unread_count: 7,
            },
          },
          {
            id: CHANNEL_ID2,
            latest: {
              ts: generateUlid(baseTimestamp + 2000),
            },
            readMembership: {
              unread_count: 3,
            },
          },
        ],
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(9);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(5);
      expect(getStoreState().totalUnreadCount).toBe(14);
    });

    it('should keep local read state when server has unreads with same timestamp', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageLatests[CHANNEL_ID] = channel1Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);

      UnreadsUpdaterService.markAsRead(CHANNEL_ID, channel1Ts);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);

      d.resolve({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: channel1Ts,
            },
            readMembership: {
              unread_count: 5,
            },
          },
        ],
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);
      expect(getStoreState().totalUnreadCount).toBe(0);
    });

    it('should keep local state when local timestamp is newer than server', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageLatests[CHANNEL_ID] = channel1Ts;

      mockGetConversations.mockResolvedValue({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: generateUlid(baseTimestamp - 1000),
            },
            readMembership: {
              unread_count: 3,
            },
          },
        ],
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().totalUnreadCount).toBe(5);
    });

    it('should skip conversations without readMembership', async () => {
      mockGetConversations.mockResolvedValue({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: generateUlid(baseTimestamp),
            },
          },
          {
            id: CHANNEL_ID2,
            latest: {
              ts: generateUlid(baseTimestamp),
            },
            readMembership: {
              unread_count: 3,
            },
          },
        ],
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBeUndefined();
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(3);
    });

    it('should persist data after sync', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageLatests[CHANNEL_ID] = channel1Ts;

      const serverTs = generateUlid(baseTimestamp + 1000);

      mockGetConversations.mockResolvedValue({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: serverTs,
            },
            readMembership: {
              unread_count: 7,
            },
          },
        ],
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getUnreadsCountStore()[CHANNEL_ID]).toBe(7);
      expect(getUnreadsLatestStore()[CHANNEL_ID]).toBe(serverTs);
    });
  });

  describe('cleanup', () => {
    it('should persist data after cleanup', async () => {
      const storageCounts = getUnreadsCountStore(USER_ID_1);
      const storageLatests = getUnreadsLatestStore(USER_ID_1);
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(8);

      UnreadsUpdaterService.cleanup();

      expect(getStoreState().unreads[CHANNEL_ID]).toBeUndefined();
      expect(getStoreState().unreads[CHANNEL_ID2]).toBeUndefined();
      expect(getStoreState().totalUnreadCount).toBe(0);

      expect(storageCounts[CHANNEL_ID]).toBe(5);
      expect(storageCounts[CHANNEL_ID2]).toBe(3);
      expect(storageLatests[CHANNEL_ID]).toBe(channel1Ts);
      expect(storageLatests[CHANNEL_ID2]).toBe(channel2Ts);

      expect((UnreadsUpdaterService as any).isSyncRunning).toBe(false);
    });

    it('should restore state after cleanup and re-init', async () => {
      const storageCounts = getUnreadsCountStore(USER_ID_1);
      const storageLatests = getUnreadsLatestStore(USER_ID_1);
      const channel1Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageLatests[CHANNEL_ID] = channel1Ts;

      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);

      UnreadsUpdaterService.cleanup();

      mockGetConversations.mockResolvedValue({
        data: [],
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().totalUnreadCount).toBe(5);
    });
  });

  describe('markAsRead', () => {
    it('should reset unread count to zero', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      // Set up unreads in separate stores
      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(8);

      UnreadsUpdaterService.markAsRead(CHANNEL_ID, channel1Ts);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(3);

      UnreadsUpdaterService.markAsRead(CHANNEL_ID2, channel2Ts);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(0);
      expect(getStoreState().totalUnreadCount).toBe(0);
    });

    it('should sync read status from another device', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      // Set up unreads in separate stores
      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);

      d.resolve({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: channel1Ts,
            },
            readMembership: {
              unread_count: 0,
            },
          },
          {
            id: CHANNEL_ID2,
            latest: {
              ts: channel2Ts,
            },
            readMembership: {
              unread_count: 0,
            },
          },
        ],
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(0);
      expect(getStoreState().totalUnreadCount).toBe(0);
    });

    it('should handle new messages after local mark as read', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      // Set up unreads in separate stores
      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);

      UnreadsUpdaterService.markAsRead(CHANNEL_ID, channel1Ts);
      UnreadsUpdaterService.markAsRead(CHANNEL_ID2, channel2Ts);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(0);

      d.resolve({
        data: [
          {
            id: CHANNEL_ID,
            latest: {
              ts: generateUlid(baseTimestamp + 100),
            },
            readMembership: {
              unread_count: 1,
            },
          },
          {
            id: CHANNEL_ID2,
            latest: {
              ts: generateUlid(baseTimestamp + 100),
            },
            readMembership: {
              unread_count: 1,
            },
          },
        ],
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(1);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(1);
      expect(getStoreState().totalUnreadCount).toBe(2);
    });

    it('should keep zero count when marked with pending messages during sync', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      // Set up unreads in separate stores
      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 100),
      );

      const latestMsgTs = generateUlid(baseTimestamp + 200);
      UnreadsUpdaterService.incrementUnread(CHANNEL_ID, latestMsgTs);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(7);

      UnreadsUpdaterService.markAsRead(CHANNEL_ID, latestMsgTs); // marked with the latest seen message ts

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);

      d.resolve({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(3);
    });

    it('should increment count when marked with pending messages during sync and new event arrived just after a markAsRead but before sync completes', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      // Set up unreads in separate stores
      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 100),
      );

      const latestMsgTs = generateUlid(baseTimestamp + 200);
      UnreadsUpdaterService.incrementUnread(CHANNEL_ID, latestMsgTs);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(7);

      UnreadsUpdaterService.markAsRead(CHANNEL_ID, latestMsgTs); // marked with the latest seen message ts

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(0);

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 300),
      );

      d.resolve({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(1);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(4);
    });

    it('should handle markAsRead for non-existing channel', () => {
      const nonExistingChannel = 'non_existing_channel';

      UnreadsUpdaterService.markAsRead(
        nonExistingChannel,
        generateUlid(baseTimestamp),
      );

      expect(getStoreState().unreads[nonExistingChannel]).toBeUndefined();
    });

    it('should persist data after markAsRead', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageLatests[CHANNEL_ID] = channel1Ts;

      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      const newTs = generateUlid(baseTimestamp + 1000);
      UnreadsUpdaterService.markAsRead(CHANNEL_ID, newTs);

      expect(getUnreadsCountStore()[CHANNEL_ID]).toBe(0);
      expect(getUnreadsLatestStore()[CHANNEL_ID]).toBe(newTs);
    });
  });

  describe('incrementUnread', () => {
    it('should increment unreads for new messages', () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });
      UnreadsUpdaterService.init(USER_ID_1);

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 1000),
      );
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 2000),
      );

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID2,
        generateUlid(baseTimestamp + 1000),
      );
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID2,
        generateUlid(baseTimestamp + 2000),
      );

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(7);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(5);
      expect(getStoreState().totalUnreadCount).toBe(12);
    });

    it('should ignore messages older than current latest', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp - 100),
      );
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp - 200),
      );

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(8);
    });

    it('should ignore messages with same timestamp as current latest', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      UnreadsUpdaterService.incrementUnread(CHANNEL_ID, channel1Ts);
      UnreadsUpdaterService.incrementUnread(CHANNEL_ID, channel1Ts);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(8);
    });

    it('should handle increment for new channel', async () => {
      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      const newChannelTs = generateUlid(baseTimestamp + 1000);
      UnreadsUpdaterService.incrementUnread(CHANNEL_ID, newChannelTs);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(1);

      d.resolve({
        data: [],
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(1);
      expect(getStoreState().totalUnreadCount).toBe(1);
    });

    it('should queue events during sync and apply after completion', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 1000),
      );
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp + 2000),
      );

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID2,
        generateUlid(baseTimestamp + 1000),
      );
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID2,
        generateUlid(baseTimestamp + 2000),
      );

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(7);

      d.resolve({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(7);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(5);
      expect(getStoreState().totalUnreadCount).toBe(12);
    });

    it('should ignore old pending events after sync', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      // Set up unreads in separate stores
      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp - 1000),
      );

      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp - 2000),
      );
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp - 2000),
      );
      UnreadsUpdaterService.incrementUnread(
        CHANNEL_ID,
        generateUlid(baseTimestamp - 2000),
      );

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);

      d.resolve({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(8);
    });

    it('should ignore duplicate timestamp events during sync', async () => {
      const storageCounts = getUnreadsCountStore();
      const storageLatests = getUnreadsLatestStore();
      const channel1Ts = generateUlid(baseTimestamp);
      const channel2Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageCounts[CHANNEL_ID2] = 3;

      storageLatests[CHANNEL_ID] = channel1Ts;
      storageLatests[CHANNEL_ID2] = channel2Ts;

      const d = deferred<{data: any[]; nextCursor: string | null}>();

      mockGetConversations.mockImplementation(() => d.promise);

      const initPromise = UnreadsUpdaterService.init(USER_ID_1);

      await waitFor(
        () => (UnreadsUpdaterService as any).isSyncRunning === true,
      );

      UnreadsUpdaterService.incrementUnread(CHANNEL_ID, channel1Ts);
      UnreadsUpdaterService.incrementUnread(CHANNEL_ID2, channel2Ts);

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);

      d.resolve({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await initPromise;

      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(3);
      expect(getStoreState().totalUnreadCount).toBe(8);
    });

    it('should persist data after increment', async () => {
      const storageCounts = getUnreadsCountStore(USER_ID_1);
      const storageLatests = getUnreadsLatestStore(USER_ID_1);
      const channel1Ts = generateUlid(baseTimestamp);

      storageCounts[CHANNEL_ID] = 5;
      storageLatests[CHANNEL_ID] = channel1Ts;

      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCounts, storageLatests),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      const newTs = generateUlid(baseTimestamp + 1000);
      UnreadsUpdaterService.incrementUnread(CHANNEL_ID, newTs);

      expect(storageCounts[CHANNEL_ID]).toBe(6);
      expect(storageLatests[CHANNEL_ID]).toBe(newTs);
    });
  });
  describe('2 accounts | should persist the data for both accs', () => {
    it('should maintain separate unreads for different users', async () => {
      /*
      -------- USER_1 -------------
      */
      const storageCountsUser1 = getUnreadsCountStore(USER_ID_1);
      const storageLatestsUser1 = getUnreadsLatestStore(USER_ID_1);

      const storeNamespaces = getNamespacesStore();

      const channel1TsUser1 = generateUlid(baseTimestamp);

      storageCountsUser1[CHANNEL_ID] = 5;
      storageCountsUser1[CHANNEL_ID2] = 1;
      storageLatestsUser1[CHANNEL_ID] = channel1TsUser1;
      storageLatestsUser1[CHANNEL_ID2] = channel1TsUser1;

      // USER_1 login and init
      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCountsUser1, storageLatestsUser1),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_1);

      expect(storeNamespaces[USER_ID_1]).toBeDefined();
      const user1StoreIds = JSON.parse(
        storeNamespaces[USER_ID_1],
      ) as StoreNamespaceIds;

      expect(user1StoreIds.counts).toBe(
        `${UNREAD_HANDLER_STORES.counts}:${USER_ID_1}`,
      );
      expect(user1StoreIds.lastRead).toBe(
        `${UNREAD_HANDLER_STORES.lastRead}:${USER_ID_1}`,
      );
      expect(user1StoreIds.latestMsg).toBe(
        `${UNREAD_HANDLER_STORES.latestMsg}:${USER_ID_1}`,
      );

      expect(storeNamespaces[USER_ID_2]).toBeUndefined();

      // MMKV store
      expect(storageCountsUser1[CHANNEL_ID]).toBe(5);
      expect(storageCountsUser1[CHANNEL_ID2]).toBe(1);
      expect(storageLatestsUser1[CHANNEL_ID]).toBe(channel1TsUser1);
      expect(storageLatestsUser1[CHANNEL_ID2]).toBe(channel1TsUser1);

      // Zustand store
      expect(getStoreState().unreads[CHANNEL_ID]).toBe(5);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(1);
      expect(getStoreState().totalUnreadCount).toBe(6);

      // USER_1 logout
      UnreadsUpdaterService.cleanup();

      // should persist the MMKV store user1 data
      // MMKV store
      expect(storageCountsUser1[CHANNEL_ID]).toBe(5);
      expect(storageCountsUser1[CHANNEL_ID2]).toBe(1);
      expect(storageLatestsUser1[CHANNEL_ID]).toBe(channel1TsUser1);
      expect(storageLatestsUser1[CHANNEL_ID2]).toBe(channel1TsUser1);

      // Zustand store
      expect(getStoreState().unreads[CHANNEL_ID]).toBeUndefined();
      expect(getStoreState().unreads[CHANNEL_ID2]).toBeUndefined();
      expect(getStoreState().totalUnreadCount).toBe(0);

      /*
      -------- USER_2 -------------
      */

      const storageCountsUser2 = getUnreadsCountStore(USER_ID_2);
      const storageLatestsUser2 = getUnreadsLatestStore(USER_ID_2);

      const channel1TsUser2 = generateUlid(baseTimestamp);

      storageCountsUser2[CHANNEL_ID] = 10;
      storageCountsUser2[CHANNEL_ID2] = 1;
      storageLatestsUser2[CHANNEL_ID] = channel1TsUser2;
      storageLatestsUser2[CHANNEL_ID2] = channel1TsUser2;

      // USER_2 login and init
      mockGetConversations.mockResolvedValue({
        data: getResponseAsLocal(storageCountsUser2, storageLatestsUser2),
        nextCursor: null,
      });

      await UnreadsUpdaterService.init(USER_ID_2);

      expect(storeNamespaces[USER_ID_2]).toBeDefined();
      const user2StoreIds = JSON.parse(
        storeNamespaces[USER_ID_2],
      ) as StoreNamespaceIds;

      expect(user2StoreIds.counts).toBe(
        `${UNREAD_HANDLER_STORES.counts}:${USER_ID_2}`,
      );
      expect(user2StoreIds.lastRead).toBe(
        `${UNREAD_HANDLER_STORES.lastRead}:${USER_ID_2}`,
      );
      expect(user2StoreIds.latestMsg).toBe(
        `${UNREAD_HANDLER_STORES.latestMsg}:${USER_ID_2}`,
      );

      // Check the user_2 mmkv store
      expect(storageCountsUser2[CHANNEL_ID]).toBe(10);
      expect(storageCountsUser2[CHANNEL_ID2]).toBe(1);
      expect(storageLatestsUser2[CHANNEL_ID]).toBe(channel1TsUser2);
      expect(storageLatestsUser2[CHANNEL_ID2]).toBe(channel1TsUser2);

      // Check the user_1 mmkv store ( should persist the data)
      expect(storageCountsUser1[CHANNEL_ID]).toBe(5);
      expect(storageCountsUser1[CHANNEL_ID2]).toBe(1);
      expect(storageLatestsUser1[CHANNEL_ID]).toBe(channel1TsUser1);
      expect(storageLatestsUser1[CHANNEL_ID2]).toBe(channel1TsUser1);

      // Zustand store
      expect(getStoreState().unreads[CHANNEL_ID]).toBe(10);
      expect(getStoreState().unreads[CHANNEL_ID2]).toBe(1);
      expect(getStoreState().totalUnreadCount).toBe(11);

      // USER_2 logout
      UnreadsUpdaterService.cleanup();

      // Should persist the data for USER_1 and USER_2
      // USER_1 mmkv stores
      expect(storageCountsUser1[CHANNEL_ID]).toBe(5);
      expect(storageCountsUser1[CHANNEL_ID2]).toBe(1);
      expect(storageLatestsUser1[CHANNEL_ID]).toBe(channel1TsUser1);
      expect(storageLatestsUser1[CHANNEL_ID2]).toBe(channel1TsUser1);
      // USER_2 mmkv stores
      expect(storageCountsUser2[CHANNEL_ID]).toBe(10);
      expect(storageCountsUser2[CHANNEL_ID2]).toBe(1);
      expect(storageLatestsUser2[CHANNEL_ID]).toBe(channel1TsUser2);
      expect(storageLatestsUser2[CHANNEL_ID2]).toBe(channel1TsUser2);

      expect(storeNamespaces[USER_ID_1]).toBeDefined();
      expect(storeNamespaces[USER_ID_2]).toBeDefined();
    });
  });
});
