import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import stringifyObject from 'stringify-object';
import _ from 'lodash';
import loopProtect from 'loop-protect';

import { defaultFunc } from '../CodeTester/helpers';

const Babel = require('babel-standalone');
const BabelTransformObjectRestSpread = require('babel-plugin-transform-object-rest-spread');

class TestRunner extends Component {
  constructor(props) {
    super(props);
    this.getIframeDoc = this.getIframeDoc.bind(this);
    this.runTests = this.runTests.bind(this);
    this.initializeLoopProtect = this.initializeLoopProtect.bind(this);
    this.handleNewCode = this.handleNewCode.bind(this);
    this.transformCode = this.transformCode.bind(this);

    this.onConsoleLog = _.debounce(this.props.onConsoleLog.bind(this), 500);

    this.state = {
      consoleLogs: [],
    };
  }

  componentDidMount() {
    this.initializeLoopProtect();
    this.props.onTestRunnerRef(this);
    // Start the tests only after iFrame is loaded
    // https://stackoverflow.com/questions/12009423/what-does-status-canceled-for-a-resource-mean-in-chrome-developer-tools
    this.iframe.onload = this.handleNewCode.bind(this, this.props);
  }

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

  shouldComponentUpdate() {
    // iframe does not need to re-render.
    // handleNewCode() will reload the iframe if necessary.
    // https://medium.com/@ebakhtarov/handling-of-iframes-in-react-f038be46ac24
    return false;
  }

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

  onRunClick() {
    this.handleNewCode();
  }

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

  initializeLoopProtect() {
    // https://github.com/jsbin/loop-protect
    /**
     * handleInfiniteLoop will execute in the context of the iFrame if the timeout is hit.
     * In the iFrame context, it will not have access to this component, and therefore
     * cannot call it's methods. e.g this.props.onError(), toggleAutoRun()
     */
    const handleInfiniteLoop = () => { console.error('Your code is a while to run. Maybe you wrote an infinite loop.'); };
    Babel.registerPlugin('loopProtection', loopProtect(this.props.infiniteLoopTimeout, handleInfiniteLoop));
  }

  handleNewCode(nextProps) {
    this.reloadIframe(nextProps);
  }

  transformCode(code) {
    if (!this.props.infLoopProtectEnabled) return code;
    // https://github.com/jsbin/loop-protect
    const transform = source => Babel.transform(source, {
      plugins: [BabelTransformObjectRestSpread, 'loopProtection'],
    }).code;

    // rewrite the user's JavaScript to protect loops
    return transform(code);
  }

  reloadIframe(nextProps) {
    this.setState({ consoleLogs: [] }, this.props.onConsoleLog.bind(this, []));
    this.iframe.onload = this.runTests.bind(this, nextProps || this.props);
    // Need to clear Mocha tests
    this.iframe.contentWindow.location.reload();
  }

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

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

    contentWindow.console.log = this.handleConsoleMessage.bind(this, 'log', nativeLog);
    contentWindow.console.error = this.handleConsoleMessage.bind(this, 'error', nativeErrorLog);

    const doc = this.getIframeDoc();
    // Remove previous <body> elements
    while (doc.body.firstChild) {
      doc.body.removeChild(doc.body.firstChild);
    }
    const scripts = [
      { type: 'script', content: this.transformCode(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() {
    return (
      <div className="test-runner-container">
        <iframe
          title="test-runner"
          src={this.props.iFrameUrl}
          ref={iframe => {
            this.iframe = iframe;
          }}
          frameBorder="0"
          id="iframe"
        />
      </div>
    );
  }
}

TestRunner.propTypes = {
  autoRun: PropTypes.bool.isRequired,
  // eslint-disable-next-line
  testSpec: PropTypes.string.isRequired,
  codeToTest: PropTypes.string,
  onTestResults: PropTypes.func,
  iFrameUrl: PropTypes.string.isRequired,
  testHandlerUrl: PropTypes.string.isRequired,
  // eslint-disable-next-line
  onError: PropTypes.func,
  infiniteLoopTimeout: PropTypes.number,
  infLoopProtectEnabled: PropTypes.bool,
  // eslint-disable-next-line
  onConsoleLog: PropTypes.func,
  onTestRunnerRef: PropTypes.func.isRequired,
};


TestRunner.defaultProps = {
  codeToTest: '',
  onTestResults: (passCount, failCount, testDuration) => {}, // eslint-disable-line no-unused-vars
  onError: (err) => {
    const message = _.get(err, 'message') ? err.message : 'Unknown';
    console.error('There was an error running your tests: ', message);
  },
  infLoopProtectEnabled: true,
  infiniteLoopTimeout: 3000,
  onConsoleLog: defaultFunc,
};

export default TestRunner;
