'use strict';

const providerFactory = require('../provider-factory');

const providerMethods = [
  'register',
  'delete',
  'get',
  'getWhere',
  'values',
  'keys',
  'has',
  'size',
  'clear',
];

describe('Provider Factory', () => {
  describe('Core', () => {
    test('Can create a default provider', () => {
      const provider = providerFactory();

      expect(provider).toHaveProperty('hooks', expect.any(Object));

      providerMethods.forEach(methodName =>
        expect(provider).toHaveProperty(methodName, expect.any(Function))
      );
    });
  });

  describe('Hooks', () => {
    test.each(['willRegister'])(
      'AsyncSeries: %s hook can be triggered and mutate the given context',
      async hookName => {
        const provider = providerFactory();
        const ctx = { bar: 'foo' };

        const handler = jest.fn(context => (context.foo = 'bar'));

        provider.hooks[hookName].register(handler);

        await provider.hooks[hookName].call(ctx);

        expect(handler).toHaveBeenCalled();
        expect(ctx).toEqual({ bar: 'foo', foo: 'bar' });
      }
    );

    test.each(['didRegister', 'willDelete', 'didDelete'])(
      'AsyncParallel: %s hook can be triggered, but cannot mutate the given context',
      async hookName => {
        const provider = providerFactory();
        const ctx = { bar: 'foo' };

        const handler = jest.fn(context => {
          context.foo = 'bar';
          return context;
        });

        provider.hooks[hookName].register(handler);

        const results = await provider.hooks[hookName].call(ctx);

        expect(handler).toHaveBeenCalled();
        expect(results).toEqual([{ foo: 'bar', bar: 'foo' }]);
        expect(ctx).toEqual({ bar: 'foo' });
      }
    );
  });

  describe('Methods', () => {
    describe('Register', () => {
      test('Can register a new item', async () => {
        const provider = providerFactory();
        const key = 'key';
        const item = { foo: 'bar' };

        await provider.register(key, item);

        expect(provider.get(key)).toEqual(item);
      });

      test(`Can't register duplicated key by default`, async () => {
        const provider = providerFactory();

        const key = 'key';
        const itemA = { foo: 'bar' };
        const itemB = { bar: 'foo' };

        await provider.register(key, itemA);

        await expect(provider.register(key, itemB)).rejects.toThrowError(
          'Duplicated item key: key'
        );
      });

      test('Can register duplicated key when "throwOnDuplicates" is false', async () => {
        const provider = providerFactory({ throwOnDuplicates: false });
        const key = 'key';
        const itemA = { foo: 'bar' };
        const itemB = { bar: 'foo' };

        await provider.register(key, itemA);

        expect(provider.get(key)).toEqual(itemA);

        await provider.register(key, itemB);

        expect(provider.get(key)).toEqual(itemB);
      });

      test('Register hooks are triggered on item registration', async () => {
        const willRegister = jest.fn(({ value }) => (value.bar = 'foo'));
        const didRegister = jest.fn();

        const key = 'key';
        const item = { foo: 'bar' };

        const provider = providerFactory();

        provider.hooks.willRegister.register(willRegister);
        provider.hooks.didRegister.register(didRegister);

        await provider.register(key, item);

        expect(willRegister).toHaveBeenCalledWith({ key, value: item });
        expect(didRegister).toHaveBeenCalledWith({ key, value: item });
        expect(item).toMatchObject({ foo: 'bar', bar: 'foo' });
      });
    });

    describe('Delete', () => {
      const key = 'key';
      const item = { foo: 'bar' };

      test('Can delete a registered item by its key', async () => {
        const provider = providerFactory();

        await provider.register(key, item);

        expect(provider.get(key)).toBe(item);

        await provider.delete(key, item);

        expect(provider.get(key)).toBeUndefined();
      });

      test('Trying to delete an unregistered item does nothing', async () => {
        const provider = providerFactory();

        expect(provider.get(key)).toBeUndefined();

        await provider.delete(key);

        expect(provider.get(key)).toBeUndefined();

        await provider.register(key, item);

        expect(provider.get(key)).toBe(item);

        await provider.delete(`${key}.${key}`);

        expect(provider.get(key)).toBe(item);
      });

      test('Register hooks are triggered on item registration', async () => {
        const willDelete = jest.fn();
        const didDelete = jest.fn();

        const provider = providerFactory();

        provider.hooks.willDelete.register(willDelete);
        provider.hooks.didDelete.register(didDelete);

        await provider.register(key, item);
        await provider.delete(key);

        expect(willDelete).toHaveBeenCalledWith({ key, value: item });
        expect(didDelete).toHaveBeenCalledWith({ key, value: item });
        expect(item).toEqual({ foo: 'bar' });
      });
    });

    describe('Get', () => {
      test('calling get on an existing key returns the registered item', async () => {
        const provider = providerFactory();
        const key = 'key';
        const item = { foo: 'bar' };

        await provider.register(key, item);

        const result = provider.get(key);

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

      test('Calling get on a non-existing key returns undefined', () => {
        const provider = providerFactory();

        const result = provider.get('key');

        expect(result).toBeUndefined();
      });
    });

    describe('GetWhere', () => {
      const items = [
        { key: 'keyA', value: { foo: 'barA', bar: 'foo1' } },
        { key: 'keyB', value: { foo: 'barB', bar: 'foo2' } },
        { key: 'keyC', value: { foo: 'barC', bar: 'foo1' } },
        { key: 'keyD', value: { foo: 'barD', bar: 'foo2' } },
      ];
      const provider = providerFactory();

      const sortItems = (a, b) => (a.key < b.key ? -1 : 1);
      const pickItems = (...indexes) => indexes.map(index => items[index].value);

      beforeAll(async () => {
        for (const item of items) {
          await provider.register(item.key, item.value);
        }
      });

      test('Calling getWhere without filters returns every registered items', async () => {
        const expected = items.map(i => i.value).sort(sortItems);

        const results = provider.getWhere();

        expect(results).toStrictEqual(expect.any(Array));
        expect(results).toHaveLength(items.length);
        expect(results.sort(sortItems)).toEqual(expected);
      });

      test.each([
        [{ foo: 'barA' }, pickItems(0)],
        [{ bar: 'foo1' }, pickItems(0, 2)],
        [{ bar: 'foo2' }, pickItems(1, 3)],
        [{ foo: 'barD', bar: 'foo2' }, pickItems(3)],
        [{ foo: 'barC', bar: 'foo2' }, []],
        [{ foobar: 'foobar' }, []],
        [{}, pickItems(0, 1, 2, 3)],
      ])('Filters %s', (filters, expected) => {
        const results = provider.getWhere(filters);

        expect(results).toStrictEqual(expect.any(Array));
        expect(results).toStrictEqual(expected);
      });
    });

    describe('Values', () => {
      test('Returns an empty array when there is no registered item', async () => {
        const provider = providerFactory();

        const values = provider.values();

        expect(values).toStrictEqual(expect.any(Array));
        expect(values).toHaveLength(0);
      });

      test('Returns every registered item (only the value, not the key)', async () => {
        const provider = providerFactory();
        const items = [
          { key: 'keyA', value: { foo: 'barA', bar: 'foo1' } },
          { key: 'keyB', value: { foo: 'barB', bar: 'foo2' } },
          { key: 'keyC', value: { foo: 'barC', bar: 'foo1' } },
          { key: 'keyD', value: { foo: 'barD', bar: 'foo2' } },
        ];

        for (const item of items) {
          await provider.register(item.key, item.value);
        }

        const values = provider.values();

        expect(values).toStrictEqual(items.map(item => item.value));
      });
    });

    describe('Keys', () => {
      test('Returns an empty array when there is no registered item', async () => {
        const provider = providerFactory();

        const keys = provider.keys();

        expect(keys).toStrictEqual(expect.any(Array));
        expect(keys).toHaveLength(0);
      });

      test('Returns every registered item (only the key, not the value)', async () => {
        const provider = providerFactory();
        const items = [
          { key: 'keyA', value: { foo: 'barA', bar: 'foo1' } },
          { key: 'keyB', value: { foo: 'barB', bar: 'foo2' } },
          { key: 'keyC', value: { foo: 'barC', bar: 'foo1' } },
          { key: 'keyD', value: { foo: 'barD', bar: 'foo2' } },
        ];

        for (const item of items) {
          await provider.register(item.key, item.value);
        }

        const keys = provider.keys();

        expect(keys).toStrictEqual(items.map(item => item.key));
      });
    });

    describe('Has', () => {
      const provider = providerFactory();
      const key = 'key';
      const item = { foo: 'bar' };

      beforeAll(async () => {
        await provider.register(key, item);
      });

      test.each([
        [key, true],
        ['foo', false],
      ])('Has %s => %s', (key, expected) => {
        const result = provider.has(key);

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

    describe('Size', () => {
      test.each([
        ['No item', []],
        ['One item', [{ key: 'key', value: { foo: 'bar' } }]],
        [
          'Multiple items',
          [
            { key: 'keyA', value: { foo: 'bar' } },
            { key: 'keyB', value: { foo: 'bar' } },
            { key: 'keyC', value: { foo: 'bar' } },
          ],
        ],
      ])('%s', async (name, items) => {
        const provider = providerFactory();

        for (const item of items) {
          await provider.register(item.key, item.value);
        }

        const size = provider.size();

        expect(size).toBe(items.length);
      });
    });

    describe('Clear', () => {
      test('Does nothing when there is no registered item', async () => {
        const provider = providerFactory();

        expect(provider.size()).toBe(0);

        await provider.clear();

        expect(provider.size()).toBe(0);
      });

      test('Delete every registered item', async () => {
        const provider = providerFactory();
        const items = [
          { key: 'keyA', value: { foo: 'barA' } },
          { key: 'keyB', value: { foo: 'barB' } },
          { key: 'keyC', value: { foo: 'barC' } },
        ];

        for (const item of items) {
          await provider.register(item.key, item.value);
        }

        expect(provider.size()).toBe(items.length);

        await provider.clear();

        expect(provider.size()).toBe(0);
      });

      test('Delete hooks are triggered for every item on clear', async () => {
        const willDelete = jest.fn();
        const didDelete = jest.fn();

        const provider = providerFactory();

        provider.hooks.willDelete.register(willDelete);
        provider.hooks.didDelete.register(didDelete);

        const items = [
          { key: 'keyA', value: { foo: 'barA' } },
          { key: 'keyB', value: { foo: 'barB' } },
          { key: 'keyC', value: { foo: 'barC' } },
        ];

        for (const item of items) {
          await provider.register(item.key, item.value);
        }

        expect(provider.size()).toBe(items.length);

        await provider.clear();

        expect(provider.size()).toBe(0);

        expect(willDelete).toHaveBeenCalledTimes(items.length);
        expect(didDelete).toHaveBeenCalledTimes(items.length);
      });
    });
  });
});
