import store, { lastUpdatedSuffix } from '../store';
import { mockStorageImpl } from '../../../test/mock-storage';
import { getRandomIntBetween } from '../../../test/randomness';
import { fireEvent } from '@testing-library/dom';
import { mockDateNow } from '../../../test/mock-date-now';

describe('Store utilities', () => {
  const { setItemSpy, getItemSpy, removeItemSpy, forceStorageError, setStorageValue } = mockStorageImpl();
  const initialDateNow = 1640645432942;

  describe('set', () => {
    it('should be able to set various types of items correctly', () => {
      const mockKey = `mock-set-key-${getRandomIntBetween(1, 10)}`;
      const mockValues = [
        1,
        false,
        null,
        Math.random(),
        'some string\n value\twith trailing whitespace    ',
        { complex: { value: true, key: 'string' } },
      ];

      mockValues.forEach((mockValue, i) => {
        const result = store.set(mockKey, mockValue);

        expect(setItemSpy).toHaveBeenNthCalledWith(i + 1, mockKey, JSON.stringify(mockValue));
        expect(result).toBe(true);
      });
      expect(setItemSpy).toHaveBeenCalledTimes(mockValues.length);
    });

    it('should gracefully handle failure to set key', () => {
      const mockKey = 'mock-set-key';
      const mockValue = Math.random();
      let result: boolean | undefined;
      forceStorageError(() => {
        result = store.set(mockKey, mockValue);
      });

      expect(result).toBe(false);
    });
  });

  describe('get', () => {
    it('should be able to get various types of items correctly', () => {
      const initialValues = {
        int: 1,
        boolean: false,
        null: null,
        float: Math.random(),
        string: '\t\t\thello\nthere\n    ',
        object: {
          rolling: {
            in: {
              the: {
                deep: true,
              },
            },
          },
        },
      };
      const initialValueKeys = Object.keys(initialValues) as (keyof typeof initialValues)[];
      setStorageValue(
        initialValueKeys.reduce((acc, key) => {
          return { ...acc, [key]: JSON.stringify(initialValues[key]) };
        }, {}),
      );

      initialValueKeys.forEach((key, i) => {
        const result = store.get(key);

        expect(getItemSpy).toHaveBeenNthCalledWith(i + 1, key);
        expect(result).toEqual(initialValues[key]);
      });
      expect(getItemSpy).toHaveBeenCalledTimes(initialValueKeys.length);
    });

    it('should return original value if item cannot be parsed', () => {
      const mockKey = 'mock-get-key';
      const malformedValue = '({[+:"`';
      setStorageValue({
        [mockKey]: malformedValue,
      });
      const result = store.get(mockKey);
      expect(getItemSpy).toHaveBeenCalledTimes(1);
      expect(getItemSpy).toHaveBeenNthCalledWith(1, mockKey);
      expect(result).toBe(malformedValue);
    });

    it('should return null if item is not set', () => {
      const mockKey = `mock-get-key-${getRandomIntBetween(1, 10)}`;
      const result = store.get(mockKey);
      expect(getItemSpy).toHaveBeenCalledTimes(1);
      expect(getItemSpy).toHaveBeenNthCalledWith(1, mockKey);
      expect(result).toBe(null);
    });
  });

  describe('remove', () => {
    it('should remove the provided key', () => {
      const mockKey = `mock-remove-key-${getRandomIntBetween(1, 10)}`;
      const result = store.remove(mockKey);
      expect(removeItemSpy).toHaveBeenCalledTimes(1);
      expect(removeItemSpy).toHaveBeenNthCalledWith(1, mockKey);
      expect(result).toBe(true);
    });

    it('should gracefully handle failure to remove key', () => {
      const mockKey = `mock-remove-key-${getRandomIntBetween(1, 10)}`;
      let result: boolean | undefined;
      forceStorageError(() => {
        result = store.remove(mockKey);
      });
      expect(result).toBe(false);
    });
  });

  describe('watch', () => {
    it('should attach a storage event listener and fire when the provide key changes', () => {
      const mockKey = `mock-watch-key-${getRandomIntBetween(1, 10)}`;
      const mockValue = Math.random();
      const mockCallback = vi.fn();
      setStorageValue({
        [mockKey]: JSON.stringify(mockValue),
      });
      const addEventListenerSpy = vi.spyOn(window, 'addEventListener');

      const cleanup = store.watch(mockKey, mockCallback);

      // Should not get the item just yet, only register the event handler
      expect(getItemSpy).not.toHaveBeenCalled();
      expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
      expect(addEventListenerSpy.mock.calls[0][0]).toEqual('storage');

      // Should not call callback for unknown key
      let storageEvent = new StorageEvent('storage', { key: 'unknown-key' });
      fireEvent(window, storageEvent);
      expect(getItemSpy).not.toHaveBeenCalled();
      expect(mockCallback).not.toHaveBeenCalled();

      // Should call callback with the value from the store
      storageEvent = new StorageEvent('storage', { key: mockKey });
      fireEvent(window, storageEvent);
      expect(getItemSpy).toHaveBeenCalledTimes(1);
      expect(mockCallback).toHaveBeenCalledTimes(1);
      expect(mockCallback).toHaveBeenNthCalledWith(1, mockValue);

      // Remove the listener
      cleanup();
      storageEvent = new StorageEvent('storage', { key: mockKey });
      fireEvent(window, storageEvent);

      // Expect unchanged call counts due to removed handler
      expect(getItemSpy).toHaveBeenCalledTimes(1);
      expect(mockCallback).toHaveBeenCalledTimes(1);
      expect(mockCallback).toHaveBeenNthCalledWith(1, mockValue);
    });
  });

  describe('setWithExpireTime', () => {
    mockDateNow(initialDateNow);

    it('should set both original and last update key', () => {
      const mockKey = `mock-setWithExpireTime-key-${getRandomIntBetween(1, 10)}`;
      const mockValue = 'mock value';
      const mockMaxAge = 3600;
      store.setWithExpireTime(mockKey, mockValue, mockMaxAge);

      expect(setItemSpy).toHaveBeenCalledTimes(2);
      expect(setItemSpy).toHaveBeenNthCalledWith(1, mockKey, JSON.stringify(mockValue));
      expect(setItemSpy).toHaveBeenNthCalledWith(
        2,
        mockKey + lastUpdatedSuffix,
        JSON.stringify(initialDateNow + mockMaxAge),
      );
    });
  });

  describe('hasExpired', () => {
    mockDateNow(initialDateNow);
    const mockKey = `mock-hasExpired-key-${getRandomIntBetween(1, 10)}`;
    const mockLastUpdatedKey = mockKey + lastUpdatedSuffix;

    it('should return true for values that have no expiration key', () => {
      const result = store.hasExpired('undefined-key');
      expect(result).toBe(true);
    });

    it('should return true for keys with last update timestamp smaller than the current time', () => {
      setStorageValue({
        [mockLastUpdatedKey]: JSON.stringify(initialDateNow - 1),
      });

      const result = store.hasExpired(mockKey);

      expect(getItemSpy).toHaveBeenCalledTimes(1);
      expect(result).toBe(true);
    });

    it('should return false for keys with last update timestamp equal to the current time', () => {
      setStorageValue({
        [mockLastUpdatedKey]: JSON.stringify(initialDateNow),
      });

      const result = store.hasExpired(mockKey);

      expect(getItemSpy).toHaveBeenCalledTimes(1);
      expect(result).toBe(false);
    });

    it('should return false for keys with last update timestamp greater than the current time', () => {
      setStorageValue({
        [mockLastUpdatedKey]: JSON.stringify(initialDateNow + 1),
      });

      const result = store.hasExpired(mockKey);

      expect(getItemSpy).toHaveBeenCalledTimes(1);
      expect(result).toBe(false);
    });
  });
});
