#include <gtest/gtest.h>
#include <vector>

#include "util/Exception.h"
#include "core/Core.h"
#include "core/CoreIncludes.h"
#include "core/module/PluginReference.h"
#include "core/commandline/CommandLineParser.h"
#include "core/command/ConsoleCommand.h"
#include "core/command/ConsoleCommandManager.h"

#include "Testclass.h"
#include "Testsingleton.h"

namespace orxonox
{
    namespace
    {
        // Fixture
        class PluginTest : public ::testing::Test
        {
            public:
                PluginTest()
                {
                    this->plugin_ = nullptr;
                }

                static void SetUpTestCase()
                {
                    new Core("--noIOConsole");
                }

                static void TearDownTestCase()
                {
                    delete &Core::getInstance();
                }

                void loadPlugin()
                {
                    this->plugin_ = new PluginReference("testplugin");
                }

                void unloadPlugin()
                {
                    delete this->plugin_;
                    this->plugin_ = nullptr;
                }

                virtual void TearDown() override
                {
                    // make sure the plugin is unloaded
                    this->unloadPlugin();
                }

            private:
                PluginReference* plugin_;
        };
    }


    TEST_F(PluginTest, CanLoadPlugin)
    {
        this->loadPlugin();
        this->unloadPlugin();
    }


    ///////////////////////////////////////////
    /////////////// Identifier ////////////////
    ///////////////////////////////////////////

    Identifier* getIdentifier()
    {
        return ClassByString("Testclass");
    }

    TEST_F(PluginTest, LoadsIdentifier)
    {
        EXPECT_TRUE(getIdentifier() == nullptr);
        this->loadPlugin();
        EXPECT_TRUE(getIdentifier() != nullptr);
        this->unloadPlugin();
    }

    TEST_F(PluginTest, UnloadsIdentifier)
    {
        this->loadPlugin();
        EXPECT_TRUE(getIdentifier() != nullptr);
        this->unloadPlugin();
        EXPECT_TRUE(getIdentifier() == nullptr);
    }

    TEST_F(PluginTest, ReloadsIdentifier)
    {
        this->loadPlugin();
        EXPECT_TRUE(getIdentifier() != nullptr);
        this->unloadPlugin();
        EXPECT_TRUE(getIdentifier() == nullptr);
        this->loadPlugin();
        EXPECT_TRUE(getIdentifier() != nullptr);
        this->unloadPlugin();
    }

    TEST_F(PluginTest, CanCreateObjectFromIdentifier)
    {
        this->loadPlugin();

        Identifier* identifier = getIdentifier();
        ASSERT_TRUE(identifier != nullptr);

        Identifiable* object = identifier->fabricate(nullptr);
        ASSERT_TRUE(object != nullptr);

        Testclass* testclass = orxonox_cast<Testclass*>(object);
        ASSERT_TRUE(testclass != nullptr);

        EXPECT_EQ(666, testclass->getValue());

        this->unloadPlugin();
    }


    ///////////////////////////////////////////
    //////////////// Singleton ////////////////
    ///////////////////////////////////////////

    // Cannot directly use Testsingleton::getInstance() because we don't link the test to the plugin.
    // Also cannot directly use ObjectList<Testsingleton> because the Identifier is not known before the plugin is loaded.
    Testsingleton* getSingleton()
    {
        std::vector<Testsingleton*> singletons;

        for (Listable* listable : ObjectList<Listable>())
        {
            Testsingleton* singleton = dynamic_cast<Testsingleton*>(listable);
            if (singleton)
                singletons.push_back(singleton);
        }

        switch (singletons.size())
        {
            case 0:
                return nullptr;
            case 1:
                return singletons[0];
            default:
                throw std::exception(); // unexpected number of singletons found
        }
    }

    TEST_F(PluginTest, LoadsSingleton)
    {
        EXPECT_TRUE(getSingleton() == nullptr);
        this->loadPlugin();
        EXPECT_TRUE(getSingleton() != nullptr);
        this->unloadPlugin();
    }

