import VueRouter from 'vue-router';
import waitForPromises from 'helpers/wait_for_promises';
import { createStore } from '~/ide/stores';
import { syncRouterAndStore } from '~/ide/sync_router_and_store';

const TEST_ROUTE = '/test/lorem/ipsum';

describe('~/ide/sync_router_and_store', () => {
  let unsync;
  let router;
  let store;
  let onRouterChange;

  const createSync = () => {
    unsync = syncRouterAndStore(router, store);
  };

  const getRouterCurrentPath = () => router.currentRoute.fullPath;
  const getStoreCurrentPath = () => store.state.router.fullPath;
  const updateRouter = async path => {
    if (getRouterCurrentPath() === path) {
      return;
    }

    router.push(path);
    await waitForPromises();
  };
  const updateStore = path => {
    store.dispatch('router/push', path);
    return waitForPromises();
  };

  beforeEach(() => {
    router = new VueRouter();
    store = createStore();
    jest.spyOn(store, 'dispatch');

    onRouterChange = jest.fn();
    router.beforeEach((to, from, next) => {
      onRouterChange(to, from);
      next();
    });
  });

  afterEach(() => {
    unsync();
    unsync = null;
  });

  it('keeps store and router in sync', async () => {
    createSync();

    await updateRouter('/test/test');
    await updateRouter('/test/test');
    await updateStore('123/abc');
    await updateRouter('def');

    // Even though we pused relative paths, the store and router kept track of the resulting fullPath
    expect(getRouterCurrentPath()).toBe('/test/123/def');
    expect(getStoreCurrentPath()).toBe('/test/123/def');
  });

  describe('default', () => {
    beforeEach(() => {
      createSync();
    });

    it('store is default', () => {
      expect(store.dispatch).not.toHaveBeenCalled();
      expect(getStoreCurrentPath()).toBe('');
    });

    it('router is default', () => {
      expect(onRouterChange).not.toHaveBeenCalled();
      expect(getRouterCurrentPath()).toBe('/');
    });

    describe('when store changes', () => {
      beforeEach(() => {
        updateStore(TEST_ROUTE);
      });

      it('store is updated', () => {
        // let's make sure the action isn't dispatched more than necessary
        expect(store.dispatch).toHaveBeenCalledTimes(1);
        expect(getStoreCurrentPath()).toBe(TEST_ROUTE);
      });

      it('router is updated', () => {
        expect(onRouterChange).toHaveBeenCalledTimes(1);
        expect(getRouterCurrentPath()).toBe(TEST_ROUTE);
      });

      describe('when store changes again to the same thing', () => {
        beforeEach(() => {
          onRouterChange.mockClear();
          updateStore(TEST_ROUTE);
        });

        it('doesnt change router again', () => {
          expect(onRouterChange).not.toHaveBeenCalled();
        });
      });
    });

    describe('when router changes', () => {
      beforeEach(() => {
        updateRouter(TEST_ROUTE);
      });

      it('store is updated', () => {
        expect(store.dispatch).toHaveBeenCalledTimes(1);
        expect(getStoreCurrentPath()).toBe(TEST_ROUTE);
      });

      it('router is updated', () => {
        // let's make sure the router change isn't triggered more than necessary
        expect(onRouterChange).toHaveBeenCalledTimes(1);
        expect(getRouterCurrentPath()).toBe(TEST_ROUTE);
      });

      describe('when router changes again to the same thing', () => {
        beforeEach(() => {
          store.dispatch.mockClear();
          updateRouter(TEST_ROUTE);
        });

        it('doesnt change store again', () => {
          expect(store.dispatch).not.toHaveBeenCalled();
        });
      });
    });

    describe('when disposed', () => {
      beforeEach(() => {
        unsync();
      });

      it('a store change does not trigger a router change', () => {
        updateStore(TEST_ROUTE);

        expect(getRouterCurrentPath()).toBe('/');
        expect(onRouterChange).not.toHaveBeenCalled();
      });

      it('a router change does not trigger a store change', () => {
        updateRouter(TEST_ROUTE);

        expect(getStoreCurrentPath()).toBe('');
        expect(store.dispatch).not.toHaveBeenCalled();
      });
    });
  });
});
