// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import React from 'react';
import { mount, shallow } from 'enzyme';

import ListView from './index';
import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame';

// Util to get list of all callbacks added to an event emitter by event type.
// jest adds "error" event listeners to window, this util makes it easier to
// ignore those calls.
function getListenersByType(mockFn) {
  const rv = {};
  mockFn.calls.forEach(([eventType, callback]) => {
    if (!rv[eventType]) {
      rv[eventType] = [callback];
    } else {
      rv[eventType].push(callback);
    }
  });
  return rv;
}

describe('<ListView>', () => {
  // polyfill window.requestAnimationFrame (and cancel) into jsDom's window
  polyfillAnimationFrame(window);

  const DATA_LENGTH = 40;

  function getHeight(index) {
    return index * 2 + 2;
  }

  function Item(props) {
    const { children, ...rest } = props;
    return <div {...rest}>{children}</div>;
  }

  function renderItem(itemKey, styles, itemIndex, attrs) {
    return (
      <Item key={itemKey} style={styles} {...attrs}>
        {itemIndex}
      </Item>
    );
  }

  let wrapper;
  let instance;

  const props = {
    dataLength: DATA_LENGTH,
    getIndexFromKey: Number,
    getKeyFromIndex: String,
    initialDraw: 5,
    itemHeightGetter: getHeight,
    itemRenderer: renderItem,
    itemsWrapperClassName: 'SomeClassName',
    viewBuffer: 10,
    viewBufferMin: 5,
    windowScroller: true,
  };

  describe('shallow tests', () => {
    beforeEach(() => {
      wrapper = shallow(<ListView {...props} />);
    });

    it('renders without exploding', () => {
      expect(wrapper).toBeDefined();
    });

    it('matches a snapshot', () => {
      expect(wrapper).toMatchSnapshot();
    });

    it('initialDraw sets the number of items initially drawn', () => {
      expect(wrapper.find(Item).length).toBe(props.initialDraw);
    });

    it('sets the height of the items according to the height func', () => {
      const items = wrapper.find(Item);
      const expectedHeights = [];
      const heights = items.map((node, i) => {
        expectedHeights.push(getHeight(i));
        return node.prop('style').height;
      });
      expect(heights.length).toBe(props.initialDraw);
      expect(heights).toEqual(expectedHeights);
    });

    it('saves the currently drawn indexes to _startIndexDrawn and _endIndexDrawn', () => {
      const inst = wrapper.instance();
      expect(inst._startIndexDrawn).toBe(0);
      expect(inst._endIndexDrawn).toBe(props.initialDraw - 1);
    });
  });

  describe('mount tests', () => {
    describe('accessor functions', () => {
      const clientHeight = 2;
      const scrollTop = 3;

      let oldRender;
      let oldInitWrapper;
      const initWrapperMock = jest.fn(elm => {
        if (elm != null) {
          // jsDom requires `defineProperties` instead of just setting the props
          Object.defineProperties(elm, {
            clientHeight: {
              get: () => clientHeight,
            },
            scrollTop: {
              get: () => scrollTop,
            },
          });
        }
        oldInitWrapper.call(this, elm);
      });

      beforeAll(() => {
        oldRender = ListView.prototype.render;
        // `_initWrapper` is not on the prototype, so it needs to be mocked
        // on each instance, use `render()` as a hook to do that
        ListView.prototype.render = function altRender() {
          if (this._initWrapper !== initWrapperMock) {
            oldInitWrapper = this._initWrapper;
            this._initWrapper = initWrapperMock;
          }
          return oldRender.call(this);
        };
      });

      afterAll(() => {
        ListView.prototype.render = oldRender;
      });

      beforeEach(() => {
        initWrapperMock.mockClear();
        wrapper = mount(<ListView {...props} />);
        instance = wrapper.instance();
      });

      it('getBottomVisibleIndex() returns a number', () => {
        const n = instance.getBottomVisibleIndex();
        expect(Number.isNaN(n)).toBe(false);
        expect(n).toEqual(expect.any(Number));
      });

      it('getTopVisibleIndex() returns a number', () => {
        const n = instance.getTopVisibleIndex();
        expect(Number.isNaN(n)).toBe(false);
        expect(n).toEqual(expect.any(Number));
      });

      it('getRowPosition() returns a number', () => {
        const { height, y } = instance.getRowPosition(2);
        expect(height).toEqual(expect.any(Number));
        expect(y).toEqual(expect.any(Number));
      });
    });

    describe('windowScroller', () => {
      let windowAddListenerSpy;
      let windowRmListenerSpy;

      beforeEach(() => {
        windowAddListenerSpy = jest.spyOn(window, 'addEventListener');
        windowRmListenerSpy = jest.spyOn(window, 'removeEventListener');
        const wsProps = { ...props, windowScroller: true };
        wrapper = mount(<ListView {...wsProps} />);
        instance = wrapper.instance();
      });

      afterEach(() => {
        windowAddListenerSpy.mockRestore();
      });

      it('adds the onScroll listener to the window element after the component mounts', () => {
        const eventListeners = getListenersByType(windowAddListenerSpy.mock);
        expect(eventListeners.scroll).toEqual([instance._onScroll]);
      });

      it('removes the onScroll listener from window when unmounting', () => {
        // jest adds "error" event listeners to window, ignore those calls
        let eventListeners = getListenersByType(windowRmListenerSpy.mock);
        expect(eventListeners.scroll).not.toBeDefined();
        wrapper.unmount();
        eventListeners = getListenersByType(windowRmListenerSpy.mock);
        expect(eventListeners.scroll).toEqual([instance._onScroll]);
      });

      it('calls _positionList when the document is scrolled', done => {
        const event = new Event('scroll');
        const fn = jest.spyOn(instance, '_positionList');
        expect(instance._isScrolledOrResized).toBe(false);
        window.dispatchEvent(event);
        expect(instance._isScrolledOrResized).toBe(true);
        window.requestAnimationFrame(() => {
          expect(fn).toHaveBeenCalled();
          done();
        });
      });

      it('uses the root HTML element to determine if the view has changed', () => {
        const htmlElm = instance._htmlElm;
        expect(htmlElm).toBeTruthy();
        const spyFns = {
          clientHeight: jest.fn(() => instance._viewHeight + 1),
          scrollTop: jest.fn(() => instance._scrollTop + 1),
        };
        Object.defineProperties(htmlElm, {
          clientHeight: {
            get: spyFns.clientHeight,
          },
          scrollTop: {
            get: spyFns.scrollTop,
          },
        });
        const hasChanged = instance._isViewChanged();
        expect(hasChanged).toBe(true);
        expect(spyFns.clientHeight).toHaveBeenCalled();
        expect(spyFns.scrollTop).toHaveBeenCalled();
      });
    });
  });
});
