/**
 * Authors: k.inaba
 * License: NYSL 0.9982 http://www.kmonos.net/nysl/
 *
 * Hepler routines for unittesting.
 * TODO: Is there any clean way to implement "assert_compiles" and "assert_not_compile"?
 */
module tricks.test;
import std.conv : text;
import core.exception;

version(unittest)
{
	import std.cstream;
	import core.runtime;

	static this()
	{
		installCustomTestRunner();
	}

	private void installCustomTestRunner()
	{
		Runtime.moduleUnitTester = function()
		{
			Throwable firstError = null;

			void logError(Throwable e)
			{
				if(firstError is null)
					firstError = e;
				derr.writefln("  !! %s(%d): %s", e.file, e.line, e.msg);
			}

			void testModule(ModuleInfo* m, void function() test)
			{
				derr.writefln("[TEST] %s", m.name);
				try { test(); } catch( Throwable e ) { logError(e); }
			}

			bool report()
			{
				if(firstError is null)
					return true;
				derr.writefln("[TEST] The first error was:\n%s", firstError);
				derr.writeString("[TEST] press enter to exit.");
				din.readLine();
				return false;
			}

			foreach(m; ModuleInfo)
				if(m && m.unitTest)
					testModule(m, m.unitTest);
			return report();
		};
	}
}

/// Unittest helper that asserts an expression must throw something

void assert_throw(ExcT=Throwable, T, string fn=__FILE__, size_t ln=__LINE__)(lazy T t, string msg="")
{
	static if( is(ExcT == Throwable) )
		try
			{ t(); }
		catch(ExcT)
			{ return; }
	else
		try
			{ t(); }
		catch(ExcT)
			{ return; }
		catch(Throwable e)
			{ onAssertErrorMsg(fn, ln, msg.length ? msg : "bad exception \n  >> "~e.toString()); }
	onAssertErrorMsg(fn, ln, msg.length ? msg : "not thrown");
}

/// Unittest helper that asserts an expression must not throw anything

T assert_nothrow(T, string fn=__FILE__, size_t ln=__LINE__)(lazy T t, string msg="")
{
	try
		{ return t(); }
	catch(Throwable e)
		{ onAssertErrorMsg(fn, ln, msg.length ? msg : "bad exception \n  >> "~e.toString()); }
	assert(false);
}

unittest
{
	auto error = {throw new Error("hello");};
	auto nothing = (){};
	auto assertError = {assert(0);};

	assert_nothrow          ( assert_nothrow(nothing()) );
	assert_throw!AssertError( assert_nothrow(error()) );
	assert_throw!AssertError( assert_nothrow(assertError()) );

	assert_nothrow          ( assert_throw!Error(error()) );
	assert_throw!AssertError( assert_throw!Error(nothing()) );
	assert_nothrow          ( assert_throw!Error(assertError()) );
	assert_throw!AssertError( assert_throw!AssertError(error()) );
}

template assertOp(string op)
{
	void assertOp(A, B, string fn=__FILE__, size_t ln=__LINE__)(A a, B b, string msg="")
	{
		try
			{ if( mixin("a"~op~"b") ) return; }
		catch(Throwable e)
			{ onAssertErrorMsg(fn, ln, msg.length ? msg : "bad exception \n  >> "~e.toString()); }
		onAssertErrorMsg(fn, ln, msg.length ? msg : text(a, " !", op, " ", b));
	}
}

alias assertOp!(`==`) assert_eq; /// asserts two operands are ==
alias assertOp!(`!=`) assert_ne; /// asserts two operands are !=
alias assertOp!(`<`)  assert_lt; /// asserts two operands are <
alias assertOp!(`<=`) assert_le; /// asserts two operands are <=
alias assertOp!(`>`)  assert_gt; /// asserts two operands are >
alias assertOp!(`>=`) assert_ge; /// asserts two operands are >=

unittest
{
	assert_nothrow( assert_eq(1, 1) );
	assert_nothrow( assert_ne(1, 0) );
	assert_nothrow( assert_lt(0, 1) );
	assert_nothrow( assert_le(0, 1) );
	assert_nothrow( assert_le(0, 0) );
	assert_nothrow( assert_gt(1, 0) );
	assert_nothrow( assert_ge(1, 0) );
	assert_nothrow( assert_ge(0, 0) );

	assert_throw!AssertError( assert_eq(1, 0) );
	assert_throw!AssertError( assert_ne(1, 1) );
	assert_throw!AssertError( assert_lt(1, 1) );
	assert_throw!AssertError( assert_lt(1, 0) );
	assert_throw!AssertError( assert_le(1, 0) );
	assert_throw!AssertError( assert_gt(0, 0) );
	assert_throw!AssertError( assert_gt(0, 1) );
	assert_throw!AssertError( assert_ge(0, 1) );

	class Temp { bool opEquals(int x){return x/x==x;} }
	assert_throw!AssertError( assert_eq(new Temp, 0) );
	assert_nothrow          ( assert_eq(new Temp, 1) );
	assert_throw!AssertError( assert_eq(new Temp, 2) );
}
