//
// microtest.h
//
// URL: https://github.com/torpedro/microtest.h
// Author: Pedro Flemming (http://torpedro.com/)
// License: MIT License (https://github.com/torpedro/microtest.h/blob/master/LICENSE)
// Copyright (c) 2017 Pedro Flemming
//
// This is a small header-only C++ unit testing framework.
// It allows to define small unit tests with set of assertions available.
//
#ifndef __MICROTEST_H__
#define __MICROTEST_H__

#include <vector>
#include <string>
#include <iostream>
#include <algorithm>

////////////////
// Assertions //
////////////////

#define ASSERT(cond)\
  ASSERT_TRUE(cond);

#define ASSERT_TRUE(cond)\
  if (!(cond)) throw mt::AssertFailedException(#cond, __FILE__, __LINE__);

#define ASSERT_FALSE(cond)\
  if (cond) throw mt::AssertFailedException(#cond, __FILE__, __LINE__);

#define ASSERT_NULL(value)\
  ASSERT_TRUE(value == NULL);

#define ASSERT_NOTNULL(value)\
  ASSERT_TRUE(value != NULL);

#define ASSERT_STREQ(a, b)\
  if (std::string(a).compare(std::string(b)) != 0) {\
    printf("%s{    info} %s", mt::yellow(), mt::def());\
    std::cout << "Actual values: " << a << " != " << b << std::endl;\
    throw mt::AssertFailedException(#a " == " #b, __FILE__, __LINE__);\
  }

#define ASSERT_STRNEQ(a, b)\
  if (std::string(a).compare(std::string(b)) == 0) {\
    printf("%s{    info} %s", mt::yellow(), mt::def());\
    std::cout << "Actual values: " << a << " == " << b << std::endl;\
    throw mt::AssertFailedException(#a " != " #b, __FILE__, __LINE__);\
  }

#define ASSERT_EQ(a, b)\
  if (a != b) {\
    printf("%s{    info} %s", mt::yellow(), mt::def());\
    std::cout << "Actual values: " << a << " != " << b << std::endl;\
  }\
  ASSERT(a == b);

#define ASSERT_NEQ(a, b)\
  if (a == b) {\
    printf("%s{    info} %s", mt::yellow(), mt::def());\
    std::cout << "Actual values: " << a << " == " << b << std::endl;\
  }\
  ASSERT(a != b);


////////////////
// Unit Tests //
////////////////

#define TEST(name) \
  void name();\
  namespace {\
    bool __##name = mt::TestsManager::AddTest(name, #name);\
  }\
  void name()


///////////////
// Framework //
///////////////

namespace mt {

  inline const char* red() {
    return "\033[1;31m";
  }

  inline const char* green() {
    return "\033[0;32m";
  }

  inline const char* yellow() {
    return "\033[0;33m";
  }

  inline const char* def() {
    return "\033[0m";
  }

  inline void printRunning(const char* message, FILE* file = stdout) {
    fprintf(file, "%s{ running}%s %s\n", green(), def(), message);
  }

  inline void printOk(const char* message, FILE* file = stdout) {
    fprintf(file, "%s{      ok}%s %s\n", green(), def(), message);
  }

  inline void printFailed(const char* message, FILE* file = stdout) {
    fprintf(file, "%s{  failed} %s%s\n", red(), message, def());
  }

  // Exception that is thrown when an assertion fails.
  class AssertFailedException : public std::exception {
   public:
    AssertFailedException(std::string description, std::string filepath, int line) :
      std::exception(),
      description_(description),
      filepath_(filepath),
      line_(line) {};

    virtual const char* what() const throw() {
      return description_.c_str();
    }

    inline const char* getFilepath() {
      return filepath_.c_str();
    }

    inline int getLine() {
      return line_;
    }

   protected:
    std::string description_;
    std::string filepath_;
    int line_;
  };

  class TestsManager {
    // Note: static initialization fiasco
    // http://www.parashift.com/c++-faq-lite/static-init-order.html
    // http://www.parashift.com/c++-faq-lite/static-init-order-on-first-use.html
   public:
    struct Test {
      const char* name;
      void (*fn)(void);
    };

    static std::vector<Test>& tests() {
      static std::vector<Test> tests_;
      return tests_;
    }

    // Adds a new test to the current set of tests.
    // Returns false if a test with the same name already exists.
    inline static bool AddTest(void (*fn)(void), const char* name) {
      tests().push_back({ name, fn });
      return true;
    }

    // Run all tests that are registered.
    // Returns the number of tests that failed.
    inline static size_t RunAllTests(FILE* file = stdout) {
      size_t num_failed = 0;

      for (const Test& test : tests()) {
        // Run the test.
        // If an AsserFailedException is thrown, the test has failed.
        try {
          printRunning(test.name, file);

          (*test.fn)();

          printOk(test.name, file);

        } catch (AssertFailedException& e) {
          printFailed(test.name, file);
          fprintf(file, "           %sAssertion failed: %s%s\n",
                  red(), e.what(), def());
          fprintf(file, "           %s%s:%d%s\n",
                  red(), e.getFilepath(), e.getLine(), def());
          ++num_failed;
        }
      }

      int return_code = (num_failed > 0) ? 1 : 0;
      return return_code;
    }
  };

  // Class that will capture the arguments passed to the program.
  class Runtime {
   public:
    static const std::vector<std::string>& args(int argc = -1, char** argv = NULL) {
      static std::vector<std::string> args_;
      if (argc >= 0) {
        for (int i = 0; i < argc; ++i) {
          args_.push_back(argv[i]);
        }
      }
      return args_;
    }
  };
}


#define TEST_MAIN() \
  int main(int argc, char *argv[]) {\
    mt::Runtime::args(argc, argv);\
    \
    size_t num_failed = mt::TestsManager::RunAllTests(stdout);\
    if (num_failed == 0) {\
      fprintf(stdout, "%s{ summary} All tests succeeded!%s\n", mt::green(), mt::def());\
      return 0;\
    } else {\
      double percentage = 100.0 * num_failed / mt::TestsManager::tests().size();\
      fprintf(stderr, "%s{ summary} %lu tests failed (%.2f%%)%s\n", mt::red(), num_failed, percentage, mt::def());\
      return -1;\
    }\
  }

#endif // __MICROTEST_H__
