// @flow

import React from 'react';
import { ReactWrapper } from 'enzyme';
import { assert } from 'chai';
import { spy, stub, useFakeTimers } from 'sinon';
import { createShallow, createMount, getClasses, unwrap } from '../test-utils';
import Collapse from './Collapse';

describe('<Collapse />', () => {
  let shallow;
  let classes;
  const props = {
    in: true,
    children: <div />,
  };

  before(() => {
    shallow = createShallow({ dive: true });
    classes = getClasses(<Collapse {...props} />);
  });

  it('should render a Transition', () => {
    const wrapper = shallow(<Collapse {...props} />);
    assert.strictEqual(wrapper.name(), 'Transition');
  });

  it('should render a container around the wrapper', () => {
    const wrapper = shallow(<Collapse {...props} classes={{ container: 'woofCollapse1' }} />);
    const child = new ReactWrapper(wrapper.props().children('entered'));
    assert.strictEqual(child.name(), 'div');
    assert.strictEqual(child.hasClass(classes.container), true);
    assert.strictEqual(child.hasClass('woofCollapse1'), true);
  });

  it('should render a wrapper around the children', () => {
    const children = <h1>Hello</h1>;
    const wrapper = shallow(<Collapse {...props}>{children}</Collapse>);
    const child = new ReactWrapper(wrapper.props().children('entered'));
    assert.strictEqual(child.childAt(0).is('div'), true, 'should be a div');
    assert.strictEqual(
      child
        .childAt(0)
        .childAt(0)
        .children()
        .type(),
      'h1',
      'should wrap the children',
    );
  });

  describe('event callbacks', () => {
    it('should fire event callbacks', () => {
      const events = ['onEnter', 'onEntering', 'onEntered', 'onExit', 'onExiting', 'onExited'];

      const handlers = events.reduce((result, n) => {
        result[n] = spy();
        return result;
      }, {});

      const wrapper = shallow(<Collapse {...props} {...handlers} />);

      events.forEach(n => {
        const event = n.charAt(2).toLowerCase() + n.slice(3);
        wrapper.simulate(event, { style: {} });
        assert.strictEqual(handlers[n].callCount, 1, `should have called the ${n} handler`);
      });
    });
  });

  describe('prop: timeout', () => {
    let wrapper;
    let instance;
    let element;
    const enterDuration = 556;
    const leaveDuration = 446;

    beforeEach(() => {
      wrapper = shallow(
        <Collapse
          {...props}
          timeout={{
            enter: enterDuration,
            exit: leaveDuration,
          }}
        />,
      );
      instance = wrapper.instance();
      element = { getBoundingClientRect: () => ({}), style: {} };
    });

    it('should create proper easeOut animation onEntering', () => {
      instance.handleEntering(element);
      assert.strictEqual(element.style.transitionDuration, `${enterDuration}ms`);
    });

    it('should create proper sharp animation onExiting', () => {
      instance.handleExiting(element);
      assert.strictEqual(element.style.transitionDuration, `${leaveDuration}ms`);
    });
  });

  describe('transition lifecycle', () => {
    let wrapper;
    let instance;

    before(() => {
      wrapper = shallow(<Collapse {...props} />);
      instance = wrapper.instance();
    });

    describe('handleEnter()', () => {
      let element;

      before(() => {
        element = { style: { height: 32 } };
        instance.handleEnter(element);
      });

      it('should set element height to 0 initially', () => {
        assert.strictEqual(element.style.height, '0px', 'should set the height to 0');
      });
    });

    describe('handleEntering()', () => {
      let heightMock;
      let element;

      before(() => {
        heightMock = 666;
        element = { style: { height: heightMock, transitionDuration: undefined } };
        instance.handleEntering(element);
      });

      it('should set height to the 0', () => {
        assert.strictEqual(element.style.height, '0px', 'should have 0px height');
      });

      it('should call handleEntering', () => {
        const onEnteringStub = spy();
        wrapper.setProps({ onEntering: onEnteringStub });
        instance = wrapper.instance();
        instance.handleEntering(element);

        assert.strictEqual(onEnteringStub.callCount, 1);
        assert.strictEqual(onEnteringStub.calledWith(element), true);
      });

      describe('timeout', () => {
        let theme;
        let timeoutMock;
        let restore;

        before(() => {
          theme = instance.props.theme;
          restore = theme.transitions.getAutoHeightDuration;
          theme.transitions.getAutoHeightDuration = stub().returns('woofCollapseStub');
          wrapper.setProps({ timeout: 'auto' });
          instance = wrapper.instance();
        });

        after(() => {
          theme.transitions.getAutoHeightDuration = restore;
        });

        it('no wrapper', () => {
          instance.wrapperRef = false;
          instance.handleEntering(element);
          assert.strictEqual(
            element.style.transitionDuration,
            `${theme.transitions.getAutoHeightDuration(0)}ms`,
          );
        });

        it('has wrapper', () => {
          const clientHeightMock = 10;
          instance.wrapperRef = { clientHeight: clientHeightMock };
          instance.handleEntering(element);
          assert.strictEqual(
            element.style.transitionDuration,
            `${theme.transitions.getAutoHeightDuration(clientHeightMock)}ms`,
          );
        });

        it('number should set timeout to ms', () => {
          timeoutMock = 3;
          wrapper.setProps({ timeout: timeoutMock });
          instance = wrapper.instance();
          instance.handleEntering(element);

          assert.strictEqual(element.style.transitionDuration, `${timeoutMock}ms`);
        });

        it('nothing should not set timeout', () => {
          const elementBackup = element;
          wrapper.setProps({ timeout: undefined });
          instance = wrapper.instance();
          instance.handleEntering(element);

          assert.strictEqual(
            element.style.transitionDuration,
            elementBackup.style.transitionDuration,
          );
        });
      });
    });

    describe('handleEntered()', () => {
      let element;
      let handleEnteredWrapper;
      let handleEnteredInstance;
      let onEnteredSpy;

      before(() => {
        handleEnteredWrapper = shallow(<Collapse {...props} />);
        onEnteredSpy = spy();
        handleEnteredWrapper.setProps({ onEntered: onEnteredSpy });
        handleEnteredInstance = handleEnteredWrapper.instance();
        element = { style: { height: 666, transitionDuration: '500ms' } };
        handleEnteredInstance.handleEntered(element);
      });

      it('should set height to auto', () => {
        assert.strictEqual(element.style.height, 'auto', 'should have auto height');
      });

      it('should have called onEntered', () => {
        assert.strictEqual(onEnteredSpy.callCount, 1, 'should have called props.onEntered');
      });
    });

    describe('handleExit()', () => {
      it('should set height to the wrapper height', () => {
        const element = { style: { height: 'auto' } };
        instance.wrapperRef = { clientHeight: 666 };
        instance.handleExit(element);
        assert.strictEqual(element.style.height, '666px', 'should have 666px height');
      });
    });

    describe('handleExiting()', () => {
      let element;

      before(() => {
        element = { style: { height: 666, transitionDuration: undefined } };
        instance.handleExiting(element);
      });

      it('should set height to the 0', () => {
        assert.strictEqual(element.style.height, '0px', 'should have 0px height');
      });

      it('should call onExiting', () => {
        const onExitingStub = spy();
        wrapper.setProps({ onExiting: onExitingStub });
        instance = wrapper.instance();
        instance.handleExiting(element);

        assert.strictEqual(onExitingStub.callCount, 1);
        assert.strictEqual(onExitingStub.calledWith(element), true);
      });

      describe('timeout', () => {
        let theme;
        let timeoutMock;
        let restore;

        before(() => {
          theme = instance.props.theme;
          restore = theme.transitions.getAutoHeightDuration;
          theme.transitions.getAutoHeightDuration = stub().returns('woofCollapseStub2');
          wrapper.setProps({ timeout: 'auto' });
          instance = wrapper.instance();
        });

        after(() => {
          theme.transitions.getAutoHeightDuration = restore;
        });

        it('no wrapper', () => {
          instance.wrapperRef = false;
          instance.handleExiting(element);
          assert.strictEqual(
            element.style.transitionDuration,
            `${theme.transitions.getAutoHeightDuration(0)}ms`,
          );
        });

        it('has wrapper', () => {
          const clientHeightMock = 10;
          instance.wrapperRef = { clientHeight: clientHeightMock };
          instance.handleExiting(element);
          assert.strictEqual(
            element.style.transitionDuration,
            `${theme.transitions.getAutoHeightDuration(clientHeightMock)}ms`,
          );
        });

        it('number should set timeout to ms', () => {
          timeoutMock = 3;
          wrapper.setProps({ timeout: timeoutMock });
          instance = wrapper.instance();
          instance.handleExiting(element);

          assert.strictEqual(element.style.transitionDuration, `${timeoutMock}ms`);
        });

        it('nothing should not set timeout', () => {
          const elementBackup = element;
          wrapper.setProps({ timeout: undefined });
          instance = wrapper.instance();
          instance.handleExiting(element);

          assert.strictEqual(
            element.style.transitionDuration,
            elementBackup.style.transitionDuration,
          );
        });
      });
    });
  });

  describe('handleRequestTimeout()', () => {
    let instance;
    let clock;

    before(() => {
      clock = useFakeTimers();
    });

    after(() => {
      clock.restore();
    });

    it('should return autoTransitionDuration when timeout is auto', () => {
      const wrapper = shallow(<Collapse {...props} timeout="auto" />);
      assert.strictEqual(wrapper.props().timeout, null);
      instance = wrapper.instance();
      const next = spy();

      const autoTransitionDuration = 10;
      instance.autoTransitionDuration = autoTransitionDuration;
      instance.addEndListener(null, next);
      assert.strictEqual(next.callCount, 0);
      clock.tick(autoTransitionDuration);
      assert.strictEqual(next.callCount, 1);

      instance.autoTransitionDuration = undefined;
      instance.addEndListener(null, next);
      assert.strictEqual(next.callCount, 1);
      clock.tick(0);
      assert.strictEqual(next.callCount, 2);
    });

    it('should return props.timeout when timeout is number', () => {
      const timeout = 10;
      const wrapper = shallow(<Collapse {...props} timeout={timeout} />);
      assert.strictEqual(wrapper.props().timeout, timeout);
      instance = wrapper.instance();

      const next = spy();
      instance.addEndListener(null, next);
      assert.strictEqual(next.callCount, 0);
      clock.tick(timeout);
      assert.strictEqual(next.callCount, 0);
    });
  });

  describe('mount', () => {
    let mount;
    let mountInstance;

    before(() => {
      mount = createMount();
      const CollapseNaked = unwrap(Collapse);
      mountInstance = mount(<CollapseNaked classes={{}} theme={{}} />).instance();
    });

    after(() => {
      mount.cleanUp();
    });

    it('instance should have a wrapper property', () => {
      assert.notStrictEqual(mountInstance.wrapper, undefined);
    });
  });

  describe('prop: collapsedHeight', () => {
    const collapsedHeight = '10px';

    it('should work when closed', () => {
      const wrapper = shallow(<Collapse {...props} collapsedHeight={collapsedHeight} />);
      const child = new ReactWrapper(wrapper.props().children('entered'));
      assert.strictEqual(child.props().style.minHeight, collapsedHeight);
    });

    it('should be taken into account in handleExiting', () => {
      const wrapper = shallow(<Collapse {...props} collapsedHeight={collapsedHeight} />);
      const instance = wrapper.instance();
      const element = { style: { height: 666, transitionDuration: undefined } };
      instance.handleExiting(element);
      assert.strictEqual(element.style.height, collapsedHeight, 'should have 0px height');
    });
  });
});
