import renderToString from 'preact-render-to-string';
import { rerender } from 'preact';
import React from '../src';
describe('components', () => {
	let scratch;
	before(() => {
		scratch = document.createElement('div');
		(document.body || document.documentElement).appendChild(scratch);
	});
	beforeEach(() => {
		scratch.innerHTML = '';
	});
	after(() => {
		scratch.parentNode.removeChild(scratch);
		scratch = null;
	});
	it('should be sane', () => {
		let props;
		class Demo extends React.Component {
			render() {
				let { a, b } = this.props;
				props = this.props;
				return 
{this.props.children}
;
			}
		}
		let html = renderToString(
			inner
		);
		expect(props).to.exist.and.deep.equal({
			a: 'b',
			c: 'd',
			children: 'inner'
		});
		expect(html).to.equal('inner
');
	});
	it('should support replaceState()', done => {
		class Demo extends React.Component {
			render() {
				return ;
			}
		}
		let render = sinon.spy(Demo.prototype, 'render'),
			inst;
		React.render( inst = c} />, scratch);
		inst.setState({ foo: 'bar', baz: 'bat' });
		setTimeout(() => {
			expect(inst.state).to.eql({ foo: 'bar', baz: 'bat' });
			let callbackState;
			let callback = sinon.spy(() => {
				callbackState = inst.state;
			});
			inst.replaceState({}, callback);
			setTimeout(() => {
				expect(callback).to.have.been.calledOnce;
				expect(callbackState).to.eql({});
				expect(inst.state).to.eql({});
				done();
			}, 10);
		}, 10);
	});
	it('should alias props.children', () => {
		class Foo extends React.Component {
			render() {
				return {this.props.children}
;
			}
		}
		let children = ['a', b, c],
			foo;
		React.render((
			 foo = c}>
				{children}
			
		), scratch);
		expect(foo.props).to.exist.and.have.property('children').eql(children);
	});
	it('should single out children before componentWillReceiveProps', () => {
		let props;
		class Child extends React.Component {
			componentWillReceiveProps(newProps) {
				props = newProps;
			}
		}
		class Parent extends React.Component {
			render() {
				return second;
			}
		}
		let a = React.render(, scratch);
		a.forceUpdate();
		expect(props).to.exist.and.deep.equal({
			children: 'second'
		});
	});
	it('should support array[object] children', () => {
		let children;
		class Foo extends React.Component {
			render() {
				children = this.props.children;
				return ;
			}
		}
		const data = [{ a: '' }];
		React.render({data}, scratch);
		expect(children).to.exist.and.deep.equal(data);
	});
	describe('getInitialState', () => {
		it('should be invoked for new components', () => {
			class Foo extends React.Component {
				getInitialState() {
					return { foo: 'bar' };
				}
				render() {
					return ;
				}
			}
			sinon.spy(Foo.prototype, 'getInitialState');
			let a = React.render(, scratch);
			expect(Foo.prototype.getInitialState).to.have.been.calledOnce;
			expect(a.state).to.eql({ foo: 'bar' });
		});
	});
	describe('defaultProps', () => {
		it('should support defaultProps for components', () => {
			let render = sinon.stub().returns();
			const Foo = React.createClass({
				defaultProps: {
					foo: 'default foo',
					bar: 'default bar'
				},
				render
			});
			React.render(, scratch);
			expect(render).to.have.been.calledWithMatch(Foo.defaultProps);
			render.resetHistory();
			React.render(, scratch);
			expect(render).to.have.been.calledWithMatch({ foo: 'default foo', bar: 'bar' });
		});
		it('should support defaultProps for pure components', () => {
			const Foo = sinon.stub().returns();
			Foo.defaultProps = {
				foo: 'default foo',
				bar: 'default bar'
			};
			React.render(, scratch);
			expect(Foo).to.have.been.calledWithMatch(Foo.defaultProps);
			Foo.resetHistory();
			React.render(, scratch);
			expect(Foo).to.have.been.calledWithMatch({ foo: 'default foo', bar: 'bar' });
		});
	});
	describe('propTypes', () => {
		function checkPropTypes(Foo, name = 'Foo') {
			sinon.stub(console, 'error');
			React.render(, scratch);
			expect(console.error).to.have.been.calledWithMatch(
				'Warning: Failed prop type: The prop `func` is marked as required in `' + name + '`, but its value is `undefined`.'
			);
			expect(console.error).to.have.been.called;
			console.error.resetHistory();
			React.render( { }} />, scratch);
			expect(console.error).not.to.have.been.called;
			React.render( { }} bool="one" />, scratch);
			expect(console.error).to.have.been.calledWithMatch(
				'Warning: Failed prop type: Invalid prop `bool` of type `string` supplied to `' + name + '`, expected `boolean`.'
			);
			console.error.restore();
		}
		it('should support propTypes for ES Class components', () => {
			class Foo extends React.Component {
				static propTypes = {
					func: React.PropTypes.func.isRequired,
					bool: React.PropTypes.bool
				};
				render() {
					return ;
				}
			}
			checkPropTypes(Foo);
		});
		it('should support propTypes for createClass components', () => {
			const Bar = React.createClass({
				propTypes: {
					func: React.PropTypes.func.isRequired,
					bool: React.PropTypes.bool
				},
				render: () => 
			});
			checkPropTypes(Bar, 'Bar');
		});
		it('should support propTypes for pure components', () => {
			function Baz() { return ; }
			Baz.propTypes = {
				func: React.PropTypes.func.isRequired,
				bool: React.PropTypes.bool
			};
			checkPropTypes(Baz, 'Baz');
			const Bip = () => ;
			Bip.displayName = 'Bip';
			Bip.propTypes = {
				func: React.PropTypes.func.isRequired,
				bool: React.PropTypes.bool
			};
			checkPropTypes(Bip, 'Bip');
		});
	});
	describe("mixins", () => {
		describe("getDefaultProps", () => {
			it('should use a mixin', () => {
				const Foo = React.createClass({
					mixins: [
						{ getDefaultProps: () => ({ a: true }) }
					],
					render() {
						return ;
					}
				});
				expect(Foo.defaultProps).to.eql({
					a: true
				});
			});
			it('should combine the results', () => {
				const Foo = React.createClass({
					mixins: [
						{ getDefaultProps: () => ({ a: true }) },
						{ getDefaultProps: () => ({ b: true }) }
					],
					getDefaultProps() {
						return { c: true };
					},
					render() {
						return ;
					}
				});
				expect(Foo.defaultProps).to.eql({
					a: true,
					b: true,
					c: true
				});
			});
			it('should work with statics', () => {
				const Foo = React.createClass({
					statics: {
						a: false
					},
					getDefaultProps() {
						return { b: true, c: this.a };
					},
					render() {
						return ;
					}
				});
				expect(Foo.defaultProps).to.eql({
					b: true,
					c: false
				});
			});
			// Disabled to save bytes
			xit('should throw an error for duplicate keys', () => {
				expect(() => {
					const Foo = React.createClass({
						mixins: [
							{ getDefaultProps: () => ({ a: true }) }
						],
						getDefaultProps() {
							return { a: true };
						},
						render() {
							return ;
						}
					});
				}).to.throw();
			});
		});
		describe("getInitialState", () => {
			it('should combine the results', () => {
				const Foo = React.createClass({
					mixins: [
						{ getInitialState: () => ({ a: true }) },
						{ getInitialState: () => ({ b: true }) }
					],
					getInitialState() {
						return { c: true };
					},
					render() {
						return ;
					}
				});
				let a = React.render(, scratch);
				expect(a.state).to.eql({
					a: true,
					b: true,
					c: true
				});
			});
			// Disabled to save bytes
			xit('should throw an error for duplicate keys', () => {
				const Foo = React.createClass({
					mixins: [
						{ getInitialState: () => ({ a: true }) }
					],
					getInitialState() {
						return { a: true };
					},
					render() {
						return ;
					}
				});
				expect(() => {
					React.render(, scratch);
				}).to.throw();
			});
		});
	});
	describe('refs', () => {
		it('should support string refs', () => {
			let inst, innerInst;
			class Foo extends React.Component {
				constructor() {
					super();
					inst = this;
				}
				render() {
					return (
						
					);
				}
			}
			const Inner = React.createClass({
				render() {
					innerInst = this;
					return (
						
							
h2
						
					);
				}
			});
			React.render(, scratch);
			expect(inst).to.exist;
			expect(inst.refs).to.be.an('object');
			expect(inst.refs).to.have.property('top', scratch.firstChild);
			expect(inst.refs).to.have.property('h1', scratch.querySelector('h1'));
			expect(inst.refs).to.have.property('p', scratch.querySelector('p'));
			expect(inst.refs).to.have.property('span', scratch.querySelector('span'));
			expect(inst.refs).to.have.property('inner', innerInst, 'ref to child component');
			expect(inst.refs).not.to.have.property('contained');
			expect(inst.refs).not.to.have.property('inner-h2');
			expect(innerInst.refs).to.have.all.keys(['contained', 'inner-h2']);
			expect(innerInst.refs).to.have.property('contained', scratch.querySelector('.contained'));
			expect(innerInst.refs).to.have.property('inner-h2', scratch.querySelector('h2'));
		});
		it('should retain support for function refs', () => {
			let ref1 = sinon.spy(),
				ref2 = sinon.spy(),
				componentRef = sinon.spy(),
				innerInst;
			class Foo extends React.Component {
				render() {
					return this.props.empty ? () : (
						
							
h1
							
						
					);
				}
			}
			const Inner = React.createClass({
				render() {
					innerInst = this;
					return ;
				}
			});
			React.render(, scratch);
			expect(ref1).to.have.have.been.calledOnce.and.calledWith(scratch.firstChild);
			expect(ref2).to.have.have.been.calledOnce.and.calledWith(scratch.querySelector('h1'));
			expect(componentRef).to.have.been.calledOnce.and.calledWith(innerInst);
			React.render(, scratch);
			// React.unmountComponentAtNode(scratch);
			expect(ref1, 'ref1').to.have.have.been.calledTwice.and.calledWith(null);
			expect(ref2, 'ref2').to.have.have.been.calledTwice.and.calledWith(null);
			expect(componentRef, 'componentRef').to.have.been.calledTwice.and.calledWith(null);
		});
		it('should support string refs via cloneElement()', () => {
			let inner, outer;
			const Inner = React.createClass({
				render() {
					inner = this;
					return (
						
							{React.cloneElement(React.Children.only(this.props.children), { id: 'one' })}
							{React.cloneElement(React.Children.only(this.props.children), { id: 'two', ref: 'two' })}
						
					);
				}
			});
			const Outer = React.createClass({
				render() {
					outer = this;
					return (
						
							foo
						
					);
				}
			});
			React.render(, scratch);
			let one = scratch.firstElementChild.children[0];
			let two = scratch.firstElementChild.children[1];
			expect(outer).to.have.property('refs').eql({ one });
			expect(inner).to.have.property('refs').eql({ two });
		});
	});
	describe('PureComponent', () => {
		it('should be a class', () => {
			expect(React).to.have.property('PureComponent').that.is.a('function');
		});
		it('should only re-render when props or state change', () => {
			class C extends React.PureComponent {
				render() {
					return ;
				}
			}
			let spy = sinon.spy(C.prototype, 'render');
			let inst = React.render(, scratch);
			expect(spy).to.have.been.calledOnce;
			spy.resetHistory();
			inst = React.render(, scratch);
			expect(spy).not.to.have.been.called;
			let b = { foo: 'bar' };
			inst = React.render(, scratch);
			expect(spy).to.have.been.calledOnce;
			spy.resetHistory();
			inst = React.render(, scratch);
			expect(spy).not.to.have.been.called;
			inst.setState({});
			rerender();
			expect(spy).not.to.have.been.called;
			inst.setState({ a: 'a', b });
			rerender();
			expect(spy).to.have.been.calledOnce;
			spy.resetHistory();
			inst.setState({ a: 'a', b });
			rerender();
			expect(spy).not.to.have.been.called;
		});
	});
});