require( 'source-map-support' ).install();
require( 'console-group' ).install();

var path = require( 'path' );
var os = require( 'os' );
var sander = require( 'sander' );
var assert = require( 'assert' );
var exec = require( 'child_process' ).exec;
var babel = require( 'babel-core' );
var rollup = require( '../dist/rollup' );

var FUNCTION = path.resolve( __dirname, 'function' );
var FORM = path.resolve( __dirname, 'form' );
var SOURCEMAPS = path.resolve( __dirname, 'sourcemaps' );
var CLI = path.resolve( __dirname, 'cli' );

var PROFILES = [
	{ format: 'amd' },
	{ format: 'cjs' },
	{ format: 'es6' },
	{ format: 'iife' },
	{ format: 'umd' }
];

function extend ( target ) {
	[].slice.call( arguments, 1 ).forEach( function ( source ) {
		source && Object.keys( source ).forEach( function ( key ) {
			target[ key ] = source[ key ];
		});
	});

	return target;
}

function normaliseOutput ( code ) {
	return code.toString().trim().replace( /\r\n/g, '\n' );
}

function loadConfig ( path ) {
	try {
		return require( path );
	} catch ( err ) {
		throw new Error( 'Failed to load ' + path + '. An old test perhaps? You should probably delete the directory' );
	}
}

