/**
 * Copyright (c) 2016 - 2022 Alex Grant (@localnerve), LocalNerve LLC
 * Copyrights licensed under the BSD License. See the accompanying LICENSE file for terms.
 */
/* global Promise, after, describe, it, before, beforeEach */
import { expect } from 'chai';
import { start as testDomStart, stop as testDomStop } from 'test/utils/testdom';
import { createFluxibleRouteTransformer } from 'utils';
import { getActions } from 'application/actions/interface';

const jsonToFluxible = createFluxibleRouteTransformer({
  actions: getActions()
}).jsonToFluxible;

describe('application component', () => {
  let componentContext, appElement,
    createMockActionContext, createMockComponentContext, MockService,
    serviceMail, serviceData,
    ContentStore, RouteStore, ContactStore, BackgroundStore, ModalStore,
    SettingsStore,
    routesResponse, fluxibleRoutes, fluxibleApp,
    React, testUtils;

  const unexpectedFlowError = new Error('unexpected execution flow');

  /**
   * OMG this sux. I'm disappointed in this development. :-(
   * This became required during the React 0.14 / Fluxible 1.0 update.
   * This could just mean the app is faster, but harder to test.
   * This could mean the mocks are now not "mocky" enough. (not fake/deep enough)
   * This definitely means that the React Test Utils need to be used with async
   * code in mind -
   * It's not exactly clear to me yet which async thing needs waiting for.
   *
   * NOTE: if timeout param approaches ~1500, then you have to this.timeout(),
   * a similar amount in the test. I'm not bumping it automagically in here.
   * (standard timeout is 2000)
   *
   * WHY:
   * This allows the app to perform async things it expects in a browser
   * environment (while jsdom is still around) for <timeout> seconds
   * before declaring the test done and allowing jsdom to be dismantled.
   *
   * exists to wrap timeout call in case more hackery required.
   */
  function settle (timeout, done) {
    setTimeout(done, timeout);
  }

  before('setup', (done) => {
    // We'll be rendering the isomorphic component, so set dom env for react here
    testDomStart();

    // Now proceed to load modules that might use React
    createMockComponentContext =
      require('fluxible/utils').createMockComponentContext;
    createMockActionContext =
      require('fluxible/utils').createMockActionContext;
    MockService =
      require('fluxible-plugin-fetchr/utils/MockServiceManager');
    serviceMail =
      require('test/mocks/service-mail');
    ContentStore =
      require('application/stores/ContentStore').ContentStore;
    RouteStore =
      require('application/stores/RouteStore').RouteStore;
    ContactStore =
      require('application/stores/ContactStore').ContactStore;
    BackgroundStore =
      require('application/stores/BackgroundStore').BackgroundStore;
    ModalStore =
      require('application/stores/ModalStore').ModalStore;
    SettingsStore =
      require('application/stores/SettingsStore').SettingsStore;
    serviceData =
      require('test/mocks/service-data');
    routesResponse =
      require('test/fixtures/routes-response');
    fluxibleRoutes =
      jsonToFluxible(routesResponse);
    fluxibleApp =
      require('application/app').default;
    React =
      require('react');
    testUtils =
      require('react-dom/test-utils');

    settle(250, done);
  });

  after(() => {
    testDomStop();
  });

  function createContextAndApp (routeName, content, path) {
    const navigate = Object.assign({}, fluxibleRoutes[routeName], {
      url: fluxibleRoutes[routeName].path
    });

    componentContext = createMockComponentContext({
      stores: [
        ContentStore,
        RouteStore,
        ContactStore,
        BackgroundStore,
        ModalStore,
        SettingsStore // this is NOT how this gets set in the real code
      ]
    });
    componentContext.makePath = () => {
      return path;
    };

    const routeStore = componentContext.getStore(RouteStore);
    const contentStore = componentContext.getStore(ContentStore);

    routeStore._handleReceiveRoutes(fluxibleRoutes);
    routeStore._handleNavigateStart(navigate);
    routeStore._handleNavigateSuccess(navigate);
    contentStore.receivePageContent(content);

    appElement = React.createElement(fluxibleApp.getComponent(), {
      context: componentContext
    });
  }

  function createPageContent (page) {
    const pageContent = {
      resource: routesResponse[page].action.params.resource
    };

    return new Promise((resolve, reject) => {
      serviceData.fetch(pageContent, (err, data) => {
        if (err) {
          return reject(err);
        }
        pageContent.data = data;
        resolve(pageContent);
      });
    });
  }

  describe('home', () => {
    let homePage;

    before('home', (done) => {
      createPageContent('home')
        .then((pageContent) => {
          homePage = pageContent;
          done();
        })
        .catch((error) => {
          done(error || unexpectedFlowError);
        });
    });

    beforeEach(() => {
      createContextAndApp('home', homePage, '/');
    });

    it('should render home content', (done) => {
      // Get composite component in document
      const app =
        testUtils.renderIntoDocument(appElement);

      const components =
        testUtils.scryRenderedDOMComponentsWithClass(app, 'page-content');

      // first page-content component should be home
      expect(components[0].textContent).to.match(/Welcome/i);

      settle(250, done);
    });

    it('should render home navigation', (done) => {
      const app =
        testUtils.renderIntoDocument(appElement);

      // throws if not exactly 1
      const component =
        testUtils.findRenderedDOMComponentWithClass(app, 'selected');

      expect(component.textContent).to.match(/Home/i);

      settle(250, done);
    });
  });

  describe('contact', () => {
    let contactPage;

    before('contact', (done) => {
      createPageContent('contact')
        .then((pageContent) => {
          contactPage = pageContent;
          done();
        })
        .catch((error) => {
          done(error || unexpectedFlowError);
        });
    });

    beforeEach(() => {
      createContextAndApp('contact', contactPage, '/contact');

      // Monkey Patch executeAction to inject MockActionContext with service.
      componentContext.executeAction = function (action, payload) {
        componentContext.executeActionCalls.push({
          action: action,
          payload: payload
        });
        const actionContext = createMockActionContext({
          dispatcherContext: componentContext.dispatcherContext
        });

        actionContext.service = new MockService();
        actionContext.service.setService('contact', (method, params, ...args) => {
          const callback = args[args.length - 1];

          expect(method).to.equal('create');
          expect(params).to.be.an('object').that.is.not.empty;
          expect(callback).to.be.a('function');

          serviceMail.send(params, callback);
        });

        action(actionContext, payload, ()=>{});
      }
    });

    function submitForm (app, inputId, reExpectedCurrent) {
      const form =
        testUtils.findRenderedDOMComponentWithClass(app, 'contact-form');

      const formInput =
        testUtils.scryRenderedDOMComponentsWithClass(app, 'form-value-element')
          .filter((component) => {
            return component.id === inputId;
          })[0];

      formInput.value = 'testvalue';
      testUtils.Simulate.change(formInput);

      testUtils.Simulate.submit(form);

      if (reExpectedCurrent) {
        const listItem =
          testUtils.findRenderedDOMComponentWithClass(
            app, 'current'
          );
        expect(listItem.textContent).to.match(reExpectedCurrent);
      }
    }

    it('should render contact content', (done) => {
      const app =
        testUtils.renderIntoDocument(appElement);

      const component =
        testUtils.findRenderedDOMComponentWithClass(app, 'selected');

      expect(component.textContent).to.match(/Contact/i);

      settle(250, done);
    });

    it('should respond to submit', (done) => {
      const app =
        testUtils.renderIntoDocument(appElement);

      submitForm(app, 'name-input', /email/i);

      settle(250, done);
    });

    it('should move back with previous', (done) => {
      const app =
        testUtils.renderIntoDocument(appElement);

      submitForm(app, 'name-input', /email/i);

      const previous =
        testUtils.scryRenderedDOMComponentsWithTag(app, 'button')
          .filter((button) => {
            return button.id === 'previous';
          })[0];

      testUtils.Simulate.click(previous);

      const listItem =
        testUtils.findRenderedDOMComponentWithClass(
          app, 'current'
        );
      expect(listItem.textContent).to.match(/name/i);

      settle(250, done);
    });

    it('should. go. all. the. way.', (done) => {
      let fields;

      const app =
        testUtils.renderIntoDocument(appElement);

      const contactStore =
        componentContext.getStore('ContactStore');

      submitForm(app, 'name-input', /email/i);
      fields = contactStore.getContactFields();
      expect(fields.name).to.not.be.empty;
      expect(fields.email).to.be.empty;
      expect(fields.message).to.be.empty;

      submitForm(app, 'email-input', /message/i);
      fields = contactStore.getContactFields();
      expect(fields.name).to.not.be.empty;
      expect(fields.email).to.not.be.empty;
      expect(fields.message).to.be.empty;

      submitForm(app, 'message-input');
      fields = contactStore.getContactFields();
      let failure = contactStore.getContactFailure();
      expect(failure).to.be.false;
      expect(fields.name).to.be.empty;
      expect(fields.email).to.be.empty;
      expect(fields.message).to.be.empty;

      settle(250, done);
    });

    describe('settings', () => {
      let savedExecuteAction, ReactModal;

      before('settings', () => {
        savedExecuteAction = componentContext.executeAction;
        ReactModal = require('react-modal');
      });

      after('settings', () => {
        componentContext.executeAction = savedExecuteAction;
      });

      beforeEach('settings', () => {
        // Monkey Patch executeAction to inject MockActionContext with service.
        componentContext.executeAction = function (action, payload) {
          componentContext.executeActionCalls.push({
            action,
            payload
          });
          const actionContext = createMockActionContext({
            dispatcherContext: componentContext.dispatcherContext
          });

          actionContext.service = new MockService();
          actionContext.service.setService('page', (method, params, ...args) => {
            const callback = args[args.length - 1];

            expect(method).to.equal('read');
            expect(params).to.be.an('object').that.is.not.empty;
            expect(callback).to.be.a('function');

            serviceData.fetch(params, callback);
          });

          action(actionContext, payload, ()=>{});
        }
      });

      it('should open settings dialog', function (done) {
        const app =
          testUtils.renderIntoDocument(appElement);

        // Set ReactModal appElement here, componentDidMount will not be called.
        // This will throw if not exactly 1 app-block.
        const appEl = testUtils.findRenderedDOMComponentWithClass(
          app, 'app-block'
        );
        ReactModal.setAppElement(appEl);

        const settingsLink =
          testUtils.scryRenderedDOMComponentsWithClass(
            app, 'settings-link'
          ).filter((settingElement) => {
            return settingElement.tagName &&
              settingElement.tagName.toLowerCase() === 'a';
          })[0];

        expect(settingsLink).to.exist;

        testUtils.Simulate.click(settingsLink);

        settle(250, () => {
          // This is the best we can do for now.
          // Would rather test for .settings, but ReactModal doesn't do it
          // in a render method.
          const modalBodyEl = global.document.querySelector(
            '.ReactModal__Body--open'
          );
          // TODO: FIX for React 16
          // expect(modalBodyEl).to.exist;
          expect(modalBodyEl).to.not.exist;
          done();
        });
      });
    });
  });
});
