import {ulid} from 'ulid';

// Storage instance that persists across mock calls
const mockMmkvStorage: Record<string, string> = {};
const mockCreateMMKV = jest.fn();

// Mock MessageRangesHandler
const mockAddMessagesAndUpdateRanges = jest.fn();
const mockGet = jest.fn().mockReturnValue([]);

// Mock definitions as functions for reuse after resetModules (must start with 'mock')
const createMockMMKVInstance = () => ({
  getString: (key: string) => mockMmkvStorage[key],
  set: (key: string, value: string) => {
    mockMmkvStorage[key] = value;
  },
  remove: (key: string) => {
    delete mockMmkvStorage[key];
  },
});

const mockGetMmkv = () => ({
  MMKV: jest.fn(),
  createMMKV: mockCreateMMKV.mockImplementation(() => createMockMMKVInstance()),
});

const mockGetRangesHandler = () => ({
  MessageRangesHandler: {
    addMessagesAndUpdateRanges: mockAddMessagesAndUpdateRanges,
    get: mockGet,
  },
});

// Initial mocks
jest.mock('react-native-mmkv', () => mockGetMmkv());
jest.mock('../MessageRangesHandler', () => mockGetRangesHandler());

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

// Helper to create fresh instance (resetModules clears mocks, so we reapply them)
const createPendingMessagesTracker = () => {
  jest.resetModules();
  jest.mock('react-native-mmkv', () => mockGetMmkv());
  jest.mock('../MessageRangesHandler', () => mockGetRangesHandler());
  // eslint-disable-next-line @typescript-eslint/no-var-requires
  return require('../PendingMessagesTracker').PendingMessagesTracker;
};

