/* global Element */

import { assign } from '@ember/polyfills';
import { getCurrentRunLoop, hasScheduledTimers, next, run } from '@ember/runloop';

import NodeQuery from './node-query';
import equalInnerHTML from '../equal-inner-html';
import equalTokens from '../equal-tokens';
import { equalsElement, regex, classes } from '../matchers';
import { Promise } from 'rsvp';

const TextNode = window.Text;
const HTMLElement = window.HTMLElement;
const Comment = window.Comment;

function isMarker(node) {
  if (node instanceof Comment && node.textContent === '') {
    return true;
  }

  if (node instanceof TextNode && node.textContent === '') {
    return true;
  }

  return false;
}

export default class AbstractTestCase {
  constructor(assert) {
    this.element = null;
    this.snapshot = null;
    this.assert = assert;

    let { fixture } = this;
    if (fixture) {
      this.setupFixture(fixture);
    }
  }

  teardown() {}
  afterEach() {}

  runTask(callback) {
    return run(callback);
  }

  runTaskNext() {
    return new Promise(resolve => {
      return next(resolve);
    });
  }

  setupFixture(innerHTML) {
    let fixture = document.getElementById('qunit-fixture');
    fixture.innerHTML = innerHTML;
  }

  // The following methods require `this.element` to work

  get firstChild() {
    return this.nthChild(0);
  }

  nthChild(n) {
    let i = 0;
    let node = this.element.firstChild;

    while (node) {
      if (!isMarker(node)) {
        i++;
      }

      if (i > n) {
        break;
      } else {
        node = node.nextSibling;
      }
    }

    return node;
  }

  get nodesCount() {
    let count = 0;
    let node = this.element.firstChild;

    while (node) {
      if (!isMarker(node)) {
        count++;
      }

      node = node.nextSibling;
    }

    return count;
  }

  $(sel) {
    if (sel instanceof Element) {
      return NodeQuery.element(sel);
    } else if (typeof sel === 'string') {
      return NodeQuery.query(sel, this.element);
    } else if (sel !== undefined) {
      throw new Error(`Invalid this.$(${sel})`);
    } else {
      return NodeQuery.element(this.element);
    }
  }

  wrap(element) {
    return NodeQuery.element(element);
  }

  click(selector) {
    let element;
    if (typeof selector === 'string') {
      element = this.element.querySelector(selector);
    } else {
      element = selector;
    }

    let event = element.click();

    return this.runLoopSettled(event);
  }

  // TODO: Find a better name 😎
  runLoopSettled(value) {
    return new Promise(function(resolve) {
      // Every 5ms, poll for the async thing to have finished
      let watcher = setInterval(() => {
        // If there are scheduled timers or we are inside of a run loop, keep polling
        if (hasScheduledTimers() || getCurrentRunLoop()) {
          return;
        }

        // Stop polling
        clearInterval(watcher);

        // Synchronously resolve the promise
        resolve(value);
      }, 5);
    });
  }

  textValue() {
    return this.element.textContent;
  }

  takeSnapshot() {
    let snapshot = (this.snapshot = []);

    let node = this.element.firstChild;

    while (node) {
      if (!isMarker(node)) {
        snapshot.push(node);
      }

      node = node.nextSibling;
    }

    return snapshot;
  }

  assertText(text) {
    this.assert.strictEqual(
      this.textValue(),
      text,
      `#qunit-fixture content should be: \`${text}\``
    );
  }

  assertInnerHTML(html) {
    equalInnerHTML(this.assert, this.element, html);
  }

  assertHTML(html) {
    equalTokens(this.element, html, `#qunit-fixture content should be: \`${html}\``);
  }

  assertElement(node, { ElementType = HTMLElement, tagName, attrs = null, content = null }) {
    if (!(node instanceof ElementType)) {
      throw new Error(`Expecting a ${ElementType.name}, but got ${node}`);
    }

    equalsElement(this.assert, node, tagName, attrs, content);
  }

  assertComponentElement(
    node,
    { ElementType = HTMLElement, tagName = 'div', attrs = null, content = null }
  ) {
    attrs = assign({}, { id: regex(/^ember\d*$/), class: classes('ember-view') }, attrs || {});
    this.assertElement(node, { ElementType, tagName, attrs, content });
  }

  assertSameNode(actual, expected) {
    this.assert.strictEqual(actual, expected, 'DOM node stability');
  }

  assertInvariants(oldSnapshot, newSnapshot) {
    oldSnapshot = oldSnapshot || this.snapshot;
    newSnapshot = newSnapshot || this.takeSnapshot();

    this.assert.strictEqual(newSnapshot.length, oldSnapshot.length, 'Same number of nodes');

    for (let i = 0; i < oldSnapshot.length; i++) {
      this.assertSameNode(newSnapshot[i], oldSnapshot[i]);
    }
  }

  assertPartialInvariants(start, end) {
    this.assertInvariants(this.snapshot, this.takeSnapshot().slice(start, end));
  }

  assertStableRerender() {
    this.takeSnapshot();
    this.runTask(() => this.rerender());
    this.assertInvariants();
  }
}