// helpers for mocking the getting and setting of: position, dimension and scroll position for:
// - elements on the page
// - the page
// - the window

const triggerEvent = require('./events.js').triggerEvent;

function getDescriptorForProperty (prop, obj) {
  const descriptors = Object.getOwnPropertyDescriptors(obj);
  const prototype = Object.getPrototypeOf(obj);

  if ((descriptors !== {}) && (prop in descriptors)) {
    return descriptors[prop];
  }

  // if not in this object's descriptors, check the prototype chain
  if (prototype !== null) {
    return getDescriptorForProperty(prop, prototype);
  }

  // no descriptor for this prop and no prototypes left in the chain
  return null;
};

class WindowMock {
  constructor (jest) {
    this._defaults = {
      height: window.innerHeight,
      width: window.innerWidth
    };
    this.spies = {
      document: {},
      window: {}
    };
    this._jest = jest;
    this._setSpies();
    this._plugJSDOM();
  }

  get top () {
    return window.scrollY;
  }

  get bottom () {
    return window.scrollY + window.innerHeight;
  }

  get height () {
    return window.innerHeight;
  }

  get width () {
    return window.innerWidth
  }

  get scrollPosition () {
    return window.scrollY;
  }

  _setSpies () {

    // remove calls to document.documentElement.clientHeight when jQuery is gone. It's called to support older browsers like IE8
    this.spies.document.clientHeight = this._jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => window.innerHeight);

    // remove calls to document.documentElement.clientWidth when jQuery is gone. It's called to support older browsers like IE8
    this.spies.document.clientWidth = this._jest.spyOn(document.documentElement, 'clientWidth', 'get').mockImplementation(() => window.innerWidth);

  }

  _plugJSDOM () {

    const self = this;

    // JSDOM doesn't support .scrollTo
    this.spies.window.scrollTo = this._jest.fn(function () {
      let y;

      // data sent as props in an object
      if (arguments.length === 1) {
        y = arguments[0].y;
      } else {
        y = arguments[1];
      }

      self.scrollTo(y);

    });

     window.scrollTo = this.spies.window.scrollTo;

  }

  setHeightTo (height) {

    window.innerHeight = height;

  }

  setWidthTo (width) {

    window.innerWidth = width;

  }

  resizeTo (dimensions) {

    this.setHeightTo(dimensions.height);
    this.setWidthTo(dimensions.width);
    triggerEvent(window, 'resize');

  }

  scrollTo (scrollPosition) {

    document.documentElement.scrollTop = scrollPosition;
    window.scrollY = scrollPosition;
    window.pageYOffset = scrollPosition;

    triggerEvent(window, 'scroll');

  }

  reset () {

    window.innerHeight = this._defaults.height;
    window.innerWidth = this._defaults.width;
    this.scrollTo(0);

    // reset all spies
    Object.keys(this.spies).forEach(key => {
      const objectSpies = this.spies[key];
      Object.keys(objectSpies).forEach(key => objectSpies[key].mockClear());
    });

  }
}

class ScreenRenderItem {
  constructor (jest, node) {

    this._jest = jest;
    this._node = node;
    this._storeProps();
    this._mockAPICalls();

  }

  setData (itemData) {

    // check all the item data is present
    const itemProps = Object.keys(itemData);
    const missingKeys = ScreenRenderItem.REQUIRED_PROPS.filter(prop => !itemProps.includes(prop));

    this._data = {};

    if (missingKeys.length) {
      throw Error(`${itemData.name ? itemData.name : itemProps.join(', ')} is missing these properties: ${missingKeys.join(', ')}`);
    }

    // default left if not set
    if (!('offsetLeft' in itemData)) { itemData.offsetLeft = 0; }

    // copy onto internal store
    Object.assign(this._data, itemData);

  }

  _getBoundingClientRect () {
    const {offsetHeight, offsetWidth, offsetTop, offsetLeft} = this._data;
    const x = offsetLeft - window.scrollX;
    const y = offsetTop - window.scrollY;

    return {
      'x': x,
      'y': y,
      'top': (offsetHeight < 0) ? y + offsetHeight : y,
      'left': (offsetWidth < 0) ? x + offsetWidth : x,
      'bottom': (offsetTop + offsetHeight) - window.scrollY,
      'right': (offsetLeft + offsetWidth) - window.scrollX,
    };

  }

  reset () {

    // reset DOMRect mock
    this._node.getBoundingClientRect.mockClear();

    ScreenRenderItem.OFFSET_PROPS.forEach(prop => {

      if (prop in this._propStore) {
        // replace property implementation
        Object.defineProperty(this._node, prop, this._propStore[prop]);
      }

    });

  }

  _storeProps () {

    this._propStore = {};

    ScreenRenderItem.OFFSET_PROPS.forEach(prop => {
      const descriptor = getDescriptorForProperty(prop, this._node);

      if (descriptor !== null) {
        this._propStore[prop] = descriptor;
      }
    });

  }

  // mock any calls to the node's DOM API for position/dimension
  _mockAPICalls () {

    // proxy boundingClientRect property calls to item data
    this._jest.spyOn(this._node, 'getBoundingClientRect').mockImplementation(() => this._getBoundingClientRect());

    // handle calls to offset properties
    ScreenRenderItem.OFFSET_PROPS.forEach(prop => {

      this._jest.spyOn(this._node, prop, 'get').mockImplementation(() => this._data[prop]);

      // proxy DOM API sets for offsetValues (not possible to mock directly)
      Object.defineProperty(this._node, prop, {
        configurable: true,
        set: jest.fn(value => {
          this._data[prop] = value;
          return true;
        })
      });

    });

  }

}
ScreenRenderItem.OFFSET_PROPS = ['offsetHeight', 'offsetWidth', 'offsetTop', 'offsetLeft'];
ScreenRenderItem.REQUIRED_PROPS = ['name', 'offsetHeight', 'offsetHeight', 'offsetWidth', 'offsetTop'];

class ScreenMock {
  constructor (jest) {

    this._jest = jest
    this._items = {};

  }

  mockPositionAndDimension (itemName, node, itemData) {

    if (itemName in this._items) { throw new Error(`${itemName} already has its position and dimension mocked`); }

    const data = Object.assign({ 'name': itemName }, itemData);
    const item = new ScreenRenderItem(this._jest, node);

    item.setData(data);

    this._items[itemName] = item;

  }

  setWindow (windowData) {

    this.window = new WindowMock(this._jest);

    // check all the window data is present
    const missingKeys = Object.keys(windowData).filter(key => !ScreenMock.REQUIRED_WINDOW_PROPS.includes(key));

    if (missingKeys.length) {
      throw Error(`Window definition is missing these properties: ${missingKeys.join(', ')}`);
    }

    this.window.setHeightTo(windowData.height);
    this.window.setWidthTo(windowData.width);
    this.window.scrollTo(windowData.scrollTop);

  }

  scrollTo (scrollTop) {

    this.window.scrollTo(scrollTop);

  }

  reset () {

    Object.keys(this._items).forEach(itemName => this._items[itemName].reset());

  }

}
ScreenMock.REQUIRED_WINDOW_PROPS = ['height', 'width', 'scrollTop'];

exports.WindowMock = WindowMock;
exports.ScreenMock = ScreenMock;