    TEST_F(PluginTest, UnloadsSingleton)
    {
        this->loadPlugin();
        EXPECT_TRUE(getSingleton() != nullptr);
        this->unloadPlugin();
        EXPECT_TRUE(getSingleton() == nullptr);
    }

    TEST_F(PluginTest, ReloadsSingleton)
    {
        this->loadPlugin();
        EXPECT_TRUE(getSingleton() != nullptr);
        this->unloadPlugin();
        EXPECT_TRUE(getSingleton() == nullptr);
        this->loadPlugin();
        EXPECT_TRUE(getSingleton() != nullptr);
        this->unloadPlugin();
    }

    TEST_F(PluginTest, CanCallFunctionOnSingleton)
    {
        this->loadPlugin();

        Testsingleton* singleton = getSingleton();
        ASSERT_TRUE(singleton != nullptr);

        EXPECT_EQ(999, singleton->getValue());

        this->unloadPlugin();
    }


    ///////////////////////////////////////////
    ////////// Command Line Argument //////////
    ///////////////////////////////////////////

    bool hasCommandLineArgument()
    {
        try
        {
            CommandLineParser::getValue("testvalue");
            return true;
        }
        catch (const ArgumentException&)
        {
            return false;
        }
    }

    TEST_F(PluginTest, LoadsCommandLineArgument)
    {
        EXPECT_FALSE(hasCommandLineArgument());
        this->loadPlugin();
        EXPECT_TRUE(hasCommandLineArgument());
        this->unloadPlugin();
    }

    TEST_F(PluginTest, UnloadsCommandLineArgument)
    {
        this->loadPlugin();
        EXPECT_TRUE(hasCommandLineArgument());
        this->unloadPlugin();
        EXPECT_FALSE(hasCommandLineArgument());
    }

    TEST_F(PluginTest, ReloadsCommandLineArgument)
    {
        this->loadPlugin();
        EXPECT_TRUE(hasCommandLineArgument());
        this->unloadPlugin();
        EXPECT_FALSE(hasCommandLineArgument());
        this->loadPlugin();
        EXPECT_TRUE(hasCommandLineArgument());
        this->unloadPlugin();
    }

    TEST_F(PluginTest, CommandLineArgumentHasCorrectValue)
    {
        this->loadPlugin();

        ASSERT_TRUE(hasCommandLineArgument());
        EXPECT_EQ(333, CommandLineParser::getValue("testvalue").get<int>());

        this->unloadPlugin();
    }


    ///////////////////////////////////////////
    ///////////// Console Command /////////////
    ///////////////////////////////////////////

    ConsoleCommand* getConsoleCommand()
    {
        return ConsoleCommandManager::getInstance().getCommand("testcommand");
    }

    TEST_F(PluginTest, LoadsConsoleCommand)
    {
        EXPECT_TRUE(getConsoleCommand() == nullptr);
        this->loadPlugin();
        EXPECT_TRUE(getConsoleCommand() != nullptr);
        this->unloadPlugin();
    }

    TEST_F(PluginTest, UnloadsConsoleCommand)
    {
        this->loadPlugin();
        EXPECT_TRUE(getConsoleCommand() != nullptr);
        this->unloadPlugin();
        EXPECT_TRUE(getConsoleCommand() == nullptr);
    }

    TEST_F(PluginTest, ReloadsConsoleCommand)
    {
        this->loadPlugin();
        EXPECT_TRUE(getConsoleCommand() != nullptr);
        this->unloadPlugin();
        EXPECT_TRUE(getConsoleCommand() == nullptr);
        this->loadPlugin();
        EXPECT_TRUE(getConsoleCommand() != nullptr);
        this->unloadPlugin();
    }

    TEST_F(PluginTest, CanCallConsoleCommand)
    {
        this->loadPlugin();

        ConsoleCommand* command = getConsoleCommand();
        ASSERT_TRUE(command != nullptr);

        EXPECT_EQ(999, (*command->getExecutor())(333, 666).get<int>());

        this->unloadPlugin();
    }
}
