import * as UserApi from '~/api/user_api';
import UsersCache from '~/lib/utils/users_cache';

describe('UsersCache', () => {
  const dummyUsername = 'win';
  const dummyUserId = 123;
  const dummyUser = {
    name: 'has a farm',
    username: 'farmer',
  };
  const dummyUserStatus = 'my status';

  beforeEach(() => {
    UsersCache.internalStorage = {};
  });

  describe('get', () => {
    it('returns undefined for empty cache', () => {
      expect(UsersCache.internalStorage).toEqual({});

      const user = UsersCache.get(dummyUsername);

      expect(user).toBe(undefined);
    });

    it('returns undefined for missing user', () => {
      UsersCache.internalStorage['no body'] = 'no data';

      const user = UsersCache.get(dummyUsername);

      expect(user).toBe(undefined);
    });

    it('returns matching user', () => {
      UsersCache.internalStorage[dummyUsername] = dummyUser;

      const user = UsersCache.get(dummyUsername);

      expect(user).toBe(dummyUser);
    });
  });

  describe('hasData', () => {
    it('returns false for empty cache', () => {
      expect(UsersCache.internalStorage).toEqual({});

      expect(UsersCache.hasData(dummyUsername)).toBe(false);
    });

    it('returns false for missing user', () => {
      UsersCache.internalStorage['no body'] = 'no data';

      expect(UsersCache.hasData(dummyUsername)).toBe(false);
    });

    it('returns true for matching user', () => {
      UsersCache.internalStorage[dummyUsername] = dummyUser;

      expect(UsersCache.hasData(dummyUsername)).toBe(true);
    });
  });

  describe('remove', () => {
    it('does nothing if cache is empty', () => {
      expect(UsersCache.internalStorage).toEqual({});

      UsersCache.remove(dummyUsername);

      expect(UsersCache.internalStorage).toEqual({});
    });

    it('does nothing if cache contains no matching data', () => {
      UsersCache.internalStorage['no body'] = 'no data';
      UsersCache.remove(dummyUsername);

      expect(UsersCache.internalStorage['no body']).toBe('no data');
    });

    it('removes matching data', () => {
      UsersCache.internalStorage[dummyUsername] = dummyUser;
      UsersCache.remove(dummyUsername);

      expect(UsersCache.internalStorage).toEqual({});
    });
  });

  describe('retrieve', () => {
    let apiSpy;

    beforeEach(() => {
      jest
        .spyOn(UserApi, 'getUsers')
        .mockImplementation((query, options) => apiSpy(query, options));
    });

    it('stores and returns data from API call if cache is empty', async () => {
      apiSpy = (query, options) => {
        expect(query).toBe('');
        expect(options).toEqual({
          username: dummyUsername,
        });

        return Promise.resolve({
          data: [dummyUser],
        });
      };

      const user = await UsersCache.retrieve(dummyUsername);
      expect(user).toBe(dummyUser);
      expect(UsersCache.internalStorage[dummyUsername]).toBe(dummyUser);
    });

    it('returns undefined if Ajax call fails and cache is empty', async () => {
      const dummyError = new Error('server exploded');

      apiSpy = (query, options) => {
        expect(query).toBe('');
        expect(options).toEqual({
          username: dummyUsername,
        });

        return Promise.reject(dummyError);
      };

      await expect(UsersCache.retrieve(dummyUsername)).rejects.toEqual(dummyError);
    });

    it('makes no Ajax call if matching data exists', async () => {
      UsersCache.internalStorage[dummyUsername] = dummyUser;

      apiSpy = () => {
        throw new Error('expected no Ajax call!');
      };

      const user = await UsersCache.retrieve(dummyUsername);
      expect(user).toBe(dummyUser);
    });
  });

  describe('retrieveById', () => {
    let apiSpy;

    beforeEach(() => {
      jest.spyOn(UserApi, 'getUser').mockImplementation((id) => apiSpy(id));
    });

    it('stores and returns data from API call if cache is empty', async () => {
      apiSpy = (id) => {
        expect(id).toBe(dummyUserId);

        return Promise.resolve({
          data: dummyUser,
        });
      };

      const user = await UsersCache.retrieveById(dummyUserId);
      expect(user).toBe(dummyUser);
      expect(UsersCache.internalStorage[dummyUserId]).toBe(dummyUser);
    });

    it('returns undefined if Ajax call fails and cache is empty', async () => {
      const dummyError = new Error('server exploded');

      apiSpy = (id) => {
        expect(id).toBe(dummyUserId);

        return Promise.reject(dummyError);
      };

      await expect(UsersCache.retrieveById(dummyUserId)).rejects.toEqual(dummyError);
    });

    it('makes no Ajax call if matching data exists', async () => {
      UsersCache.internalStorage[dummyUserId] = dummyUser;

      apiSpy = () => {
        throw new Error('expected no Ajax call!');
      };

      const user = await UsersCache.retrieveById(dummyUserId);
      expect(user).toBe(dummyUser);
    });
  });

  describe('retrieveStatusById', () => {
    let apiSpy;

    beforeEach(() => {
      jest.spyOn(UserApi, 'getUserStatus').mockImplementation((id) => apiSpy(id));
    });

    it('stores and returns data from API call if cache is empty', async () => {
      apiSpy = (id) => {
        expect(id).toBe(dummyUserId);

        return Promise.resolve({
          data: dummyUserStatus,
        });
      };

      const userStatus = await UsersCache.retrieveStatusById(dummyUserId);
      expect(userStatus).toBe(dummyUserStatus);
      expect(UsersCache.internalStorage[dummyUserId].status).toBe(dummyUserStatus);
    });

    it('returns undefined if Ajax call fails and cache is empty', async () => {
      const dummyError = new Error('server exploded');

      apiSpy = (id) => {
        expect(id).toBe(dummyUserId);

        return Promise.reject(dummyError);
      };

      await expect(UsersCache.retrieveStatusById(dummyUserId)).rejects.toEqual(dummyError);
    });

    it('makes no Ajax call if matching data exists', async () => {
      UsersCache.internalStorage[dummyUserId] = {
        status: dummyUserStatus,
      };

      apiSpy = () => {
        throw new Error('expected no Ajax call!');
      };

      const userStatus = await UsersCache.retrieveStatusById(dummyUserId);
      expect(userStatus).toBe(dummyUserStatus);
    });
  });

  describe('updateById', () => {
    describe('when the user is not cached', () => {
      it('does nothing and returns undefined', () => {
        expect(UsersCache.updateById(dummyUserId, { name: 'root' })).toBe(undefined);
        expect(UsersCache.internalStorage).toStrictEqual({});
      });
    });

    describe('when the user is cached', () => {
      const updatedName = 'has two farms';
      beforeEach(() => {
        UsersCache.internalStorage[dummyUserId] = dummyUser;
      });

      it('updates the user only with the new data', async () => {
        UsersCache.updateById(dummyUserId, { name: updatedName });

        expect(await UsersCache.retrieveById(dummyUserId)).toStrictEqual({
          username: dummyUser.username,
          name: updatedName,
        });
      });
    });
  });
});