import Vue from 'vue';

import MockAdapter from 'axios-mock-adapter';
import appComponent from 'ee/geo_nodes/components/app.vue';
import eventHub from 'ee/geo_nodes/event_hub';
import GeoNodesStore from 'ee/geo_nodes/store/geo_nodes_store';
import GeoNodesService from 'ee/geo_nodes/service/geo_nodes_service';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { NODE_ACTIONS } from 'ee/geo_nodes/constants';
import axios from '~/lib/utils/axios_utils';
import {
  PRIMARY_VERSION,
  NODE_DETAILS_PATH,
  mockNodes,
  mockNode,
  rawMockNodeDetails,
} from '../mock_data';

const createComponent = () => {
  const Component = Vue.extend(appComponent);
  const store = new GeoNodesStore(PRIMARY_VERSION.version, PRIMARY_VERSION.revision);
  const service = new GeoNodesService(NODE_DETAILS_PATH);

  return mountComponent(Component, {
    store,
    service,
    nodeActionsAllowed: true,
    nodeEditAllowed: true,
    geoTroubleshootingHelpPath: '/foo/bar',
  });
};

describe('AppComponent', () => {
  let vm;
  let mock;
  let statusCode;
  let response;

  beforeEach(() => {
    statusCode = 200;
    response = mockNodes;

    mock = new MockAdapter(axios);

    document.body.innerHTML += '<div class="flash-container"></div>';
    mock.onGet(/(.*)\/geo_nodes$/).reply(() => [statusCode, response]);
    vm = createComponent();
  });

  afterEach(() => {
    document.querySelector('.flash-container').remove();
    vm.$destroy();
    mock.restore();
  });

  describe('data', () => {
    it('returns default data props', () => {
      expect(vm.isLoading).toBe(true);
      expect(vm.hasError).toBe(false);
      expect(vm.targetNode).toBeNull();
      expect(vm.targetNodeActionType).toBe('');
      expect(vm.modalKind).toBe('warning');
      expect(vm.modalMessage).toBe('');
      expect(vm.modalActionLabel).toBe('');
      expect(vm.modalTitle).toBe('');
      expect(vm.modalId).toBe('node-action');
    });
  });

  describe('computed', () => {
    describe('nodes', () => {
      it('returns list of nodes from store', () => {
        expect(Array.isArray(vm.nodes)).toBeTruthy();
      });
    });
  });

  describe('methods', () => {
    describe('setNodeActionStatus', () => {
      it('sets `nodeActionActive` property with value of `status` parameter for provided `node` parameter', () => {
        const node = {
          nodeActionActive: false,
        };
        vm.setNodeActionStatus(node, true);

        expect(node.nodeActionActive).toBe(true);
      });
    });

    describe('initNodeDetailsPolling', () => {
      it('initializes SmartInterval and sets it to component', () => {
        vm.initNodeDetailsPolling(2);

        expect(vm.nodePollingInterval).toBeDefined();
      });
    });

    describe('fetchGeoNodes', () => {
      it('calls service.getGeoNodes and sets response to the store on success', done => {
        spyOn(vm.store, 'setNodes');

        vm.fetchGeoNodes()
          .then(() => {
            expect(vm.store.setNodes).toHaveBeenCalledWith(mockNodes);
            expect(vm.isLoading).toBe(false);
          })
          .then(done)
          .catch(done.fail);
      });

      it('sets error flag and message on failure', done => {
        response = 'Something went wrong';
        statusCode = 500;

        vm.fetchGeoNodes()
          .then(() => {
            expect(vm.isLoading).toBe(false);
            expect(document.querySelector('.flash-text').innerText.trim()).toBe(
              'Something went wrong while fetching nodes',
            );
          })
          .then(done)
          .catch(done.fail);
      });
    });

    describe('fetchNodeDetails', () => {
      it('calls service.getGeoNodeDetails and sets response to the store on success', done => {
        mock.onGet(mockNode.statusPath).reply(200, rawMockNodeDetails);
        spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();

        vm.fetchNodeDetails(mockNode)
          .then(() => {
            expect(vm.service.getGeoNodeDetails).toHaveBeenCalled();
            expect(Object.keys(vm.store.state.nodeDetails).length).not.toBe(0);
            expect(vm.store.state.nodeDetails['1']).toBeDefined();
          })
          .then(done)
          .catch(done.fail);
      });

      it('emits `nodeDetailsLoaded` event with fake nodeDetails object on 404 failure', done => {
        spyOn(eventHub, '$emit');
        mock.onGet(mockNode.statusPath).reply(404, {});
        spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();

        vm.fetchNodeDetails(mockNode)
          .then(() => {
            expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoaded', jasmine.any(Object));
            const nodeDetails = vm.store.state.nodeDetails['1'];

            expect(nodeDetails).toBeDefined();
            expect(nodeDetails.syncStatusUnavailable).toBe(true);
            expect(nodeDetails.health).toBe('Request failed with status code 404');
          })
          .then(done)
          .catch(done.fail);
      });

      it('emits `nodeDetailsLoaded` event with fake nodeDetails object when a network error occurs', done => {
        spyOn(eventHub, '$emit');
        mock.onGet(mockNode.statusPath).networkError();
        spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();

        vm.fetchNodeDetails(mockNode)
          .then(() => {
            expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoaded', jasmine.any(Object));
            const nodeDetails = vm.store.state.nodeDetails['1'];

            expect(nodeDetails).toBeDefined();
            expect(nodeDetails.syncStatusUnavailable).toBe(true);
            expect(nodeDetails.health).toBe('Network Error');
          })
          .then(done)
          .catch(done.fail);
      });

      it('emits `nodeDetailsLoaded` event with fake nodeDetails object when a timeout occurs', done => {
        spyOn(eventHub, '$emit');
        mock.onGet(mockNode.statusPath).timeout();
        spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();

        vm.fetchNodeDetails(mockNode)
          .then(() => {
            expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoaded', jasmine.any(Object));
            const nodeDetails = vm.store.state.nodeDetails['1'];

            expect(nodeDetails).toBeDefined();
            expect(nodeDetails.syncStatusUnavailable).toBe(true);
            expect(nodeDetails.health).toBe('timeout of 0ms exceeded');
          })
          .then(done)
          .catch(done.fail);
      });
    });

    describe('repairNode', () => {
      it('calls service.repairNode and shows success Flash message on request success', done => {
        const node = { ...mockNode };
        mock.onPost(node.repairPath).reply(() => {
          expect(node.nodeActionActive).toBe(true);
          return [200];
        });
        spyOn(vm.service, 'repairNode').and.callThrough();

        vm.repairNode(node)
          .then(() => {
            expect(vm.service.repairNode).toHaveBeenCalledWith(node);
            expect(document.querySelector('.flash-text').innerText.trim()).toBe(
              'Node Authentication was successfully repaired.',
            );

            expect(node.nodeActionActive).toBe(false);
          })
          .then(done)
          .catch(done.fail);
      });

      it('calls service.repairNode and shows failure Flash message on request failure', done => {
        const node = { ...mockNode };
        mock.onPost(node.repairPath).reply(() => {
          expect(node.nodeActionActive).toBe(true);
          return [500];
        });
        spyOn(vm.service, 'repairNode').and.callThrough();

        vm.repairNode(node)
          .then(() => {
            expect(vm.service.repairNode).toHaveBeenCalledWith(node);
            expect(document.querySelector('.flash-text').innerText.trim()).toBe(
              'Something went wrong while repairing node',
            );

            expect(node.nodeActionActive).toBe(false);
          })
          .then(done)
          .catch(done.fail);
      });
    });

    describe('toggleNode', () => {
      it('calls service.toggleNode for enabling node and updates toggle button on request success', done => {
        const node = { ...mockNode };
        mock.onPut(node.basePath).reply(() => {
          expect(node.nodeActionActive).toBe(true);
          return [
            200,
            {
              enabled: true,
            },
          ];
        });
        spyOn(vm.service, 'toggleNode').and.callThrough();
        node.enabled = false;

        vm.toggleNode(node)
          .then(() => {
            expect(vm.service.toggleNode).toHaveBeenCalledWith(node);
            expect(node.enabled).toBe(true);
            expect(node.nodeActionActive).toBe(false);
          })
          .then(done)
          .catch(done.fail);
      });

      it('calls service.toggleNode and shows Flash error on request failure', done => {
        const node = { ...mockNode };
        mock.onPut(node.basePath).reply(() => {
          expect(node.nodeActionActive).toBe(true);
          return [500];
        });
        spyOn(vm.service, 'toggleNode').and.callThrough();
        node.enabled = false;

        vm.toggleNode(node)
          .then(() => {
            expect(vm.service.toggleNode).toHaveBeenCalledWith(node);
            expect(document.querySelector('.flash-text').innerText.trim()).toBe(
              'Something went wrong while changing node status',
            );

            expect(node.nodeActionActive).toBe(false);
          })
          .then(done)
          .catch(done.fail);
      });
    });

    describe('removeNode', () => {
      it('calls service.removeNode for removing node and shows Flash message on request success', done => {
        const node = { ...mockNode };
        mock.onDelete(node.basePath).reply(() => {
          expect(node.nodeActionActive).toBe(true);
          return [200];
        });
        spyOn(vm.service, 'removeNode').and.callThrough();
        spyOn(vm.store, 'removeNode').and.stub();

        vm.removeNode(node)
          .then(() => {
            expect(vm.service.removeNode).toHaveBeenCalledWith(node);
            expect(vm.store.removeNode).toHaveBeenCalledWith(node);
            expect(document.querySelector('.flash-text').innerText.trim()).toBe(
              'Node was successfully removed.',
            );
          })
          .then(done)
          .catch(done.fail);
      });

      it('calls service.removeNode and shows Flash message on request failure', done => {
        const node = { ...mockNode };
        mock.onDelete(node.basePath).reply(() => {
          expect(node.nodeActionActive).toBe(true);
          return [500];
        });
        spyOn(vm.service, 'removeNode').and.callThrough();
        spyOn(vm.store, 'removeNode').and.stub();

        vm.removeNode(node)
          .then(() => {
            expect(vm.service.removeNode).toHaveBeenCalledWith(node);
            expect(vm.store.removeNode).not.toHaveBeenCalled();
            expect(document.querySelector('.flash-text').innerText.trim()).toBe(
              'Something went wrong while removing node',
            );
          })
          .then(done)
          .catch(done.fail);
      });
    });

    describe('handleNodeAction', () => {
      it('calls `toggleNode` and `hideNodeActionModal` when `targetNodeActionType` is `toggle`', () => {
        vm.targetNode = { ...mockNode };
        vm.targetNodeActionType = NODE_ACTIONS.TOGGLE;
        spyOn(vm, 'hideNodeActionModal');
        spyOn(vm, 'toggleNode').and.stub();

        vm.handleNodeAction();

        expect(vm.hideNodeActionModal).toHaveBeenCalled();
        expect(vm.toggleNode).toHaveBeenCalledWith(vm.targetNode);
      });

      it('calls `removeNode` and `hideNodeActionModal` when `targetNodeActionType` is `remove`', () => {
        vm.targetNode = { ...mockNode };
        vm.targetNodeActionType = NODE_ACTIONS.REMOVE;
        spyOn(vm, 'hideNodeActionModal');
        spyOn(vm, 'removeNode').and.stub();

        vm.handleNodeAction();

        expect(vm.hideNodeActionModal).toHaveBeenCalled();
        expect(vm.removeNode).toHaveBeenCalledWith(vm.targetNode);
      });
    });

    describe('showNodeActionModal', () => {
      let node;
      let modalKind;
      let modalMessage;
      let modalActionLabel;
      let modalTitle;
      let rootEmit;

      beforeEach(() => {
        node = { ...mockNode };
        modalKind = 'warning';
        modalMessage = 'Foobar message';
        modalActionLabel = 'Disable';
        modalTitle = 'Test title';
        rootEmit = spyOn(vm.$root, '$emit');
      });

      it('sets target node and modal config props on component', () => {
        vm.showNodeActionModal({
          actionType: NODE_ACTIONS.TOGGLE,
          node,
          modalKind,
          modalMessage,
          modalActionLabel,
          modalTitle,
        });

        expect(vm.targetNode).toBe(node);
        expect(vm.targetNodeActionType).toBe(NODE_ACTIONS.TOGGLE);
        expect(vm.modalKind).toBe(modalKind);
        expect(vm.modalMessage).toBe(modalMessage);
        expect(vm.modalActionLabel).toBe(modalActionLabel);
        expect(vm.modalTitle).toBe(modalTitle);
      });

      it('emits `bv::show::modal` when actionType is `toggle` and node is enabled', () => {
        node.enabled = true;
        vm.showNodeActionModal({
          actionType: NODE_ACTIONS.TOGGLE,
          node,
          modalKind,
          modalMessage,
          modalActionLabel,
          modalTitle,
        });

        expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', vm.modalId);
      });

      it('calls toggleNode when actionType is `toggle` and node.enabled is `false`', () => {
        node.enabled = false;
        spyOn(vm, 'toggleNode').and.stub();

        vm.showNodeActionModal({
          actionType: NODE_ACTIONS.TOGGLE,
          node,
          modalKind,
          modalMessage,
          modalActionLabel,
          modalTitle,
        });

        expect(vm.toggleNode).toHaveBeenCalledWith(vm.targetNode);
      });

      it('emits `bv::show::modal` when actionType is not `toggle`', () => {
        node.enabled = true;
        vm.showNodeActionModal({
          actionType: NODE_ACTIONS.REMOVE,
          node,
          modalKind,
          modalMessage,
          modalActionLabel,
        });

        expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', vm.modalId);
      });
    });

    describe('hideNodeActionModal', () => {
      it('emits `bv::hide::modal`', () => {
        const rootEmit = spyOn(vm.$root, '$emit');
        vm.hideNodeActionModal();

        expect(rootEmit).toHaveBeenCalledWith('bv::hide::modal', vm.modalId);
      });
    });
  });

  describe('created', () => {
    it('binds event handler for `pollNodeDetails`', () => {
      spyOn(eventHub, '$on');
      const vmX = createComponent();

      expect(eventHub.$on).toHaveBeenCalledWith('pollNodeDetails', jasmine.any(Function));
      expect(eventHub.$on).toHaveBeenCalledWith('showNodeActionModal', jasmine.any(Function));
      expect(eventHub.$on).toHaveBeenCalledWith('repairNode', jasmine.any(Function));
      vmX.$destroy();
    });
  });

  describe('beforeDestroy', () => {
    it('unbinds event handler for `pollNodeDetails`', () => {
      spyOn(eventHub, '$off');
      const vmX = createComponent();
      vmX.$destroy();

      expect(eventHub.$off).toHaveBeenCalledWith('pollNodeDetails', jasmine.any(Function));
      expect(eventHub.$off).toHaveBeenCalledWith('showNodeActionModal', jasmine.any(Function));
      expect(eventHub.$off).toHaveBeenCalledWith('repairNode', jasmine.any(Function));
    });
  });

  describe('template', () => {
    it('renders container element with class `geo-nodes-container`', () => {
      expect(vm.$el.classList.contains('geo-nodes-container')).toBe(true);
    });

    it('renders loading animation when `isLoading` is true', () => {
      vm.isLoading = true;

      expect(
        vm.$el.querySelectorAll('.loading-animation.prepend-top-20.append-bottom-20').length,
      ).not.toBe(0);
    });
  });
});
