import {shallow} from 'enzyme';
import React from 'react';

import {
  DEFAULT_DASHBOARDS_CONTEXT, DashboardsContext,
} from 'src/context/DashboardsContext';
import * as HashLib from 'src/lib/hashlib';
import * as ServicesLib from 'src/lib/services';
import * as WebSocketLib from 'src/lib/websocket';
import {DashboardsProvider} from 'src/providers/DashboardsProvider';

import {
  DashboardsFactory, DashboardsJSONFactory,
} from 'tests/__fixtures__/dashboards';
import {FakeService, FakeWidget} from 'tests/__fixtures__/services';

describe('src/providers/DashboardsProvider', () => {
  describe('DashboardsProvider', () => {
    const Loader = (props) => <span>LOADER</span>; // eslint-disable-line no-unused-vars
    const Children = (props) => <span>LOADER</span>; // eslint-disable-line no-unused-vars
    let loadDashboards = null;
    let saveDashboards = null;
    let settings = null;

    beforeEach(() => {
      loadDashboards = jasmine.createSpy();
      saveDashboards = jasmine.createSpy();
      settings = {
        DEBUG: false,
        SERVICES: {
          [FakeService.kind]: FakeService,
        },
        WIDGETS: {
          [FakeService.widget]: FakeWidget,
        },
        WEBSOCKET: {
          url: '/backend/websocket',
        },
      };
    });

    describe('constructor', () => {
      it('initializes the instance', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // Then
        expect(component.instance().webSocket).toBe(null);
        expect(component.instance().webSocketUnsubscriber).toBe(null);
        expect(component.instance().dashboardsHash).toBe(null);
      });

      it('initializes the state', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // Then
        expect(component.state('currentDashboardId')).toEqual(
          DEFAULT_DASHBOARDS_CONTEXT.currentDashboardId
        );
        expect(component.state('dashboards')).toEqual(
          DEFAULT_DASHBOARDS_CONTEXT.dashboards
        );
        expect(component.state('isLoading')).toEqual(
          DEFAULT_DASHBOARDS_CONTEXT.isLoading
        );
        expect(component.state('lastSaveTimestamp')).toEqual(
          DEFAULT_DASHBOARDS_CONTEXT.lastSaveTimestamp
        );
        expect(component.state('lastSaveError')).toEqual(
          DEFAULT_DASHBOARDS_CONTEXT.lastSaveError
        );
        expect(component.state('isSaving')).toEqual(
          DEFAULT_DASHBOARDS_CONTEXT.isSaving
        );
        expect(component.state('isWebSocketConnected')).toEqual(
          DEFAULT_DASHBOARDS_CONTEXT.isWebSocketConnected
        );
        expect(component.state('loadError')).toBe(
          DEFAULT_DASHBOARDS_CONTEXT.loadError
        );
      });
    });

    describe('handleSaveError', () => {
      beforeEach(() => {
        spyOn(console, 'error');
      });

      it('handles a save error', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        const error = new Error('FIAL');

        // When
        const result = component.instance().handleSaveError(error);

        // Then
        expect(result).toEqual('FIAL');
        expect(console.error).toHaveBeenCalledWith(jasmine.any(String), error);
      });
    });

    describe('onWebSocketStart', () => {
      it('handles websocket start event', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // When
        component.instance().onWebSocketStart();

        // Then
        expect(component.state('isWebSocketConnected')).toBe(true);
      });
    });

    describe('onWebSocketStop', () => {
      it('handles websocket stop event', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        component.setState({isWebSocketConnected: true});

        // When
        component.instance().onWebSocketStop();

        // Then
        expect(component.state('isWebSocketConnected')).toBe(false);
      });
    });

    describe('onWebSocketMessage', () => {
      let fakeDate = null;

      beforeEach(() => {
        fakeDate = new Date(1987, 9, 3, 8, 0, 0);
        jasmine.clock().install();
        jasmine.clock().mockDate(fakeDate);
      });

      afterEach(() => {
        jasmine.clock().uninstall();
      });

      it('handles a service notification for a single service', async () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        const dashboards = DashboardsFactory();
        dashboards[0].services.forEach((service) => {
          spyOn(service, 'onMessage');
        });
        component.instance().setState({dashboards: dashboards});

        const data = {spam: true};

        // When
        await component.instance().onWebSocketMessage({
          type: WebSocketLib.kWebSocketMsgTypeServiceNotification,
          kind: FakeService.kind,
          instance: 'fake_instance',
          data: data,
        });

        // Then
        expect(dashboards[0].services[0].onMessage).toHaveBeenCalledWith(data);
        expect(dashboards[0].services[1].onMessage).not.toHaveBeenCalled();
      });

      it('handles a service notification for multiple services', async () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        const dashboards = DashboardsFactory();
        dashboards[0].services.forEach((service) => {
          spyOn(service, 'onMessage');
        });
        component.instance().setState({dashboards: dashboards});

        const data = {spam: true};

        // When
        await component.instance().onWebSocketMessage({
          type: WebSocketLib.kWebSocketMsgTypeServiceNotification,
          kind: FakeService.kind,
          data: data,
        });

        // Then
        expect(dashboards[0].services[0].onMessage).toHaveBeenCalledWith(data);
        expect(dashboards[0].services[1].onMessage).toHaveBeenCalledWith(data);
      });

      it('handles a state notification', async () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        const dashboards = DashboardsFactory();
        const data = DashboardsJSONFactory(dashboards);

        spyOn(component.instance(), 'handleLoadedState').and.resolveTo(dashboards);

        // When
        await component.instance().onWebSocketMessage({
          type: WebSocketLib.kWebSocketMsgTypeStateNotification,
          data: data,
        });

        // Then
        expect(component.state('dashboards')).toEqual(dashboards);
        expect(component.state('lastSaveTimestamp')).toEqual(fakeDate);
        expect(component.instance().handleLoadedState).toHaveBeenCalledWith(data);
      });

      it('does not handle a state notification if the dashboards hash did not change', async () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        const dashboards = DashboardsFactory();
        const data = DashboardsJSONFactory(dashboards);
        component.instance().dashboardsHash = await HashLib.sha256(
          JSON.stringify(data)
        );

        spyOn(component.instance(), 'handleLoadedState').and.resolveTo(dashboards);

        // When
        await component.instance().onWebSocketMessage({
          type: WebSocketLib.kWebSocketMsgTypeStateNotification,
          data: data,
        });

        // Then
        expect(component.state('dashboards')).not.toEqual(dashboards);
        expect(component.state('lastSaveTimestamp')).not.toEqual(fakeDate);
        expect(component.instance().handleLoadedState).not.toHaveBeenCalled();
      });
    });

    describe('currentDashboard', () => {
      it('returns the current dashboard object', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        const dashboards = DashboardsFactory();
        component.setState({
          dashboards: dashboards,
          currentDashboardId: 'testing',
        });

        // When
        const result = component.instance().currentDashboard();

        // Then
        expect(result).toEqual(dashboards[0]);
      });
    });

    describe('mutateCurrentDashboardServices', () => {
      it('returns the mutated current dashboard object', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        const dashboards = DashboardsFactory();
        dashboards.push({
          id: 'testing2',
          name: 'Testing2',
          services: [
            new FakeService({
              instance: 'other_dashboard_instance',
              widgetComponent: FakeWidget,
              characteristics: {
                spam: true,
              },
              layout: {
                x: 0,
                y: 0,
                w: 1,
                h: 1,
              },
            }),
          ],
        });
        component.setState({
          dashboards: dashboards,
          currentDashboardId: 'testing',
        });

        // When
        const result = component.instance().mutateCurrentDashboardServices(
          (services) => {
            return services.map((service) => {
              service.setCharacteristics({id: service.instance});
              return service;
            });
          }
        );

        // Then
        expect(result.length).toEqual(2);
        expect(result[0].services[0].characteristics).toEqual({
          id: result[0].services[0].instance,
        });
        expect(result[0].services[1].characteristics).toEqual({
          id: result[0].services[1].instance,
        });
        expect(result[1].services[0].characteristics).toEqual({
          spam: true,
        });
      });
    });

    describe('serviceFromSpec', () => {
      let spec = null;

      beforeEach(() => {
        spec = {
          kind: 'FakeService',
          instance: 'fake_instance',
          characteristics: {
            spam: true,
          },
          layout: {
            x: 0,
            y: 0,
            w: 1,
            h: 1,
          },
        };
      });

      it('creates a dummy service is the service is uknown', () => {
        // Given
        settings.SERVICES = {};
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // When
        const result = component.instance().serviceFromSpec(spec);

        // Then
        expect(result).toBeInstanceOf(ServicesLib.DummyService);
      });

      it('instantiates a service from the spec', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // When
        const result = component.instance().serviceFromSpec(spec);

        // Then
        expect(result).toBeInstanceOf(FakeService);
        expect(result.instance).toEqual(spec.instance);
        expect(result.characteristics).toEqual(spec.characteristics);
        expect(result.layout).toEqual(spec.layout);
        expect(result.widgetComponent).toEqual(FakeWidget);
      });

      it('nullifies the widgetComponent if the widget is not known', () => {
        // Given
        settings.WIDGETS = {};
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // When
        const result = component.instance().serviceFromSpec(spec);

        // Then
        expect(result).toBeInstanceOf(FakeService);
        expect(result.widgetComponent).toBe(null);
      });
    });

    describe('saveDashboards', () => {
      let fakeDate = null;

      beforeEach(() => {
        saveDashboards = jasmine.createSpy().and.resolveTo('ok');

        fakeDate = new Date(1987, 9, 3, 8, 0, 0);
        jasmine.clock().install();
        jasmine.clock().mockDate(fakeDate);

        spyOn(console, 'error');
      });

      afterEach(() => {
        jasmine.clock().uninstall();
      });

      it('does not save the dashboards when dashboards hash did not change', async () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        component.setState({lastSaveError: 'FIAL'});

        const dashboards = DashboardsFactory();
        const data = DashboardsJSONFactory(dashboards);

        component.instance().dashboardsHash = await HashLib.sha256(
          JSON.stringify({dashboards: data})
        );

        // When
        await component.instance().saveDashboards(dashboards);

        // Then
        expect(saveDashboards).not.toHaveBeenCalled();
        expect(component.state('lastSaveTimestamp')).not.toEqual(fakeDate);
        expect(component.state('lastSaveError')).toEqual('FIAL');
      });

      it('saves the dashboards', async () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        component.setState({lastSaveError: 'FIAL'});

        const dashboards = DashboardsFactory();

        // When
        await component.instance().saveDashboards(dashboards);

        // Then
        expect(saveDashboards).toHaveBeenCalled();
        const savedDashboards = saveDashboards.calls.first().args[0];
        expect(savedDashboards.dashboards.length).toEqual(1);
        expect(savedDashboards.dashboards[0].id).toEqual(dashboards[0].id);
        expect(savedDashboards.dashboards[0].name).toEqual(dashboards[0].name);
        expect(savedDashboards.dashboards[0].services.length).toEqual(2);
        expect(savedDashboards.dashboards[0].services[0]).toEqual(
          dashboards[0].services[0].toJSON()
        );
        expect(savedDashboards.dashboards[0].services[1]).toEqual(
          dashboards[0].services[1].toJSON()
        );

        expect(component.state('lastSaveTimestamp')).toEqual(fakeDate);
        expect(component.state('lastSaveError')).toBe(null);
      });

      it('gracefully handles save error', async () => {
        // Given
        saveDashboards = jasmine.createSpy().and.rejectWith(new Error('FIAL'));
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        const dashboards = DashboardsFactory();

        component.instance().dashboardsHash = 'spam';
        component.setState({lastSaveTimestamp: fakeDate});

        // When
        await component.instance().saveDashboards(dashboards);

        // Then
        expect(component.instance().dashboardsHash).toBe(null);
        expect(component.state('lastSaveTimestamp')).toBe(null);
        expect(component.state('lastSaveError')).toEqual('FIAL');
      });
    });

    describe('nukeService', () => {
      it('stops and removes the specified service', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'saveDashboards');

        const dashboards = DashboardsFactory();
        const serviceToRemove = dashboards[0].services[0];
        spyOn(serviceToRemove, 'stop');

        component.setState({
          dashboards: dashboards,
          currentDashboardId: dashboards[0].id,
        });

        // When
        component.instance().nukeService('FakeService', 'fake_instance');

        // Then
        const newDashboards = component.state('dashboards');
        expect(newDashboards[0].services.length).toEqual(1);
        expect(newDashboards[0].services).not.toContain(serviceToRemove);

        expect(serviceToRemove.stop).toHaveBeenCalled();
        expect(component.instance().saveDashboards).toHaveBeenCalledWith(
          newDashboards
        );
      });
    });

    describe('saveServiceCharacteristics', () => {
      it('saves new characteristics for the specified service', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'saveDashboards');

        const dashboards = DashboardsFactory();
        const serviceToModify = dashboards[0].services[0];
        spyOn(serviceToModify, 'setCharacteristics');

        component.setState({
          dashboards: dashboards,
          currentDashboardId: dashboards[0].id,
        });

        const newCharacteristics = {spam: false};

        // When
        component.instance().saveServiceCharacteristics(
          'FakeService', 'fake_instance', newCharacteristics
        );

        // Then
        const newDashboards = component.state('dashboards');
        expect(newDashboards[0].services.length).toEqual(2);

        expect(serviceToModify.setCharacteristics).toHaveBeenCalledWith(
          newCharacteristics
        );
        expect(component.instance().saveDashboards).toHaveBeenCalledWith(
          newDashboards
        );
      });
    });

    describe('saveServiceLayout', () => {
      it('saves new characteristics for the specified service', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'saveDashboards');

        const dashboards = DashboardsFactory();
        const serviceToModify = dashboards[0].services[0];
        spyOn(serviceToModify, 'setLayout');

        component.setState({
          dashboards: dashboards,
          currentDashboardId: dashboards[0].id,
        });

        const newLayout = {x: 1, y: 1, w: 1, h: 1};

        // When
        component.instance().saveServiceLayout('fake_instance', newLayout);

        // Then
        const newDashboards = component.state('dashboards');
        expect(newDashboards[0].services.length).toEqual(2);

        expect(serviceToModify.setLayout).toHaveBeenCalledWith(newLayout);
        expect(component.instance().saveDashboards).toHaveBeenCalledWith(
          newDashboards
        );
      });
    });

    describe('addService', () => {
      it('is a noop if no dashboard is selected', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'saveDashboards');

        const dashboards = DashboardsFactory();
        component.setState({dashboards: dashboards});

        // When
        component.instance().addService('FakeService', {new: true});

        // Then
        expect(component.state('dashboards')).toBe(dashboards);
        expect(component.instance().saveDashboards).not.toHaveBeenCalled();
      });

      it('adds a new service', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'saveDashboards');

        const dashboards = DashboardsFactory();
        component.setState({
          dashboards: dashboards,
          currentDashboardId: dashboards[0].id,
        });

        const newServiceCharacteristics = {new: true};

        // When
        component.instance().addService(
          'FakeService', newServiceCharacteristics
        );

        // Then
        const newDashboards = component.state('dashboards');
        expect(newDashboards[0].services.length).toEqual(3);
        expect(component.instance().saveDashboards).toHaveBeenCalledWith(
          newDashboards
        );

        const newService = newDashboards[0].services[2];
        expect(newService).toBeInstanceOf(FakeService);
        expect(newService.instance).toBeUUIDv4();
        expect(newService.characteristics).toEqual(newServiceCharacteristics);
        expect(newService.layout).toEqual({x: 0, y: 2, w: 1, h: 1});
      });
    });

    describe('handleLoadedState', () => {
      it('handles loaded state', async () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'serviceFromSpec').and.returnValue(
          new ServicesLib.DummyService()
        );

        const loadedState = {dashboards: DashboardsJSONFactory()};
        const dashboardsHash = await HashLib.sha256(
          JSON.stringify(loadedState)
        );

        // When
        const result = await component.instance().handleLoadedState(
          loadedState
        );

        // Then
        expect(component.instance().dashboardsHash).toEqual(dashboardsHash);
        expect(result.length).toEqual(1);
        expect(result[0].id).toEqual(loadedState.dashboards[0].id);
        expect(result[0].name).toEqual(loadedState.dashboards[0].name);
        expect(result[0].services.length).toEqual(
          loadedState.dashboards[0].services.length
        );
        expect(component.instance().serviceFromSpec.calls.count()).toEqual(2);
        expect(component.instance().serviceFromSpec).toHaveBeenCalledWith(
          loadedState.dashboards[0].services[0]
        );
        expect(component.instance().serviceFromSpec).toHaveBeenCalledWith(
          loadedState.dashboards[0].services[1]
        );
      });
    });

    describe('loadDashboards', () => {
      let fakeDate = null;
      let loadedDashboards = null;
      let loadedState = null;

      beforeEach(() => {
        loadedDashboards = DashboardsFactory();
        loadedState = {dashboards: DashboardsJSONFactory(loadedDashboards)};
        loadDashboards = jasmine.createSpy().and.resolveTo(loadedState);

        fakeDate = new Date(1987, 9, 3, 8, 0, 0);
        jasmine.clock().install();
        jasmine.clock().mockDate(fakeDate);

        spyOn(console, 'error');
      });

      afterEach(() => {
        jasmine.clock().uninstall();
      });

      it('loads the dashboards', async () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'handleLoadedState').and.resolveTo(
          loadedDashboards
        );

        // When
        await component.instance().loadDashboards();

        // Then
        expect(component.state('dashboards')).toEqual(loadedDashboards);
        expect(component.state('currentDashboardId')).toEqual(
          loadedDashboards[0].id
        );
        expect(component.state('lastSaveTimestamp')).toEqual(fakeDate);
        expect(component.instance().handleLoadedState).toHaveBeenCalledWith(
          loadedState
        );
      });

      it('gracefully handles load error', async () => {
        // Given
        loadDashboards = jasmine.createSpy().and.rejectWith(new Error('FIAL'));
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'handleLoadedState').and.resolveTo([]);
        spyOn(component.instance(), 'throwLoadError');

        // When
        await component.instance().loadDashboards();

        // Then
        expect(component.state('dashboards')).toEqual([]);
        expect(component.state('currentDashboardId')).toBe(null);
        expect(component.state('lastSaveTimestamp')).toBe(null);
        expect(component.state('loadError')).toEqual('FIAL');
        expect(component.instance().handleLoadedState).not.toHaveBeenCalled();
        expect(component.instance().throwLoadError).toHaveBeenCalledWith(
          component.state('loadError'),
        );
      });
    });

    describe('setCurrentDashboardId', () => {
      it('sets the current dashboard id', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // When
        component.instance().setCurrentDashboardId('spam');

        // Then
        expect(component.state('currentDashboardId')).toEqual('spam');
      });
    });

    describe('addDashboard', () => {
      it('adds a dashboard', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'saveDashboards');

        // When
        component.instance().addDashboard('New');

        // Then
        const newDashboards = component.state('dashboards');
        expect(newDashboards.length).toEqual(1);
        expect(newDashboards[0].name).toEqual('New');
        expect(component.instance().saveDashboards).toHaveBeenCalledWith(
          newDashboards
        );
      });

      it('auto selects the first added dashboard', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        component.setState({dashboards: []});

        // When
        component.instance().addDashboard('New');

        // Then
        expect(component.state('currentDashboardId')).toEqual(
          component.state('dashboards')[0].id
        );
      });

      it('does not auto select the other added dashboard', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        const dashboards = DashboardsFactory();
        component.setState({
          dashboards: dashboards,
          currentDashboardId: 'testing',
        });

        // When
        component.instance().addDashboard('New');

        // Then
        expect(component.state('currentDashboardId')).toEqual('testing');
      });
    });

    describe('throwLoadError', () => {
      it('throws the error as is if it is an Error', () => {
        // Given
        const error = new Error('FIAL');

        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // When
        expect(() => component.instance().throwLoadError(error)).toThrow(error);
      });

      it('wraps a string in an error and throws it', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // When
        expect(() => component.instance().throwLoadError('FIAL')).toThrowError(
          Error, 'FIAL',
        );
      });
    });

    describe('componentDidMount', () => {
      let fakeHomeHubWebSocket = null;
      let mockWebSocketUnsubscriber = null;

      beforeEach(() => {
        fakeHomeHubWebSocket = jasmine.createSpyObj('HomeHubWebSocket', [
          'start', 'stop', 'addEventListener', 'removeEventListener',
          'subscribe',
        ]);
        mockWebSocketUnsubscriber = jasmine.createSpy();
        fakeHomeHubWebSocket.subscribe.and.returnValue(
          mockWebSocketUnsubscriber
        );
        spyOn(WebSocketLib, 'HomeHubWebSocket').and.returnValue(
          fakeHomeHubWebSocket
        );
      });

      it('sets the isLoading state to true', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'loadDashboards');

        // When
        component.instance().componentDidMount();

        // Then
        expect(component.state('isLoading')).toBe(true);
      });

      it('configures and starts the websocket connection', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'loadDashboards');

        // When
        component.instance().componentDidMount();

        // Then
        expect(component.instance().webSocket).toEqual(fakeHomeHubWebSocket);
        expect(WebSocketLib.HomeHubWebSocket).toHaveBeenCalledWith(
          settings.DEBUG, settings.WEBSOCKET
        );
        expect(fakeHomeHubWebSocket.addEventListener).toHaveBeenCalledWith(
          'start', component.instance().onWebSocketStart
        );
        expect(fakeHomeHubWebSocket.addEventListener).toHaveBeenCalledWith(
          'stop', component.instance().onWebSocketStop
        );
        expect(component.instance().webSocketUnsubscriber).toEqual(
          mockWebSocketUnsubscriber
        );
        expect(fakeHomeHubWebSocket.subscribe).toHaveBeenCalledWith(
          component.instance().onWebSocketMessage
        );
        expect(fakeHomeHubWebSocket.start).toHaveBeenCalled();
      });

      it('configures and starts the websocket connection using custom connector', () => {
        // Given
        settings.CONNECTOR = jasmine.createSpy().and.returnValue(
          fakeHomeHubWebSocket
        );

        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'loadDashboards');

        // When
        component.instance().componentDidMount();

        // Then
        expect(component.instance().webSocket).toEqual(fakeHomeHubWebSocket);
        expect(WebSocketLib.HomeHubWebSocket).not.toHaveBeenCalled();
        expect(settings.CONNECTOR).toHaveBeenCalledWith(
          settings.DEBUG, settings.WEBSOCKET
        );
        expect(fakeHomeHubWebSocket.addEventListener).toHaveBeenCalledWith(
          'start', component.instance().onWebSocketStart
        );
        expect(fakeHomeHubWebSocket.addEventListener).toHaveBeenCalledWith(
          'stop', component.instance().onWebSocketStop
        );
        expect(component.instance().webSocketUnsubscriber).toEqual(
          mockWebSocketUnsubscriber
        );
        expect(fakeHomeHubWebSocket.subscribe).toHaveBeenCalledWith(
          component.instance().onWebSocketMessage
        );
        expect(fakeHomeHubWebSocket.start).toHaveBeenCalled();
      });

      it('does not start the websocket in offline mode', () => {
        // Given
        settings.OFFLINE_MODE = true;
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'loadDashboards');

        // When
        component.instance().componentDidMount();

        // Then
        expect(component.instance().webSocket).toEqual(fakeHomeHubWebSocket);
        expect(fakeHomeHubWebSocket.start).not.toHaveBeenCalled();
      });

      it('loads the dashboards', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        spyOn(component.instance(), 'loadDashboards');

        // When
        component.instance().componentDidMount();

        // Then
        expect(component.instance().loadDashboards).toHaveBeenCalled();
      });
    });

    describe('componentWillUnmount', () => {
      let fakeHomeHubWebSocket = null;
      let mockWebSocketUnsubscriber = null;

      beforeEach(() => {
        fakeHomeHubWebSocket = jasmine.createSpyObj('HomeHubWebSocket', [
          'start', 'stop', 'addEventListener', 'removeEventListener',
          'subscribe',
        ]);
        mockWebSocketUnsubscriber = jasmine.createSpy();
      });

      it('unsubscribes from the websocket', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        component.instance().webSocketUnsubscriber = mockWebSocketUnsubscriber;

        // When
        component.instance().componentWillUnmount();

        // Then
        expect(component.instance().webSocketUnsubscriber).toBe(null);
        expect(mockWebSocketUnsubscriber).toHaveBeenCalled();
      });

      it('deconfigures and stops the websocket connection', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        component.instance().webSocket = fakeHomeHubWebSocket;

        // When
        component.instance().componentWillUnmount();

        // Then
        expect(fakeHomeHubWebSocket.removeEventListener).toHaveBeenCalledWith(
          'start', component.instance().onWebSocketStart
        );
        expect(fakeHomeHubWebSocket.removeEventListener).toHaveBeenCalledWith(
          'stop', component.instance().onWebSocketStop
        );
        expect(fakeHomeHubWebSocket.stop).toHaveBeenCalled();
      });
    });

    describe('render', () => {
      it('configures and renders the DashboardsContext provider', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );

        // When
        const provider = component.find(DashboardsContext.Provider);

        // Then
        expect(provider.exists());
        expect(provider.prop('value')).toEqual({
          currentDashboardId: component.state('currentDashboardId'),
          dashboards: component.state('dashboards'),
          nukeService: component.instance().nukeService,
          saveServiceCharacteristics: component.instance().saveServiceCharacteristics,
          saveServiceLayout: component.instance().saveServiceLayout,
          addService: component.instance().addService,
          isLoading: component.state('isLoading'),
          lastSaveTimestamp: component.state('lastSaveTimestamp'),
          lastSaveError: component.state('lastSaveError'),
          isSaving: component.state('isSaving'),
          isWebSocketConnected: component.state('isWebSocketConnected'),
          setCurrentDashboardId: component.instance().setCurrentDashboardId,
          dashboardsHash: component.instance().dashboardsHash,
          addDashboard: component.instance().addDashboard,
          loadError: component.state('loadError'),
        });
      });

      it('renders the Loader if it is loading dashboards', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        component.setState({isLoading: true});

        // Then
        expect(component.find(Loader).exists()).toBe(true);
        expect(component.find(Children).exists()).toBe(false);
      });

      it('renders the Children if it is not loading', () => {
        // Given
        const component = shallow(
          <DashboardsProvider
            loader={<Loader />}
            loadDashboards={loadDashboards}
            saveDashboards={saveDashboards}
            settings={settings}
          >
            <Children />
          </DashboardsProvider>
        );
        component.setState({isLoading: false});

        // Then
        expect(component.find(Loader).exists()).toBe(false);
        expect(component.find(Children).exists()).toBe(true);
      });
    });
  });
});
