/**
 * Copyright 2017 Red Hat Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */

import IronicApiService from '../../js/services/IronicApiService';
import IronicInspectorApiService from '../../js/services/IronicInspectorApiService';
import MistralApiService from '../../js/services/MistralApiService';
import { mockStore } from './utils';
import NodesActions from '../../js/actions/NodesActions';
import * as ErrorActions from '../../js/actions/ErrorActions';
import NodesConstants from '../../js/constants/NodesConstants';
import MistralConstants from '../../js/constants/MistralConstants';

const mockGetNodesResponse = [{ uuid: 1 }, { uuid: 2 }];

describe('Nodes Actions', () => {
  it('creates action to request nodes', () => {
    const expectedAction = {
      type: NodesConstants.REQUEST_NODES
    };
    expect(NodesActions.requestNodes()).toEqual(expectedAction);
  });

  it('creates action to receive nodes', () => {
    const expectedAction = {
      type: NodesConstants.RECEIVE_NODES,
      payload: mockGetNodesResponse
    };
    expect(NodesActions.receiveNodes(mockGetNodesResponse)).toEqual(
      expectedAction
    );
  });

  it('creates action to notify that nodes operation started', () => {
    const expectedAction = {
      type: NodesConstants.START_NODES_OPERATION,
      payload: mockGetNodesResponse
    };
    expect(NodesActions.startOperation(mockGetNodesResponse)).toEqual(
      expectedAction
    );
  });

  it('creates action to notify that nodes operation finished', () => {
    const expectedAction = {
      type: NodesConstants.FINISH_NODES_OPERATION,
      payload: mockGetNodesResponse
    };
    expect(NodesActions.finishOperation(mockGetNodesResponse)).toEqual(
      expectedAction
    );
  });

  it('should create an action for pending Node update', () => {
    const expectedAction = {
      type: NodesConstants.UPDATE_NODE_PENDING,
      payload: 'someId'
    };
    expect(NodesActions.updateNodePending('someId')).toEqual(expectedAction);
  });

  it('should create an action for successful Node update', () => {
    const updatedNode = {
      uuid: 1
    };
    const expectedAction = {
      type: NodesConstants.UPDATE_NODE_SUCCESS,
      payload: updatedNode
    };
    expect(NodesActions.updateNodeSuccess(updatedNode)).toEqual(expectedAction);
  });

  it('should create an action for failed Node update', () => {
    const expectedAction = {
      type: NodesConstants.UPDATE_NODE_FAILED,
      payload: 'someNodeId'
    };
    expect(NodesActions.updateNodeFailed('someNodeId')).toEqual(expectedAction);
  });

  it('should create an action to add Nodes', () => {
    const registeredNodes = {
      uuid1: {
        uuid: 'uuid1',
        name: 'node1'
      },
      uuid2: {
        uuid: 'uuid2',
        name: 'node2'
      }
    };
    const expectedAction = {
      type: NodesConstants.ADD_NODES,
      payload: registeredNodes
    };
    expect(NodesActions.addNodes(registeredNodes)).toEqual(expectedAction);
  });
});

describe('Fetching Nodes Actions', () => {
  beforeEach(() => {
    IronicApiService.getNodes = jest
      .fn()
      .mockReturnValue(() => Promise.resolve({ nodes: [{ uuid: '123' }] }));
    IronicApiService.getPorts = jest.fn().mockReturnValue(() =>
      Promise.resolve({
        ports: [{ uuid: 'port1', address: 'mac', node_uuid: '123' }]
      })
    );
    IronicInspectorApiService.getIntrospectionStatuses = jest
      .fn()
      .mockReturnValue(() =>
        Promise.resolve({
          introspection: [{ uuid: 'node1', state: 'finished' }]
        })
      );
  });

  it('fetches the data and dispatches actions', () => {
    const store = mockStore({});
    const normalizedData = {
      nodes: {
        123: {
          uuid: '123'
        }
      },
      ports: {
        port1: {
          uuid: 'port1',
          address: 'mac',
          node_uuid: '123'
        }
      },
      introspectionStatuses: {
        node1: {
          uuid: 'node1',
          state: 'finished'
        }
      }
    };

    return store.dispatch(NodesActions.fetchNodes()).then(() => {
      expect(IronicApiService.getNodes).toHaveBeenCalled();
      expect(IronicApiService.getPorts).toHaveBeenCalled();
      expect(
        IronicInspectorApiService.getIntrospectionStatuses
      ).toHaveBeenCalled();

      expect(store.getActions()).toEqual([
        NodesActions.requestNodes(),
        NodesActions.receiveNodes(normalizedData)
      ]);
    });
  });
});

