/* eslint no-use-before-define: 0 no-unused-vars: 0 */

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import stringifyObject from 'stringify-object';
import _ from 'lodash';
import classNames from 'classnames';
import Toggle from 'react-bootstrap-toggle';
import 'react-bootstrap-toggle/dist/bootstrap2-toggle.css';
import { renderIf, limitEval } from '../../helpers';


class TestRunner extends Component {
  constructor(props) {
    super(props);
    this.getIframeDoc = this.getIframeDoc.bind(this);
    this.runTests = this.runTests.bind(this);
    this.handleInfiniteLoop = this.handleInfiniteLoop.bind(this);
    this.handleNewCode = this.handleNewCode.bind(this);
    this.state = {
      consoleLogs: [],
      hasRun: false
    };
  }

  componentWillReceiveProps(nextProps) {
    if (
      this.props.codeToTest !== nextProps.codeToTest &&
      this.props.autoRun
    ) {
      this.handleNewCode(nextProps);
    }
  }

  componentDidMount() {
    this.props.onTestRunnerRef(this);
    this.handleNewCode();
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.codeToTest.trim() !== nextProps.codeToTest.trim()) {
      return true;
    }
    // Only update if console anything but the console logs have changed
    return (
      !_.isEqual(nextState, this.state) &&
      _.isEqual(nextState.consoleLogs, this.state.consoleLogs)
    );
  }

  componentWillUnmount() {
    this.props.onTestRunnerRef(null);
    ReactDOM.unmountComponentAtNode(this.iframe);
  }

  handleNewCode() {
    const { hasRun } = this.state;
    if (!hasRun) {
      this.setState({ hasRun: true });
    }
    limitEval(
      this.props.codeToTest,
      success => {
        if (success) {
          this.setState({ consoleLogs: [] });
          this.iframe.onload = this.runTests.bind(this, this.props);
          // Need to clear Mocha tests
          this.iframe.contentWindow.location.reload();
        } else {
          this.handleInfiniteLoop();
        }
      },
      this.props.infiniteLoopTimeout
    );
  }

  onRunClick() {
    this.handleNewCode();
  }

  handleInfiniteLoop() {
    this.props.onError({
      message:
        'Your Code is taking too long to run. Maybe you wrote an infinite loop.'
    });
    this.props.toggleAutoRun();
  }

  getIframeDoc() {
    if (!this.iframe) return;
    const iFrame = this.iframe;
    let iFrameDoc = iFrame.contentDocument;
    if (!iFrameDoc && iFrame.contentWindow) {
      iFrameDoc = iFrame.contentWindow.document;
    }
    return iFrameDoc;
  }

  // TODO: move to file where the new submit button can hit this
  runTests({ codeToTest, testSpec }) {
    const { testHandlerUrl } = this.props;
    // Set up test results handler
    this.iframe.contentWindow.testHandler = this.props.onTestResults;
    // Set up console scrapper
    const nativeLog = this.iframe.contentWindow.console.log;
    const nativeErrorLog = this.iframe.contentWindow.console.error;

    this.iframe.contentWindow.console.log = (...args) => {
      const log = args.reduce(
        (log, arg) => `${log}\n${stringifyObject(arg)}`,
        ''
      );
      this.setState(
        {
          consoleLogs: this.state.consoleLogs.concat(log)
        },
        () => {
          this.props.onConsoleLog(this.state.consoleLogs);
        }
      );
      nativeLog.apply(this.iframe.contentWindow.console, args);
    };

    const doc = this.getIframeDoc();
    // Remove previous <body> elements
    while (doc.body.firstChild) {
      doc.body.removeChild(doc.body.firstChild);
    }
    const scripts = [
      { type: 'script', content: codeToTest, url: '' },
      { type: 'script', content: testSpec, url: '' },
      // Append script to handle Mocha results
      { type: 'script', url: testHandlerUrl }
    ];
    const mochaDiv = document.createElement('div');
    mochaDiv.id = 'mocha';
    doc.body.appendChild(mochaDiv);
    scripts.forEach(el => {
      const domElement = document.createElement(el.type);
      if (el.type === 'script' && el.url.length) {
        domElement.src = el.url;
      } else {
        domElement.innerHTML = el.content;
      }
      doc.body.appendChild(domElement);
    });
  }

  render() {
    const { autoRun, submitEnabled, onSubmitClick } = this.props;
    const { hasRun } = this.state;

    const SubmitButton = ({ onSubmit }) => (
      <button className="btn btn-default btn-sm" onClick={onSubmit}>
        Submit
      </button>
    );

    const TestToggleButton = ({ onToggle }) => (
      <Toggle
        onClick={onToggle}
        on="ON"
        off="OFF"
        offstyle="danger"
        active={autoRun}
      />
    );

    return (
      <div className="test-runner-container">
        <iframe
          title="test-runner"
          src={this.props.iFrameUrl}
          ref={iframe => {
            this.iframe = iframe;
          }}
          frameBorder="0"
          id="iframe"
        />
        {renderIf(!autoRun && !hasRun)(
          <h3 className="text-center">
            Click <code>Run</code> to start tests.
          </h3>
        )}
      </div>
    );
  }
}

TestRunner.propTypes = {
  autoRun: PropTypes.bool.isRequired,
  toggleAutoRun: PropTypes.func,
  testSpec: PropTypes.string.isRequired,
  codeToTest: PropTypes.string,
  onTestResults: PropTypes.func,
  iFrameUrl: PropTypes.string.isRequired,
  testHandlerUrl: PropTypes.string.isRequired,
  onError: PropTypes.func,
  infiniteLoopTimeout: PropTypes.number,
  submitEnabled: PropTypes.bool,
  onSubmitClick: PropTypes.func,
  onConsoleLog: PropTypes.func,
  onTestRunnerRef: PropTypes.func,
};


TestRunner.defaultProps = {
  scriptUrl: '',
  codeToTest: '',
  onTestResults: (passCount, failCount, testDuration) => {},
  testHandlerUrl: 'get_results.js',
  onError: (err) => {
    const message = _.get(err, 'message') ? err.message : 'Unknown';
    console.error('There was an error running your tests: ', message);
  },
  submitEnabled: false,
};

export default TestRunner;