describe( 'rollup', function () {
	this.timeout( 10000 );

	describe( 'sanity checks', function () {
		it( 'exists', function () {
			assert.ok( !!rollup );
		});

		it( 'has a rollup method', function () {
			assert.equal( typeof rollup.rollup, 'function' );
		});

		it( 'fails without options', function () {
			return rollup.rollup().then( function () {
				throw new Error( 'Missing expected error' );
			}, function (err) {
				assert.equal( 'You must supply options.entry to rollup', err.message );
			});
		});

		it( 'fails without options.entry', function () {
			return rollup.rollup({}).then( function () {
				throw new Error( 'Missing expected error' );
			}, function (err) {
				assert.equal( 'You must supply options.entry to rollup', err.message );
			});
		});

		it( 'fails with invalid keys', function () {
			return rollup.rollup({ entry: 'x', plUgins: [] }).then( function () {
				throw new Error( 'Missing expected error' );
			}, function ( err ) {
				assert.equal( err.message, 'Unexpected key \'plUgins\' found, expected one of: banner, dest, entry, exports, external, footer, format, globals, indent, intro, moduleId, moduleName, noConflict, onwarn, outro, plugins, sourceMap, treeshake, useStrict' );
			});
		});
	});

	describe( 'bundle.write()', function () {
		it( 'fails without options or options.dest', function () {
			return rollup.rollup({
				entry: 'x',
				plugins: [{
					resolveId: function () { return 'test'; },
					load: function () {
						return '// empty';
					}
				}]
			}).then( function ( bundle ) {
				assert.throws( function () {
					bundle.write();
				}, /must supply options\.dest/ );

				assert.throws( function () {
					bundle.write({});
				}, /must supply options\.dest/ );
			});
		});

		it( 'expects options.moduleName for IIFE and UMD bundles', function () {
			return rollup.rollup({
				entry: 'x',
				plugins: [{
					resolveId: function () { return 'test'; },
					load: function () {
						return 'export var foo = 42;';
					}
				}]
			}).then( function ( bundle ) {
				assert.throws( function () {
					bundle.generate({
						format: 'umd'
					});
				}, /You must supply options\.moduleName for UMD bundles/ );

				assert.throws( function () {
					bundle.generate({
						format: 'iife'
					});
				}, /You must supply options\.moduleName for IIFE bundles/ );
			});
		});
	});

	describe( 'function', function () {
		sander.readdirSync( FUNCTION ).sort().forEach( function ( dir ) {
			if ( dir[0] === '.' ) return; // .DS_Store...

			var config = loadConfig( FUNCTION + '/' + dir + '/_config.js' );
			( config.skip ? it.skip : config.solo ? it.only : it )( dir, function () {
				var warnings = [];
				var captureWarning = msg => warnings.push( msg );

				var options = extend( {
					entry: FUNCTION + '/' + dir + '/main.js',
					onwarn: captureWarning
				}, config.options );

				if ( config.solo ) console.group( dir );

				return rollup.rollup( options )
					.then( function ( bundle ) {
						var unintendedError;

						if ( config.error ) {
							throw new Error( 'Expected an error while rolling up' );
						}

						// try to generate output
						try {
							if(config.bundleOptions) { console.log(config.bundleOptions); }
							var result = bundle.generate( extend( {}, config.bundleOptions, {
								format: 'cjs'
							}));

							if ( config.generateError ) {
								unintendedError = new Error( 'Expected an error while generating output' );
							}
						} catch ( err ) {
							if ( config.generateError ) {
								config.generateError( err );
							} else {
								unintendedError = err;
							}
						}

						if ( unintendedError ) throw unintendedError;
						if ( config.error || config.generateError ) return;

						var code;

						if ( config.babel ) {
							code = babel.transform( result.code, {
								blacklist: [ 'es6.modules' ],
								loose: [ 'es6.classes' ]
							}).code;
						} else {
							code = result.code;
						}

						if ( config.code ) config.code( code );

						var module = {
							exports: {}
						};

						var context = extend({
							require: require,
							module: module,
							exports: module.exports,
							assert: assert
						}, config.context || {} );

						var contextKeys = Object.keys( context );
						var contextValues = contextKeys.map( function ( key ) {
							return context[ key ];
						});

						try {
							var fn = new Function( contextKeys, code );
							fn.apply( {}, contextValues );

							if ( config.runtimeError ) {
								unintendedError = new Error( 'Expected an error while executing output' );
							} else {
								if ( config.exports ) config.exports( module.exports );
								if ( config.bundle ) config.bundle( bundle );
							}
						} catch ( err ) {
							if ( config.runtimeError ) {
								config.runtimeError( err );
							} else {
								unintendedError = err;
							}
						}

						if ( config.warnings ) {
							config.warnings( warnings );
						} else if ( warnings.length ) {
							throw new Error( `Got unexpected warnings:\n${warnings.join('\n')}` );
						}

						if ( config.show || unintendedError ) {
							console.log( code + '\n\n\n' );
						}

						if ( config.solo ) console.groupEnd();

						if ( unintendedError ) throw unintendedError;
					}, function ( err ) {
						if ( config.error ) {
							config.error( err );
						} else {
							throw err;
						}
					});
			});
		});
	});

	describe( 'form', function () {
		sander.readdirSync( FORM ).sort().forEach( function ( dir ) {
			if ( dir[0] === '.' ) return; // .DS_Store...

			var config = loadConfig( FORM + '/' + dir + '/_config.js' );

			if ( config.skipIfWindows && process.platform === 'win32' ) return;

			var options = extend( {}, config.options, {
				entry: FORM + '/' + dir + '/main.js'
			});

			( config.skip ? describe.skip : config.solo ? describe.only : describe)( dir, function () {
				PROFILES.forEach( function ( profile ) {
					it( 'generates ' + profile.format, function () {
						return rollup.rollup( options ).then( function ( bundle ) {
							var options = extend( {}, config.options, {
								dest: FORM + '/' + dir + '/_actual/' + profile.format + '.js',
								format: profile.format
							});

							return bundle.write( options ).then( function () {
								var actualCode = normaliseOutput( sander.readFileSync( FORM, dir, '_actual', profile.format + '.js' ) );
								var expectedCode;
								var actualMap;
								var expectedMap;

								try {
									expectedCode = normaliseOutput( sander.readFileSync( FORM, dir, '_expected', profile.format + '.js' ) );
								} catch ( err ) {
									expectedCode = 'missing file';
								}

								try {
									actualMap = JSON.parse( sander.readFileSync( FORM, dir, '_actual', profile.format + '.js.map' ).toString() );
									actualMap.sourcesContent = actualMap.sourcesContent.map( normaliseOutput );
								} catch ( err ) {}

								try {
									expectedMap = JSON.parse( sander.readFileSync( FORM, dir, '_expected', profile.format + '.js.map' ).toString() );
									expectedMap.sourcesContent = expectedMap.sourcesContent.map( normaliseOutput );
								} catch ( err ) {}

								if ( config.show ) {
									console.log( actualCode + '\n\n\n' );
								}

								assert.equal( actualCode, expectedCode );
								assert.deepEqual( actualMap, expectedMap );
							});
						});
					});
				});
			});
		});
	});

	describe( 'sourcemaps', function () {
		sander.readdirSync( SOURCEMAPS ).sort().forEach( function ( dir ) {
			if ( dir[0] === '.' ) return; // .DS_Store...

			describe( dir, function () {
				var config = loadConfig( SOURCEMAPS + '/' + dir + '/_config.js' );

				var entry = path.resolve( SOURCEMAPS, dir, 'main.js' );
				var dest = path.resolve( SOURCEMAPS, dir, '_actual/bundle.js' );

				var options = extend( {}, config.options, {
					entry: entry
				});

				PROFILES.forEach( function ( profile ) {
					( config.skip ? it.skip : config.solo ? it.only : it )( 'generates ' + profile.format, function () {
						return rollup.rollup( options ).then( function ( bundle ) {
							var options = extend( {}, {
								format: profile.format,
								sourceMap: true,
								dest: dest
							}, config.options );

							bundle.write( options );

							var result = bundle.generate( options );
							config.test( result.code, result.map );
						});
					});
				});
			});
		});
	});

	describe( 'cli', function () {
		sander.readdirSync( CLI ).sort().forEach( function ( dir ) {
			if ( dir[0] === '.' ) return; // .DS_Store...

			describe( dir, function () {
				var config = loadConfig( CLI + '/' + dir + '/_config.js' );

				( config.skip ? it.skip : config.solo ? it.only : it )( dir, function ( done ) {
					process.chdir( path.resolve( CLI, dir ) );

					if (os.platform() === 'win32') {
						config.command = "node " + path.resolve( __dirname, '../bin' ) + path.sep + config.command;
					}

					exec( config.command, {
						env: {
							PATH: path.resolve( __dirname, '../bin' ) + path.delimiter + process.env.PATH
						}
					}, function ( err, code, stderr ) {
						if ( err || config.error ) {
							config.error( err );
							return done();
						}

						if ( stderr ) console.error( stderr );

						var unintendedError;

						if ( config.execute ) {
							try {
								if ( config.babel ) {
									code = babel.transform( code, {
										blacklist: [ 'es6.modules' ],
										loose: [ 'es6.classes' ]
									}).code;
								}

								var fn = new Function( 'require', 'module', 'exports', 'assert', code );
								var module = {
									exports: {}
								};
								fn( require, module, module.exports, assert );

								if ( config.error ) {
									unintendedError = new Error( 'Expected an error while executing output' );
								}

								if ( config.exports ) {
									config.exports( module.exports );
								}
							} catch ( err ) {
								if ( config.error ) {
									config.error( err );
								} else {
									unintendedError = err;
								}
							}

							if ( config.show || unintendedError ) {
								console.log( code + '\n\n\n' );
							}

							if ( config.solo ) console.groupEnd();

							unintendedError ? done( unintendedError ) : done();
						}

						else if ( config.result ) {
							try {
								config.result( code );
							} catch ( err ) {
								done( err );
							}
						}

						else {
							var expected = sander.readFileSync( '_expected.js' ).toString();
							try {
								assert.equal( normaliseOutput( code ), normaliseOutput( expected ) );
								done();
							} catch ( err ) {
								done( err );
							}
						}
					});
				});
			});
		});
	});
});