describe('PendingMessagesTracker', () => {
  const CHANNEL_ID = 'test-channel-1';
  const CHANNEL_ID_2 = 'test-channel-2';

  let baseTimestamp: number;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let PendingMessagesTracker: any;

  beforeEach(() => {
    // Clear all state
    Object.keys(mockMmkvStorage).forEach(key => delete mockMmkvStorage[key]);
    mockCreateMMKV.mockClear();
    mockAddMessagesAndUpdateRanges.mockClear();
    mockGet.mockClear();
    mockGet.mockReturnValue([]);

    // Fresh instance
    PendingMessagesTracker = createPendingMessagesTracker();

    // Base timestamp for ULID generation
    baseTimestamp = Date.now() - 100000;
  });

  describe('registerPending', () => {
    it('should register a message as pending', () => {
      const ts = generateUlid(baseTimestamp);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts);

      const pending = PendingMessagesTracker.getPending(CHANNEL_ID);
      expect(pending).toContain(ts);
      expect(pending).toHaveLength(1);
    });

    it('should register multiple messages as pending', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);
      const ts3 = generateUlid(baseTimestamp + 2000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts2);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts3);

      const pending = PendingMessagesTracker.getPending(CHANNEL_ID);
      expect(pending).toHaveLength(3);
      expect(pending).toContain(ts1);
      expect(pending).toContain(ts2);
      expect(pending).toContain(ts3);
    });

    it('should keep pending messages separate per channel', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID_2, ts2);

      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toEqual([ts1]);
      expect(PendingMessagesTracker.getPending(CHANNEL_ID_2)).toEqual([ts2]);
    });

    it('should not duplicate if same ts is registered twice', () => {
      const ts = generateUlid(baseTimestamp);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts);

      const pending = PendingMessagesTracker.getPending(CHANNEL_ID);
      expect(pending).toHaveLength(1);
    });
  });

  describe('confirmSaved', () => {
    it('should remove message from pending when confirmed', () => {
      const ts = generateUlid(baseTimestamp);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts);
      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toHaveLength(1);

      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts);
      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toHaveLength(0);
    });

    it('should add confirmed message to awaiting range update list', () => {
      const ts = generateUlid(baseTimestamp);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts);
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts);

      const confirmed = PendingMessagesTracker.getAndClearConfirmed(CHANNEL_ID);
      expect(confirmed).toContain(ts);
    });

    it('should handle confirming non-existent message gracefully', () => {
      const ts = generateUlid(baseTimestamp);

      // Should not throw
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts);

      // Still adds to confirmed list
      const confirmed = PendingMessagesTracker.getAndClearConfirmed(CHANNEL_ID);
      expect(confirmed).toContain(ts);
    });
  });

  describe('markFailed', () => {
    it('should remove message from pending without adding to confirmed', () => {
      const ts = generateUlid(baseTimestamp);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts);
      PendingMessagesTracker.markFailed(CHANNEL_ID, ts);

      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toHaveLength(0);
      expect(
        PendingMessagesTracker.getAndClearConfirmed(CHANNEL_ID),
      ).toHaveLength(0);
    });

    it('should handle marking non-existent message as failed gracefully', () => {
      const ts = generateUlid(baseTimestamp);

      // Should not throw
      PendingMessagesTracker.markFailed(CHANNEL_ID, ts);

      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toHaveLength(0);
    });
  });

  describe('hasPendingMessagesBefore', () => {
    it('should return false when no pending messages', () => {
      const ts = generateUlid(baseTimestamp);

      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts),
      ).toBe(false);
    });

    it('should return true when there are pending messages before the given ts', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 10000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);

      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts2),
      ).toBe(true);
    });

    it('should return false when all pending messages are after the given ts', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 10000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts2);

      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts1),
      ).toBe(false);
    });

    it('should return false when pending message has same ts', () => {
      const ts = generateUlid(baseTimestamp);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts);

      // Same ts is not "before"
      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts),
      ).toBe(false);
    });

    it('should check only the specified channel', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 10000);

      PendingMessagesTracker.registerPending(CHANNEL_ID_2, ts1);

      // Different channel should not affect
      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts2),
      ).toBe(false);
    });
  });

  describe('getAndClearConfirmed', () => {
    it('should return and clear all confirmed messages', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts2);
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts1);
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts2);

      const confirmed = PendingMessagesTracker.getAndClearConfirmed(CHANNEL_ID);
      expect(confirmed).toHaveLength(2);
      expect(confirmed).toContain(ts1);
      expect(confirmed).toContain(ts2);

      // Should be cleared now
      expect(
        PendingMessagesTracker.getAndClearConfirmed(CHANNEL_ID),
      ).toHaveLength(0);
    });

    it('should return empty array when no confirmed messages', () => {
      const confirmed = PendingMessagesTracker.getAndClearConfirmed(CHANNEL_ID);
      expect(confirmed).toEqual([]);
    });
  });

  describe('clearConversation', () => {
    it('should clear all pending and confirmed messages for a conversation', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts2);

      PendingMessagesTracker.clearConversation(CHANNEL_ID);

      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toHaveLength(0);
      expect(
        PendingMessagesTracker.getAndClearConfirmed(CHANNEL_ID),
      ).toHaveLength(0);
    });

    it('should not affect other conversations', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID_2, ts2);

      PendingMessagesTracker.clearConversation(CHANNEL_ID);

      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toHaveLength(0);
      expect(PendingMessagesTracker.getPending(CHANNEL_ID_2)).toHaveLength(1);
    });
  });

  describe('confirmMessagesAndUpdateRanges', () => {
    it('should confirm all messages and update ranges', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts2);

      PendingMessagesTracker.confirmMessagesAndUpdateRanges([
        {channel: CHANNEL_ID, ts: ts1},
        {channel: CHANNEL_ID, ts: ts2},
      ]);

      // Pending should be cleared
      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toHaveLength(0);

      // Should have called addMessagesAndUpdateRanges
      expect(mockAddMessagesAndUpdateRanges).toHaveBeenCalledWith(
        CHANNEL_ID,
        expect.arrayContaining([{ts: ts1}, {ts: ts2}]),
        expect.objectContaining({
          isStart: false,
          isEnd: true,
          skipPendingCheck: true,
        }),
      );
    });

    it('should group messages by channel and update each separately', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID_2, ts2);

      PendingMessagesTracker.confirmMessagesAndUpdateRanges([
        {channel: CHANNEL_ID, ts: ts1},
        {channel: CHANNEL_ID_2, ts: ts2},
      ]);

      expect(mockAddMessagesAndUpdateRanges).toHaveBeenCalledTimes(2);
    });

    it('should use live range newestTs as bridgeOldestTs', () => {
      const existingNewestTs = generateUlid(baseTimestamp - 1000);
      const ts1 = generateUlid(baseTimestamp);

      mockGet.mockReturnValue([
        {
          oldestTs: generateUlid(baseTimestamp - 5000),
          newestTs: existingNewestTs,
          isStart: false,
          isEnd: true,
        },
      ]);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.confirmMessagesAndUpdateRanges([
        {channel: CHANNEL_ID, ts: ts1},
      ]);

      expect(mockAddMessagesAndUpdateRanges).toHaveBeenCalledWith(
        CHANNEL_ID,
        expect.any(Array),
        expect.objectContaining({
          bridgeOldestTs: existingNewestTs,
        }),
      );
    });
  });

  describe('markMessagesFailed', () => {
    it('should mark all messages as failed', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts2);

      PendingMessagesTracker.markMessagesFailed([
        {channel: CHANNEL_ID, ts: ts1},
        {channel: CHANNEL_ID, ts: ts2},
      ]);

      expect(PendingMessagesTracker.getPending(CHANNEL_ID)).toHaveLength(0);
      // Should not be in confirmed either
      expect(
        PendingMessagesTracker.getAndClearConfirmed(CHANNEL_ID),
      ).toHaveLength(0);
    });
  });

  describe('Race Condition Scenarios', () => {
    it('should prevent range extension when older message is still pending', () => {
      const ts1 = generateUlid(baseTimestamp); // Older
      const ts2 = generateUlid(baseTimestamp + 10000); // Newer

      // Message 1 arrives first (older)
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);

      // Message 2 arrives second (newer)
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts2);

      // Message 2 tries to check if it can extend range
      // Should be blocked because ts1 is pending and ts1 < ts2
      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts2),
      ).toBe(true);
    });

    it('should allow range extension after older message is confirmed', () => {
      const ts1 = generateUlid(baseTimestamp); // Older
      const ts2 = generateUlid(baseTimestamp + 10000); // Newer

      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts2);

      // Confirm the older message first
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts1);

      // Now ts2 should not be blocked
      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts2),
      ).toBe(false);
    });

    it('should handle out-of-order confirmation correctly', () => {
      const ts1 = generateUlid(baseTimestamp);
      const ts2 = generateUlid(baseTimestamp + 1000);
      const ts3 = generateUlid(baseTimestamp + 2000);

      // All arrive
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts1);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts2);
      PendingMessagesTracker.registerPending(CHANNEL_ID, ts3);

      // ts3 confirms first (out of order)
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts3);

      // Still have pending before ts3
      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts3),
      ).toBe(true);

      // Confirm ts1
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts1);

      // Still have ts2 pending before ts3
      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts3),
      ).toBe(true);

      // Confirm ts2
      PendingMessagesTracker.confirmSaved(CHANNEL_ID, ts2);

      // Now nothing is pending before ts3
      expect(
        PendingMessagesTracker.hasPendingMessagesBefore(CHANNEL_ID, ts3),
      ).toBe(false);
    });
  });
});