describe('Fetching Introspection data success', () => {
  const response = { interfaces: { eth0: { mac: '00:00:00:00:00:11' } } };
  const nodeId = '598612eb-f21b-435e-a868-7bb74e576cc2';
  const store = mockStore({});

  beforeEach(() => {
    IronicInspectorApiService.getIntrospectionData = jest
      .fn()
      .mockReturnValue(() => Promise.resolve(response));
  });

  it('dispatches fetchNodeIntrospectionDataSuccess', () => {
    return store
      .dispatch(NodesActions.fetchNodeIntrospectionData(nodeId))
      .then(() => {
        expect(
          IronicInspectorApiService.getIntrospectionData
        ).toHaveBeenCalledWith(nodeId);
        expect(store.getActions()).toEqual([
          NodesActions.fetchNodeIntrospectionDataSuccess(nodeId, response)
        ]);
      });
  });
});

describe('Fetching Introspection data error', () => {
  const store = mockStore({});
  const nodeId = '598612eb-f21b-435e-a868-7bb74e576cc2';
  const error = {
    response: {
      data: { error: { message: 'some error message' } },
      status: 404
    }
  };

  beforeEach(() => {
    IronicInspectorApiService.getIntrospectionData = jest
      .fn()
      .mockReturnValue(() => Promise.reject(error));
    ErrorActions.handleErrors = jest.fn().mockReturnValue(() => {});
  });

  it('dispatches fetchNodeIntrospectionDataFailed', () => {
    return store
      .dispatch(NodesActions.fetchNodeIntrospectionData(nodeId))
      .then(() => {
        expect(
          IronicInspectorApiService.getIntrospectionData
        ).toHaveBeenCalledWith(nodeId);
        expect(store.getActions()).toEqual([
          NodesActions.fetchNodeIntrospectionDataFailed(nodeId)
        ]);
      });
  });
});

describe('Asynchronous Introspect Nodes Action', () => {
  const store = mockStore({});
  const nodeIds = ['598612eb-f21b-435e-a868-7bb74e576cc2'];

  beforeEach(() => {
    MistralApiService.runWorkflow = jest
      .fn()
      .mockReturnValue(() => Promise.resolve({ state: 'RUNNING' }));
    NodesActions.pollNodeslistDuringProgress = jest
      .fn()
      .mockReturnValue(() => {});
  });

  it('dispatches startOperation', () => {
    return store
      .dispatch(NodesActions.startNodesIntrospection(nodeIds))
      .then(() => {
        expect(MistralApiService.runWorkflow).toHaveBeenCalledWith(
          MistralConstants.BAREMETAL_INTROSPECT,
          {
            node_uuids: nodeIds
          }
        );
        expect(NodesActions.pollNodeslistDuringProgress).toHaveBeenCalled();
        expect(store.getActions()).toEqual([
          NodesActions.startOperation(nodeIds)
        ]);
      });
  });
});

describe('nodesIntrospectionFinished', () => {
  beforeEach(() => {
    NodesActions.fetchNodes = jest.fn().mockReturnValue(() => {});
  });

  it('handles successful nodes introspection', () => {
    const store = mockStore({});
    const messagePayload = {
      execution: {
        input: {
          node_uuids: ['598612eb-f21b-435e-a868-7bb74e576cc2']
        }
      },
      status: 'SUCCESS',
      message: 'Nodes Introspection was successful',
      introspected_nodes: {
        '598612eb-f21b-435e-a868-7bb74e576cc2': { finished: true, error: null }
      },
      execution_id: '622eb415-a522-4016-b5f6-6e9e0b3f687a',
      queue_name: 'tripleo',
      ttl: 3600
    };

    store.dispatch(NodesActions.nodesIntrospectionFinished(messagePayload));
    expect(NodesActions.fetchNodes).toHaveBeenCalled();
    const actions = store.getActions();
    expect(Object.keys(actions).map(key => actions[key].type)).toEqual([
      'FINISH_NODES_OPERATION',
      'NOTIFY'
    ]);
  });

  it('handles failed nodes introspection', () => {
    const store = mockStore({});
    const messagePayload = {
      execution: {
        input: {
          node_uuids: ['598612eb-f21b-435e-a868-7bb74e576cc2']
        }
      },
      status: 'FAILED',
      message: [
        { message: 'Nodes Introspection failed' },
        { message: 'Some error occurred during introspection' }
      ],
      introspected_nodes: {},
      execution_id: '622eb415-a522-4016-b5f6-6e9e0b3f687a',
      queue_name: 'tripleo',
      ttl: 3600
    };

    store.dispatch(NodesActions.nodesIntrospectionFinished(messagePayload));
    expect(NodesActions.fetchNodes).toHaveBeenCalled();
    const actions = store.getActions();
    expect(Object.keys(actions).map(key => actions[key].type)).toEqual([
      'FINISH_NODES_OPERATION',
      'NOTIFY'
    ]);
  });
});

