// Copyright (c) 2021 Graphcore Ltd. All rights reserved.
#define BOOST_TEST_MODULE ExampleUnittest

#include <boost/test/unit_test.hpp>
#include <boost/trompeloeil.hpp>
#include <memory>
#include <stdexcept>
#include <type_traits>
#include <popart/logging.hpp>

/**
 * A dependency class. The class we want to test needs a reference to a class
 * of this kind. In this case, the functionality in our dependency class is a
 * 'foo' function that returns an int.
 *
 * NOTE: Dependencies should be interfaces so that it is possible to create
 * mock versions of them when testing the classes that depend on them.
 *
 * NOTE: Normally the dependencies involved in tests are defined in a header
 * in the code base somewhere.
 */
class Dependency {
public:
  virtual int foo() = 0;
  virtual ~Dependency() {}
};

/**
 * The class under tests in this example unit test. The class has a reference
 * to a class it depends on. This reference can be set via the 'setDependency'
 * method. When the 'bar' method is called ClassUnderTest will call its
 * dependency's 'foo' method and add 2.
 *
 * NOTE: Normally the classes under tests are defined in the code base and their
 * definitions are included via the appropriate header.
 */
class ClassUnderTest {
public:
  /**
   * Constructor.
   */
  ClassUnderTest() : dependency() {}

  /**
   * In unit tests it's good practice to test the code in ClassUnderTest --and
   * only the code in ClassUnderTest-- without simultaneously testing the
   * classes it depends on. In our example, we want to test that ClassUnderTest
   * calls the depenency and adds two to the result -- regardless of what the
   * dependency actually returns. To do this, we need a way to control the
   * dependency that ClassUnderTest uses during unit testing (this is called an
   * an Enabling Point in some literature).
   *
   * NOTE: We set dependencies in a separate method rather than the constructor
   * because without this it is not possible to have circular dependencies.
   *
   * NOTE: I've made the dependency a shared pointer with the view that for this
   * use case the dependency is not exclusively owned by ClassUnderTest.
   */
  void setDependency(const std::shared_ptr<Dependency> &dep) {
    dependency = dep;
  }

  /**
   * This method calls Dependency::foo() on the dependency passed to this object
   * and adds 2 to the result before returning. If the dependency is not set,
   * this method will throw an std::runtime_error.
   */
  virtual int bar() {
    if (!dependency) {
      throw std::runtime_error("[ClassUnderTest] Dependency not set.");
    }

    return dependency->foo() + 2;
  }

private:
  // Dependency.
  std::shared_ptr<Dependency> dependency;
};

/**
 * Mock class for Dependency. A mock class is an implementation of an interface
 * generated by a mocking framework through which we can test interactions
 * with said interface with relative ease.
 */
class MockDependency : public Dependency {
public:
  MAKE_MOCK0(foo, int(), override);
};

// Positive test. Test that the dependency is called and that if the dependency
// returns 5 the class under test returns 7.
BOOST_AUTO_TEST_CASE(example_unittest_pos_0) {

  auto test = [](int fooValue, int expectedBar) {
    // Setup.
    ClassUnderTest inst;
    auto dep = std::make_shared<MockDependency>();
    inst.setDependency(dep);

    // Set the expectation that mock.foo() is called. Also, instruct our mock
    // to return fooValue when called so we can test properly.
    REQUIRE_CALL(*dep, foo()).RETURN(fooValue);

    // Test that bar returns expectedBar.
    auto actualBar = inst.bar();
    BOOST_REQUIRE_MESSAGE(expectedBar == actualBar,
                          popart::logging::format(
                              "Expected ClassUnderTest::bar to return {} (got "
                              "{}).",
                              expectedBar,
                              actualBar));
  };

  {
    // Test params.
    auto fooValue    = 5;
    auto expectedBar = 7;

    // Run test.
    test(fooValue, expectedBar);
  }

  {
    // Test params.
    auto fooValue    = 113;
    auto expectedBar = 115;

    // Run test.
    test(fooValue, expectedBar);
  }
}

// Negative test. Test that if the dependency is not set the class under test
// throws a std::runtime_error when ClassUnderTest::bar is called.
BOOST_AUTO_TEST_CASE(example_unittest_err_0) {

  ClassUnderTest inst;

  // Test we get an exception if we call bar() without setting a dependency.
  BOOST_CHECK_EXCEPTION(inst.bar(),
                        std::runtime_error,
                        [](const std::runtime_error &err) { return true; });
}
