import { h, render, rerender, Component } from '../../src/preact';
/** @jsx h */
let spyAll = obj => Object.keys(obj).forEach( key => sinon.spy(obj,key) );
describe('Lifecycle methods', () => {
	let scratch;
	before( () => {
		scratch = document.createElement('div');
		(document.body || document.documentElement).appendChild(scratch);
	});
	beforeEach( () => {
		scratch.innerHTML = '';
	});
	after( () => {
		scratch.parentNode.removeChild(scratch);
		scratch = null;
	});
	describe('#componentWillUpdate', () => {
		it('should NOT be called on initial render', () => {
			class ReceivePropsComponent extends Component {
				componentWillUpdate() {}
				render() {
					return 
;
				}
			}
			sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate');
			render(, scratch);
			expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have.been.called;
		});
		it('should be called when rerender with new props from parent', () => {
			let doRender;
			class Outer extends Component {
				constructor(p, c) {
					super(p, c);
					this.state = { i: 0 };
				}
				componentDidMount() {
					doRender = () => this.setState({ i: this.state.i + 1 });
				}
				render(props, { i }) {
					return ;
				}
			}
			class Inner extends Component {
				componentWillUpdate(nextProps, nextState) {
					expect(nextProps).to.be.deep.equal({i: 1});
					expect(nextState).to.be.deep.equal({});
				}
				render() {
					return ;
				}
			}
			sinon.spy(Inner.prototype, 'componentWillUpdate');
			sinon.spy(Outer.prototype, 'componentDidMount');
			// Initial render
			render(, scratch);
			expect(Inner.prototype.componentWillUpdate).not.to.have.been.called;
			// Rerender inner with new props
			doRender();
			rerender();
			expect(Inner.prototype.componentWillUpdate).to.have.been.called;
		});
		it('should be called on new state', () => {
			let doRender;
			class ReceivePropsComponent extends Component {
				componentWillUpdate() {}
				componentDidMount() {
					doRender = () => this.setState({ i: this.state.i + 1 });
				}
				render() {
					return ;
				}
			}
			sinon.spy(ReceivePropsComponent.prototype, 'componentWillUpdate');
			render(, scratch);
			expect(ReceivePropsComponent.prototype.componentWillUpdate).not.to.have.been.called;
			doRender();
			rerender();
			expect(ReceivePropsComponent.prototype.componentWillUpdate).to.have.been.called;
		});
	});
	describe('#componentWillReceiveProps', () => {
		it('should NOT be called on initial render', () => {
			class ReceivePropsComponent extends Component {
				componentWillReceiveProps() {}
				render() {
					return ;
				}
			}
			sinon.spy(ReceivePropsComponent.prototype, 'componentWillReceiveProps');
			render(, scratch);
			expect(ReceivePropsComponent.prototype.componentWillReceiveProps).not.to.have.been.called;
		});
		it('should be called when rerender with new props from parent', () => {
			let doRender;
			class Outer extends Component {
				constructor(p, c) {
					super(p, c);
					this.state = { i: 0 };
				}
				componentDidMount() {
					doRender = () => this.setState({ i: this.state.i + 1 });
				}
				render(props, { i }) {
					return ;
				}
			}
			class Inner extends Component {
				componentWillMount() {
					expect(this.props.i).to.be.equal(0);
				}
				componentWillReceiveProps(nextProps) {
					expect(nextProps.i).to.be.equal(1);
				}
				render() {
					return ;
				}
			}
			sinon.spy(Inner.prototype, 'componentWillReceiveProps');
			sinon.spy(Outer.prototype, 'componentDidMount');
			// Initial render
			render(, scratch);
			expect(Inner.prototype.componentWillReceiveProps).not.to.have.been.called;
			// Rerender inner with new props
			doRender();
			rerender();
			expect(Inner.prototype.componentWillReceiveProps).to.have.been.called;
		});
		it('should be called in right execution order', () => {
			let doRender;
			class Outer extends Component {
				constructor(p, c) {
					super(p, c);
					this.state = { i: 0 };
				}
				componentDidMount() {
					doRender = () => this.setState({ i: this.state.i + 1 });
				}
				render(props, { i }) {
					return ;
				}
			}
			class Inner extends Component {
				componentDidUpdate() {
					expect(Inner.prototype.componentWillReceiveProps).to.have.been.called;
					expect(Inner.prototype.componentWillUpdate).to.have.been.called;
				}
				componentWillReceiveProps() {
					expect(Inner.prototype.componentWillUpdate).not.to.have.been.called;
					expect(Inner.prototype.componentDidUpdate).not.to.have.been.called;
				}
				componentWillUpdate() {
					expect(Inner.prototype.componentWillReceiveProps).to.have.been.called;
					expect(Inner.prototype.componentDidUpdate).not.to.have.been.called;
				}
				render() {
					return ;
				}
			}
			sinon.spy(Inner.prototype, 'componentWillReceiveProps');
			sinon.spy(Inner.prototype, 'componentDidUpdate');
			sinon.spy(Inner.prototype, 'componentWillUpdate');
			sinon.spy(Outer.prototype, 'componentDidMount');
			render(, scratch);
			doRender();
			rerender();
			expect(Inner.prototype.componentWillReceiveProps).to.have.been.calledBefore(Inner.prototype.componentWillUpdate);
			expect(Inner.prototype.componentWillUpdate).to.have.been.calledBefore(Inner.prototype.componentDidUpdate);
		});
	});
	let _it = it;
	describe('#constructor and component(Did|Will)(Mount|Unmount)', () => {
		/* global DISABLE_FLAKEY */
		let it = DISABLE_FLAKEY ? xit : _it;
		let setState;
		class Outer extends Component {
			constructor(p, c) {
				super(p, c);
				this.state = { show:true };
				setState = s => this.setState(s);
			}
			render(props, { show }) {
				return (
					
						{ show && (
							
						) }
					
				);
			}
		}
		class LifecycleTestComponent extends Component {
			constructor(p, c) { super(p, c); this._constructor(); }
			_constructor() {}
			componentWillMount() {}
			componentDidMount() {}
			componentWillUnmount() {}
			componentDidUnmount() {}
			render() { return ; }
		}
		class Inner extends LifecycleTestComponent {
			render() {
				return (
					
						
					
				);
			}
		}
		class InnerMost extends LifecycleTestComponent {
			render() { return ; }
		}
		let spies = ['_constructor', 'componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount'];
		let verifyLifycycleMethods = (TestComponent) => {
			let proto = TestComponent.prototype;
			spies.forEach( s => sinon.spy(proto, s) );
			let reset = () => spies.forEach( s => proto[s].reset() );
			it('should be invoked for components on initial render', () => {
				reset();
				render(, scratch);
				expect(proto._constructor).to.have.been.called;
				expect(proto.componentDidMount).to.have.been.called;
				expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount);
				expect(proto.componentDidMount).to.have.been.called;
			});
			it('should be invoked for components on unmount', () => {
				reset();
				setState({ show:false });
				rerender();
				expect(proto.componentDidUnmount).to.have.been.called;
				expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount);
				expect(proto.componentDidUnmount).to.have.been.called;
			});
			it('should be invoked for components on re-render', () => {
				reset();
				setState({ show:true });
				rerender();
				expect(proto._constructor).to.have.been.called;
				expect(proto.componentDidMount).to.have.been.called;
				expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount);
				expect(proto.componentDidMount).to.have.been.called;
			});
		};
		describe('inner components', () => {
			verifyLifycycleMethods(Inner);
		});
		describe('innermost components', () => {
			verifyLifycycleMethods(InnerMost);
		});
		describe('when shouldComponentUpdate() returns false', () => {
			let setState;
			class Outer extends Component {
				constructor() {
					super();
					this.state = { show:true };
					setState = s => this.setState(s);
				}
				render(props, { show }) {
					return (
						
					);
				}
			}
			class Inner extends Component {
				shouldComponentUpdate(){ return false; }
				componentWillMount() {}
				componentDidMount() {}
				componentWillUnmount() {}
				componentDidUnmount() {}
				render() {
					return ;
				}
			}
			let proto = Inner.prototype;
			let spies = ['componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount'];
			spies.forEach( s => sinon.spy(proto, s) );
			let reset = () => spies.forEach( s => proto[s].reset() );
			beforeEach( () => reset() );
			it('should be invoke normally on initial mount', () => {
				render(, scratch);
				expect(proto.componentWillMount).to.have.been.called;
				expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount);
				expect(proto.componentDidMount).to.have.been.called;
			});
			it('should be invoked normally on unmount', () => {
				setState({ show:false });
				rerender();
				expect(proto.componentWillUnmount).to.have.been.called;
				expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount);
				expect(proto.componentDidUnmount).to.have.been.called;
			});
			it('should still invoke mount for shouldComponentUpdate():false', () => {
				setState({ show:true });
				rerender();
				expect(proto.componentWillMount).to.have.been.called;
				expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount);
				expect(proto.componentDidMount).to.have.been.called;
			});
			it('should still invoke unmount for shouldComponentUpdate():false', () => {
				setState({ show:false });
				rerender();
				expect(proto.componentWillUnmount).to.have.been.called;
				expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount);
				expect(proto.componentDidUnmount).to.have.been.called;
			});
		});
	});
	describe('Lifecycle DOM Timing', () => {
		it('should be invoked when dom does (DidMount, WillUnmount) or does not (WillMount, DidUnmount) exist', () => {
			let setState;
			class Outer extends Component {
				constructor() {
					super();
					this.state = { show:true };
					setState = s => {
						this.setState(s);
						this.forceUpdate();
					};
				}
				componentWillMount() {
					expect(document.getElementById('OuterDiv'), 'Outer componentWillMount').to.not.exist;
				}
				componentDidMount() {
					expect(document.getElementById('OuterDiv'), 'Outer componentDidMount').to.exist;
				}
				componentWillUnmount() {
					expect(document.getElementById('OuterDiv'), 'Outer componentWillUnmount').to.exist;
				}
				componentDidUnmount() {
					expect(document.getElementById('OuterDiv'), 'Outer componentDidUnmount').to.not.exist;
				}
				render(props, { show }) {
					return (
						
					);
				}
			}
			class Inner extends Component {
				componentWillMount() {
					expect(document.getElementById('InnerDiv'), 'Inner componentWillMount').to.not.exist;
				}
				componentDidMount() {
					expect(document.getElementById('InnerDiv'), 'Inner componentDidMount').to.exist;
				}
				componentWillUnmount() {
					// @TODO Component mounted into elements (non-components)
					// are currently unmounted after those elements, so their
					// DOM is unmounted prior to the method being called.
					//expect(document.getElementById('InnerDiv'), 'Inner componentWillUnmount').to.exist;
				}
				componentDidUnmount() {
					expect(document.getElementById('InnerDiv'), 'Inner componentDidUnmount').to.not.exist;
				}
				render() {
					return ;
				}
			}
			let proto = Inner.prototype;
			let spies = ['componentWillMount', 'componentDidMount', 'componentWillUnmount', 'componentDidUnmount'];
			spies.forEach( s => sinon.spy(proto, s) );
			let reset = () => spies.forEach( s => proto[s].reset() );
			render(, scratch);
			expect(proto.componentWillMount).to.have.been.called;
			expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount);
			expect(proto.componentDidMount).to.have.been.called;
			reset();
			setState({ show:false });
			expect(proto.componentWillUnmount).to.have.been.called;
			expect(proto.componentWillUnmount).to.have.been.calledBefore(proto.componentDidUnmount);
			expect(proto.componentDidUnmount).to.have.been.called;
			reset();
			setState({ show:true });
			expect(proto.componentWillMount).to.have.been.called;
			expect(proto.componentWillMount).to.have.been.calledBefore(proto.componentDidMount);
			expect(proto.componentDidMount).to.have.been.called;
		});
		it('should remove this.base for HOC', () => {
			let createComponent = (name, fn) => {
				class C extends Component {
					componentWillUnmount() {
						expect(this.base, `${name}.componentWillUnmount`).to.exist;
					}
					componentDidUnmount() {
						expect(this.base, `${name}.componentDidUnmount`).not.to.exist;
					}
					render(props) { return fn(props); }
				}
				spyAll(C.prototype);
				return C;
			};
			class Wrapper extends Component {
				render({ children }) {
					return {children}
;
				}
			}
			let One = createComponent('One', () => one );
			let Two = createComponent('Two', () => two );
			let Three = createComponent('Three', () => three );
			let components = [One, Two, Three];
			let Selector = createComponent('Selector', ({ page }) => {
				let Child = components[page];
				return ;
			});
			class App extends Component {
				render(_, { page }) {
					return ;
				}
			}
			let app;
			render( app=c } />, scratch);
			for (let i=0; i<20; i++) {
				app.setState({ page: i%components.length });
				app.forceUpdate();
			}
		});
	});
});