describe('startProvideNodes Action', () => {
  const store = mockStore({});
  const nodeIds = ['598612eb-f21b-435e-a868-7bb74e576cc2'];

  beforeEach(() => {
    MistralApiService.runWorkflow = jest
      .fn()
      .mockReturnValue(() => Promise.resolve({ state: 'RUNNING' }));
    NodesActions.pollNodeslistDuringProgress = jest
      .fn()
      .mockReturnValue(() => {});
  });

  it('dispatches actions', () => {
    return store.dispatch(NodesActions.startProvideNodes(nodeIds)).then(() => {
      expect(MistralApiService.runWorkflow).toHaveBeenCalledWith(
        MistralConstants.BAREMETAL_PROVIDE,
        {
          node_uuids: nodeIds
        }
      );
      expect(NodesActions.pollNodeslistDuringProgress).toHaveBeenCalled();
      expect(store.getActions()).toEqual([
        NodesActions.startOperation(nodeIds)
      ]);
    });
  });
});

describe('provideNodesFinished', () => {
  beforeEach(() => {
    NodesActions.fetchNodes = jest.fn().mockReturnValue(() => {});
  });

  it('handles success', () => {
    const store = mockStore({});
    const messagePayload = {
      status: 'SUCCESS',
      message: 'Nodes were successfully made available',
      execution: { input: { node_uuids: [] } }
    };

    store.dispatch(NodesActions.nodesIntrospectionFinished(messagePayload));
    expect(NodesActions.fetchNodes).toHaveBeenCalled();
    const actions = store.getActions();
    expect(Object.keys(actions).map(key => actions[key].type)).toEqual([
      'FINISH_NODES_OPERATION',
      'NOTIFY'
    ]);
  });

  it('handles failure', () => {
    const store = mockStore({});
    const messagePayload = {
      status: 'FAILED',
      message: [{ result: 'Failed to set nodes to available.' }],
      execution: { input: { node_uuids: [] } }
    };

    store.dispatch(NodesActions.nodesIntrospectionFinished(messagePayload));
    expect(NodesActions.fetchNodes).toHaveBeenCalled();
    const actions = store.getActions();
    expect(Object.keys(actions).map(key => actions[key].type)).toEqual([
      'FINISH_NODES_OPERATION',
      'NOTIFY'
    ]);
  });
});

describe('Update Node thunk action', () => {
  const store = mockStore({});
  const nodePatch = {
    uuid: 'someId',
    patches: [
      {
        op: 'replace',
        path: '/properties/capabilities',
        value: 'updated value for path'
      }
    ]
  };

  beforeEach(() => {
    IronicApiService.patchNode = jest
      .fn()
      .mockReturnValue(() => Promise.resolve({ uuid: 'someId' }));
  });

  it('dispatches required actions', () => {
    return store.dispatch(NodesActions.updateNode(nodePatch)).then(() => {
      expect(store.getActions()).toEqual([
        NodesActions.updateNodePending('someId'),
        NodesActions.updateNodeSuccess({ uuid: 'someId' })
      ]);
    });
  });
});

describe('Delete Nodes thunk action', () => {
  const store = mockStore({});
  const nodeIds = ['598612eb-f21b-435e-a868-7bb74e576cc2'];

  beforeEach(() => {
    IronicApiService.deleteNode = jest
      .fn()
      .mockReturnValue(() => Promise.resolve());
  });

  it('successfully deletes a set of nodes', () => {
    return store.dispatch(NodesActions.deleteNodes(nodeIds)).then(() => {
      expect(store.getActions()).toEqual([
        NodesActions.startOperation(nodeIds),
        NodesActions.deleteNodeSuccess(nodeIds[0])
      ]);
    });
  });
});
