#include <catch2/catch.hpp>

#include <alib/iostream>

#include <sys/stat.h>
#include <registry/AlgorithmRegistry.hpp>
#include <registration/AlgoRegistration.hpp>

#include "common/TestLine.hpp"

class Foo {
	int m_base;
	public:
	Foo ( int base ) : m_base ( base ) {
	}

	int bar ( int value ) const {
		return m_base + value;
	}

	int base ( ) const {
		return m_base;
	}

	static Foo make_foo ( int base ) {
		return Foo ( base );
	}
};

namespace {
	auto fooBar = registration::MethodRegister < Foo, int, const Foo, int > ( & Foo::bar, "bar" );
} /* namespace */


TEST_CASE ( "Cli", "[unit][cli]" ) {
	class One {
	public:
		static int one ( ) {
			return 1;
		}
	};

	class Two {
	public:
		static int two ( ) {
			return 2;
		}
	};

	class Add {
	public:
		static int add ( int a, const int & b ) {
			return a + b;
		}

		static int add2 ( const int & a, int b ) {
			return a + b;
		}
	};

	class Neg {
	public:
		static int neg ( int a ) {
			return - a;
		}
	};

	class IntToDouble {
	public:
		static double cast ( int a ) {
			return a;
		}
	};

	class Divide {
	public:
		static double divide ( double a, double b ) {
			return a / b;
		}

		static int divide2 ( int a, int b ) {
			return a / b;
		}
	};

	SECTION ( "Test Create Unique" ) {
		abstraction::AlgorithmRegistry::registerAlgorithm < One > ( One::one, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 0 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < Two > ( Two::two, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 0 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < Add > ( Add::add, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 2 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < Add > ( Add::add2, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 2 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < Neg > ( Neg::neg, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 1 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < Divide > ( Divide::divide, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 2 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < Divide > ( Divide::divide2, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 2 > ( ) );

		mkdir ( "local", S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH );

		// execute automaton::Determinize < aaa.xml <( parseString aaa.text ) <(decodeTree aaa.raw) > res.xml // determinizuj, parametry podle ocekavaneho typu z xml, string z textove reprezentace, strom z raw reprezentace; zapis jako xml do souboru
		// execute automaton::RandomAutomaton DFA 2 2 2 // nahodny automat DFA s danymi vlastnostmi, vypis na konzoli
		// execute string::Normalize < str.xml | string::PatternMatch - < pattern.xml > match_result.xml // - je predchozi vysledek

		cli::Environment environment;
		environment.setBinding ( "1", "1" );

		testLine ( "execute One | Add <( Add (int) #1 <(One) ) - | Neg - > local/xxx.xml", environment );

		environment.setBinding ( "2", "local/xxx.xml" );

		testLine ( "execute One | Add <( Add (int) <#2 <(One) ) - | Neg (double) - | Divide (double) - <(One | (double) Add <(One) - )", environment );
		testLine ( "execute <:int #2", environment );
		testLine ( "execute One > $res", environment );
		testLine ( "execute $res", environment );
		CHECK_NOTHROW ( testLine ( "execute Divide <(One) <(One)", environment ) );

		abstraction::AlgorithmRegistry::unregisterAlgorithm < Divide, double, double > ( abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT );
		abstraction::AlgorithmRegistry::unregisterAlgorithm < Divide, int, int > ( abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT );

		CHECK_THROWS ( testLine ( "execute Divide <(One) <(One)", environment ) );
	}

	class Source {
	public:
		static std::unique_ptr < int > source ( ) {
			return std::make_unique < int > ( 1 );
		}
	};

	class Sink {
	public:
		static void sink ( std::unique_ptr < int > val ) {
			std::cout << * val << std::endl;
		}
	};

	SECTION ( "Test Move" ) {
		abstraction::AlgorithmRegistry::registerAlgorithm < Source > ( Source::source, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 0 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < Sink > ( Sink::sink, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 1 > ( ) );

		cli::Environment environment;
		testLine ( "execute Source | Move - | Sink -", environment );
	}

	static std::unique_ptr < int > source;
	static std::unique_ptr < int > target;

	class RvalueReferenceProvider {
	public:
		static std::unique_ptr < int > && foo ( ) {
			return std::move ( source );
		}
	};

	class RvalueReferenceAcceptor {
	public:
		static void bar ( std::unique_ptr < int > && out ) {
			target = std::move ( out );
			out = nullptr;
		}
	};

	SECTION ( "test Rvalue Reference Passing" ) {
		abstraction::AlgorithmRegistry::registerAlgorithm < RvalueReferenceProvider > ( RvalueReferenceProvider::foo, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 0 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < RvalueReferenceAcceptor > ( RvalueReferenceAcceptor::bar, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 1 > ( ) );

		{
			source = std::make_unique < int > ( 1 );
			cli::Environment environment;
			testLine ( "execute RvalueReferenceProvider | RvalueReferenceAcceptor -", environment );
		}

		CHECK ( * target == 1 );
		CHECK ( source == nullptr );

		{
			source = std::make_unique < int > ( 1 );
			cli::Environment environment;
			CHECK_NOTHROW ( testLine ( "execute RvalueReferenceProvider > $tmp", environment ) );
			CHECK_NOTHROW ( testLine ( "execute $tmp | Move - | RvalueReferenceAcceptor -", environment ) );
		}

		CHECK ( * target == 1 );
		CHECK ( source == nullptr );

		{
			source = std::make_unique < int > ( 1 );
			cli::Environment environment;
			CHECK_NOTHROW ( testLine ( "execute RvalueReferenceProvider > $tmp", environment ) );
			CHECK_THROWS ( testLine ( "execute $tmp | RvalueReferenceAcceptor -", environment ) );
		}

	}

	class ConstReferenceProvider {
	public:
		static const std::string & foo ( ) {
			static std::string dummy = "dummy";
			return dummy;
		}
	};

	class ConstReferenceAcceptor {
	public:
		static void bar ( const std::string & str ) {
			std::cout << str << std::endl;
		}
	};

	SECTION ( "Test Const Reference Passing" ) {
		abstraction::AlgorithmRegistry::registerAlgorithm < ConstReferenceProvider > ( ConstReferenceProvider::foo, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 0 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < ConstReferenceAcceptor > ( ConstReferenceAcceptor::bar, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 1 > ( ) );

		cli::Environment environment;
		testLine ( "execute ConstReferenceProvider | ConstReferenceAcceptor -", environment );
	}

	class ReferenceProvider {
	public:
		static std::ostream & foo ( ) {
			return std::cout;
		}
	};

	class ReferenceAcceptor {
	public:
		static void bar ( std::ostream & out ) {
			out << "yay" << std::endl;
		}
	};

	SECTION ( "Test Reference Passing" ) {
		abstraction::AlgorithmRegistry::registerAlgorithm < ReferenceProvider > ( ReferenceProvider::foo, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 0 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < ReferenceAcceptor > ( ReferenceAcceptor::bar, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 1 > ( ) );

		cli::Environment environment;
		testLine ( "execute ReferenceProvider | ReferenceAcceptor -", environment );
	}

	class ConstRvalueReferenceProvider {
	public:
		static const std::string && foo ( ) {
			static std::string dummy = "dummy";
			return std::move ( dummy );
		}
	};

	class ConstRvalueReferenceAcceptor {
	public:
		static void bar ( const std::string && str ) {
			std::cout << str << std::endl;
		}
	};

	SECTION ( "Test Const Rvalue Reference Passing" ) {
		abstraction::AlgorithmRegistry::registerAlgorithm < ConstRvalueReferenceProvider > ( ConstRvalueReferenceProvider::foo, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 0 > ( ) );
		abstraction::AlgorithmRegistry::registerAlgorithm < ConstRvalueReferenceAcceptor > ( ConstRvalueReferenceAcceptor::bar, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 1 > ( ) );

		cli::Environment environment;
		testLine ( "execute ConstRvalueReferenceProvider | Move - | ConstRvalueReferenceAcceptor -", environment );
	}

	class Print {
	public:
		static void print ( ext::set < int > theSet ) {
			for ( int value : theSet )
				std::cout << value << ", ";
			std::cout << std::endl;
		}
	};

	SECTION ( "Test Set Construction " ) {
		abstraction::AlgorithmRegistry::registerAlgorithm < Print > ( Print::print, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 1 > ( ) );

		cli::Environment environment;

		testLine ( "execute { :int 1 2 3 } > $set", environment );
		CHECK_THAT ( environment.getVariable ( "set" )->getType ( ), Catch::Matchers::StartsWith ( "ext::set<int," ) );

		testLine ( "execute $set | Print -", environment );

		testLine ( "execute $set >lo\\cal/yyy.xml", environment );

		testLine ( "execute < :set @int \"local/yyy.xml\" > $set2", environment );
		CHECK_THAT ( environment.getVariable ( "set2" )->getType ( ), Catch::Matchers::StartsWith ( "ext::set<int," ) );

		testLine ( "execute $set2 | Print -", environment );
	}

	SECTION ( "Test Member" ) {
		abstraction::AlgorithmRegistry::registerAlgorithm < Foo > ( Foo::make_foo, abstraction::AlgorithmCategories::AlgorithmCategory::DEFAULT, std::array < std::string, 1 > ( ) );
		abstraction::AlgorithmRegistry::registerMethod < Foo > ( & Foo::base, "base", std::array < std::string, 0 > ( ) );

		cli::Environment environment;
		testLine ( "execute Foo 3 | Foo::base -", environment );
		testLine ( "execute Foo 3 | Foo::bar - 2", environment );
	}

	SECTION ( "Test XML IO" ) {
		std::string in = "<Integer>1</Integer>";

		cli::Environment environment;
		environment.setVariable ( "in", in );
		testLine ( "execute sax::SaxParseInterface $in | Move - | xml::Parse - | Add <(One) - | xml::Compose - | sax::SaxComposeInterface - > $out", environment );
		std::string out = environment.getVariable < std::string > ( "out" );

		std::string ref = "<?xml version=\"1.0\"?>\n<Integer>2</Integer>\n";

		CAPTURE ( out, ref );
		CHECK ( out == ref );
	}
}
