// ./tests/catch2-tests [section] -s


/////////////////////// Stdlib includes


/////////////////////// Qt includes
#include <QDebug>
#include <QString>


/////////////////////// Catch2 includes
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>


/////////////////////// libXpertMassCore includes
#include "MsXpS/libXpertMassCore/Isotope.hpp"
#include "MsXpS/libXpertMassCore/IsotopicData.hpp"

/////////////////////// Local includes
#include "TestUtils.hpp"


namespace MsXpS
{
namespace libXpertMassCore
{

SCENARIO("Creation of an empty IsotopicData instance", "[IsotopicData]")
{
  TestUtils test_utils;

  test_utils.initializeXpertmassLibrary();

  WHEN("Creating an empty IsotopicData instance")
  {
    IsotopicData isotopic_data;

    THEN("It is created invalid")
    {
      REQUIRE_FALSE(isotopic_data.isValid());
    }
  }
}

SCENARIO(
  "Adding Isotope instances to an IsotopicData instance without map updating",
  "[IsotopicData]")
{
  TestUtils test_utils;

  QVector<QString> error_list;

  GIVEN("An empty IsotopicData instance and some Isotope instances")
  {
    IsotopicData isotopic_data;

    REQUIRE(test_utils.msp_isotopeC12->isValid());
    REQUIRE(test_utils.msp_isotopeC12->validate(&error_list));
    REQUIRE(test_utils.msp_isotopeC13->isValid());
    REQUIRE(test_utils.msp_isotopeC13->validate(&error_list));
    REQUIRE(test_utils.msp_isotopeN14->isValid());
    REQUIRE(test_utils.msp_isotopeN14->validate(&error_list));
    REQUIRE(test_utils.msp_isotopeN15->isValid());
    REQUIRE(test_utils.msp_isotopeN15->validate(&error_list));

    QList<IsotopeQSPtr> isotopes;
    // isotopes.push_back(test_utils.msp_isotopeC12);
    // isotopes.push_back(test_utils.msp_isotopeC13);
    isotopes.push_back(test_utils.msp_isotopeN14);
    isotopes.push_back(test_utils.msp_isotopeN15);

    THEN("All the containers are empty")
    {
      REQUIRE(isotopic_data.getIsotopes().size() == 0);
      REQUIRE(isotopic_data.getUniqueSymbolsCount() == 0);
      REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().size() == 0);
      REQUIRE(isotopic_data.updateMassMaps() == 0);
    }

    WHEN("Appending a new Isotope instance without updating the maps")
    {
      isotopic_data.appendNewIsotope(test_utils.msp_isotopeC12,
                                     /*update_maps*/ false);

      THEN("It has that new Isotope in the m_isotopes member")
      {
        REQUIRE(isotopic_data.size() == 1);
        REQUIRE(isotopic_data.getUniqueSymbolsCount() == 1);
        REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().size() == 1);
        REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder()
                  .front()
                  .toStdString() == "C");
        REQUIRE(isotopic_data.validate(&error_list));
      }

      THEN("It cannot validate because there is only one isotope")
      {
        REQUIRE_FALSE(isotopic_data.validateBySymbol("C", &error_list));
      }

      THEN(
        "Getting masses is not possible because the maps have not been updated")
      {
        bool ok = false;

        REQUIRE(isotopic_data.getMonoMassBySymbol("C", ok) == 0);
        REQUIRE_FALSE(ok);
        REQUIRE(isotopic_data.getAvgMassBySymbol("C", ok) == 0);
        REQUIRE_FALSE(ok);
      }

      WHEN("Adding the second carbon isotope without updating the maps")
      {
        isotopic_data.appendNewIsotope(test_utils.msp_isotopeC13,
                                       /*update_maps*/ false);

        THEN("Still cannot get masses")
        {
          bool ok = false;

          REQUIRE(isotopic_data.getMonoMassBySymbol("C", ok) == 0);
          REQUIRE_FALSE(ok);
          REQUIRE(isotopic_data.getAvgMassBySymbol("C", ok) == 0);
          REQUIRE_FALSE(ok);
        }

        AND_WHEN("Updating the maps explicitely")
        {
          REQUIRE(isotopic_data.updateMassMaps() == 1);
          isotopic_data.updateMassMaps("C");

          THEN("The masses finally become available")
          {
            bool ok = false;

            REQUIRE_THAT(isotopic_data.getMonoMassBySymbol("C", ok),
                         Catch::Matchers::WithinAbs(12.00000, 0.0000000001));
            REQUIRE_THAT(
              isotopic_data.getAvgMassBySymbol("C", ok),
              Catch::Matchers::WithinAbs(12.0108242503, 0.0000000001));
          }

          THEN("Cumulated probabilities for isotopes C and N equal 1")
          {
            REQUIRE(isotopic_data.getCumulatedProbabilitiesBySymbol(
                      "C", &error_list) == 1);
          }
        }

        AND_WHEN("Adding the two isotopes of N as a vector with maps updating")
        {
          isotopic_data.appendNewIsotopes(isotopes, /*update maps*/ true);

          THEN("The full set of isotopes for C and N can be tested")
          {
            REQUIRE(isotopic_data.getIsotopeCountBySymbol("C") == 2);
            REQUIRE(isotopic_data.getIsotopeCountBySymbol("N") == 2);

            REQUIRE(isotopic_data.getCumulatedProbabilitiesBySymbol(
                      "C", &error_list) == 1);
            REQUIRE(isotopic_data.getCumulatedProbabilitiesBySymbol(
                      "N", &error_list) == 1);
          }
        }
      }
    }
  }
}

SCENARIO(
  "Adding Isotope instances to an IsotopicData instance with map updating",
  "[IsotopicData]")
{
  TestUtils test_utils;

  QVector<QString> error_list;

  IsotopicData isotopic_data;

  REQUIRE(test_utils.msp_isotopeC12->isValid());
  REQUIRE(test_utils.msp_isotopeC12->validate(&error_list));
  REQUIRE(test_utils.msp_isotopeC13->isValid());
  REQUIRE(test_utils.msp_isotopeC13->validate(&error_list));
  REQUIRE(test_utils.msp_isotopeN14->isValid());
  REQUIRE(test_utils.msp_isotopeN14->validate(&error_list));
  REQUIRE(test_utils.msp_isotopeN15->isValid());
  REQUIRE(test_utils.msp_isotopeN15->validate(&error_list));

  QList<IsotopeQSPtr> isotopes;
  // isotopes.push_back(test_utils.msp_isotopeC12);
  isotopes.push_back(test_utils.msp_isotopeC13);
  isotopes.push_back(test_utils.msp_isotopeN14);
  isotopes.push_back(test_utils.msp_isotopeN15);

  GIVEN("An empty IsotopicData instance and some Isotope instances")
  {

    WHEN("Appending a new Isotope instance with maps update")
    {
      isotopic_data.appendNewIsotope(test_utils.msp_isotopeC12);

      THEN("It has that new Isotope in the m_isotopes member")
      {
        REQUIRE(isotopic_data.size() == 1);
        REQUIRE(isotopic_data.getUniqueSymbolsCount() == 1);
        REQUIRE(isotopic_data.validate(&error_list));
      }

      THEN("It cannot validate because there is only one isotope")
      {
        REQUIRE_FALSE(isotopic_data.validateBySymbol("C", &error_list));
      }

      THEN("Getting masses is possible because the maps have been updated")
      {
        bool ok = false;

        REQUIRE(isotopic_data.getMonoMassBySymbol("C", ok) == 12.0);
        REQUIRE(ok);
        REQUIRE(isotopic_data.getAvgMassBySymbol("C", ok) == 12.0);
        REQUIRE(ok);
      }

      AND_WHEN("Appending new Isotope instances in one go")
      {
        isotopic_data.appendNewIsotopes(isotopes, /*update_maps*/ true);

        THEN("All sorts of validations should be successful")
        {
          REQUIRE(isotopic_data.validate(&error_list));
          REQUIRE(isotopic_data.validateBySymbol("C", &error_list));
          REQUIRE(isotopic_data.validateBySymbol("N", &error_list));

          int count = 0;

          REQUIRE(isotopic_data.containsSymbol("C", count));
          REQUIRE(count == 2);
          REQUIRE(isotopic_data.containsSymbol("N", count));
          REQUIRE(count == 2);

          REQUIRE(isotopic_data.containsName("carbon", count));
          REQUIRE(count == 2);
          REQUIRE(isotopic_data.containsName("nitrogen", count));
          REQUIRE(count == 2);

          REQUIRE(isotopic_data.getIsotopeCountBySymbol("C") == 2);
          REQUIRE(isotopic_data.getIsotopeCountBySymbol("N") == 2);

          REQUIRE(isotopic_data.getCumulatedProbabilitiesBySymbol(
                    "C", &error_list) == 1);
          REQUIRE(isotopic_data.getCumulatedProbabilitiesBySymbol(
                    "N", &error_list) == 1);
        }
      }
    }
  }

  isotopic_data.clear();
  isotopes.clear();

  GIVEN("An isotopic data instance that has been cleared.")
  {
    THEN("The various member containers should be back emtpy")
    {
      REQUIRE(isotopic_data.getIsotopes().size() == 0);
      REQUIRE(isotopic_data.getUniqueSymbolsCount() == 0);
      REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().size() == 0);
      REQUIRE(isotopic_data.updateMassMaps() == 0);
    }
  }

  GIVEN(
    "Addition of a set of three isotopes in a row (C12,*not C13*,N14,N15) with "
    "maps update")
  {
    isotopes.push_back(test_utils.msp_isotopeC12);
    isotopes.push_back(test_utils.msp_isotopeN14);
    isotopes.push_back(test_utils.msp_isotopeN15);

    isotopic_data.appendNewIsotopes(isotopes, /* update maps */ true);

    THEN("The various container members should contain multiple items.")
    {
      REQUIRE(isotopic_data.getIsotopes().size() == 3);
      REQUIRE(isotopic_data.getUniqueSymbolsCount() == 2);
      REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().size() == 2);
      REQUIRE(isotopic_data.updateMassMaps() == 2);
    }

    THEN(
      "Checking the index of the C12 isotope and inserting the missing C13 "
      "isotope")
    {
      IsotopeListCstIteratorPair iter_pair =
        isotopic_data.getIsotopesBySymbol("C");
      std::size_t index =
        std::distance(std::begin(isotopic_data.getIsotopes()), iter_pair.first);

      REQUIRE(index == 0);
      // We insert C13 right after C12, so that they are in the right order: C12
      // and C13.
      isotopic_data.insertNewIsotope(
        test_utils.msp_isotopeC13, index + 1, /*update maps*/ true);

      AND_THEN(
        "The two C isotopes should come in order C12 and C13 "
        "with proper masses and probabilities.")
      {
        iter_pair = isotopic_data.getIsotopesBySymbol("C");

        // Check by the Isotope instances themselves.
        REQUIRE(iter_pair.first->get()->getMass() == 12.0);
        REQUIRE_THAT(std::prev(iter_pair.second)->get()->getMass(),
                     Catch::Matchers::WithinAbs(13.0033548352, 0.0000000001));

        // Check by the symbol-based iterator pair, which ensures that
        // the average mass calculation involved two immediately consecutive
        // isotopes.
        REQUIRE_THAT(isotopic_data.getMonoMass(iter_pair, &error_list),
                     Catch::Matchers::WithinAbs(12.0, 0.0000000001));
        REQUIRE_THAT(isotopic_data.computeAvgMass(iter_pair, &error_list),
                     Catch::Matchers::WithinAbs(12.0108242503, 0.0000000001));

        REQUIRE(
          isotopic_data.getCumulatedProbabilities(iter_pair, &error_list) == 1);

        REQUIRE_THAT(
          iter_pair.first->get()->getProbability(),
          Catch::Matchers::WithinAbs(
            0.989211941850466902614869013632414862513542175292968750000000,
            0.0000000001));
        REQUIRE_THAT(
          std::prev(iter_pair.second)->get()->getProbability(),
          Catch::Matchers::WithinAbs(
            0.010788058149533083507343178553128382191061973571777343750000,
            0.0000000001));

        bool ok = false;
        REQUIRE_THAT(isotopic_data.getAvgMassBySymbol("C", ok),
                     Catch::Matchers::WithinAbs(12.0108242503, 0.0000000001));
      }

      AND_THEN(
        "Let's check that the average mass is correct also for the N symbol")
      {
        bool ok   = false;
        iter_pair = isotopic_data.getIsotopesBySymbol("N");

        // Check by the Isotope instances themselves.
        REQUIRE(iter_pair.first->get()->getMass() == 14.0030740042);
        REQUIRE_THAT(std::prev(iter_pair.second)->get()->getMass(),
                     Catch::Matchers::WithinAbs(15.0001088994, 0.0000000001));

        // Check by the symbol-based iterator pair, which ensures that
        // the average mass calculation involved two immediately consecutive
        // isotopes.
        REQUIRE_THAT(isotopic_data.getMonoMass(iter_pair, &error_list),
                     Catch::Matchers::WithinAbs(14.0030740042, 0.0000000001));
        REQUIRE_THAT(isotopic_data.computeAvgMass(iter_pair, &error_list),
                     Catch::Matchers::WithinAbs(14.0067051908, 0.0000000001));

        REQUIRE_THAT(isotopic_data.getAvgMassBySymbol("N", ok),
                     Catch::Matchers::WithinAbs(14.0067051908, 0.0000000001));

        REQUIRE(
          isotopic_data.getCumulatedProbabilities(iter_pair, &error_list) == 1);
      }
    }
  }
}

SCENARIO("Accessing Isotope instances using iterators", "[IsotopicData]")
{
  TestUtils test_utils;

  QList<IsotopeQSPtr> isotopes;
  isotopes.push_back(test_utils.msp_isotopeC12);
  isotopes.push_back(test_utils.msp_isotopeC13);
  isotopes.push_back(test_utils.msp_isotopeN14);
  isotopes.push_back(test_utils.msp_isotopeN15);

  IsotopicData isotopic_data;

  isotopic_data.appendNewIsotopes(isotopes, /*update_maps*/ true);

  QVector<QString> error_list;

  GIVEN("An IsotopicData instance with the four C and N isotopes")
  {
    isotopic_data.appendNewIsotopes(isotopes, /*update_maps*/ true);

    WHEN("Asking for isotopes of 'carbon' via iterators")
    {
      IsotopeListCstIteratorPair iter_pair =
        isotopic_data.getIsotopesByName("carbon");

      THEN("The masses might be asked")
      {
        REQUIRE_THAT(isotopic_data.getMonoMass(iter_pair, &error_list),
                     Catch::Matchers::WithinAbs(12.0, 0.0000000001));
        REQUIRE_THAT(isotopic_data.computeAvgMass(iter_pair, &error_list),
                     Catch::Matchers::WithinAbs(12.0108242503, 0.0000000001));
        REQUIRE_THAT(
          isotopic_data.getCumulatedProbabilities(iter_pair, &error_list),
          Catch::Matchers::WithinAbs(1, 0.0000000001));
      }
    }

    WHEN("Asking for isotopes of 'C' via iterators")
    {
      IsotopeListCstIteratorPair iter_pair =
        isotopic_data.getIsotopesBySymbol("C");

      THEN("The masses might be asked")
      {
        REQUIRE_THAT(isotopic_data.getMonoMass(iter_pair, &error_list),
                     Catch::Matchers::WithinAbs(12.0, 0.0000000001));
        REQUIRE_THAT(isotopic_data.computeAvgMass(iter_pair, &error_list),
                     Catch::Matchers::WithinAbs(12.0108242503, 0.0000000001));
        REQUIRE_THAT(
          isotopic_data.getCumulatedProbabilities(iter_pair, &error_list),
          Catch::Matchers::WithinAbs(1, 0.0000000001));
      }
    }
  }
}

SCENARIO("Isotopes can be removed from IsotopicData", "[IsotopicData]")
{
  TestUtils test_utils;

  QList<IsotopeQSPtr> isotopes;
  isotopes.push_back(test_utils.msp_isotopeC12);
  isotopes.push_back(test_utils.msp_isotopeC13);
  isotopes.push_back(test_utils.msp_isotopeN14);
  isotopes.push_back(test_utils.msp_isotopeN15);

  IsotopicData isotopic_data;

  bool ok;
  QVector<QString> error_list;

  WHEN("There are no isotopes in the isotopic data, no isotope can be removed")
  {
    QList<IsotopeQSPtr>::const_iterator iterator;

    iterator = isotopic_data.eraseIsotopes(0, 20, false);
    REQUIRE(iterator == isotopic_data.getIsotopes().cend());
  }

  WHEN("The isotopic data are filled with the four (C12,C13,N14,N15) isotopes")
  {
    isotopic_data.appendNewIsotopes(isotopes, /* update maps */ true);

    THEN("The sizes of the containers can be checked")
    {
      REQUIRE(isotopic_data.getIsotopes().size() == 4);
      REQUIRE(isotopic_data.getUniqueSymbolsCount() == 2);
      REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().size() == 2);
      REQUIRE(isotopic_data.updateMassMaps() == 2);
    }

    AND_THEN("Erasing an isotope out-of-bound fails")
    {
      QList<IsotopeQSPtr>::const_iterator iterator;

      iterator = isotopic_data.eraseIsotopes(4, 20, false);
      REQUIRE(iterator == isotopic_data.getIsotopes().cend());
    }

    AND_THEN("If the two C isotopes are erased, the container sizes change")
    {
      isotopic_data.eraseIsotopes(0, 1, /*update maps*/ true);

      // qDebug() << "The new size: " << isotopic_data.getIsotopes().size();

      REQUIRE(isotopic_data.getIsotopes().size() == 2);
      REQUIRE(isotopic_data.getUniqueSymbolsCount() == 1);
      REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().size() == 1);
      REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().at(0) == "N");
      REQUIRE(isotopic_data.updateMassMaps() == 1);
    }

    AND_THEN("Check the masses for the remaining N isotopes")
    {
      IsotopeListCstIteratorPair iter_pair =
        isotopic_data.getIsotopesBySymbol("N");

      REQUIRE(std::distance(iter_pair.first, iter_pair.second) == 2);

      // Check by the Isotope instances themselves.
      REQUIRE(iter_pair.first->get()->getMass() == 14.0030740042);
      REQUIRE_THAT(std::prev(iter_pair.second)->get()->getMass(),
                   Catch::Matchers::WithinAbs(15.0001088994, 0.0000000001));

      // Check by the symbol-based iterator pair, which ensures that
      // the average mass calculation involved two immediately consecutive
      // isotopes.
      REQUIRE_THAT(isotopic_data.getMonoMass(iter_pair, &error_list),
                   Catch::Matchers::WithinAbs(14.0030740042, 0.0000000001));
      REQUIRE_THAT(isotopic_data.computeAvgMass(iter_pair, &error_list),
                   Catch::Matchers::WithinAbs(14.0067051908, 0.0000000001));

      REQUIRE_THAT(isotopic_data.getAvgMassBySymbol("N", ok),
                   Catch::Matchers::WithinAbs(14.0067051908, 0.0000000001));

      REQUIRE(isotopic_data.getCumulatedProbabilities(iter_pair, &error_list) ==
              1);
    }
  }
}

SCENARIO("IsotopicData can be copied, replaced and compared", "[IsotopicData]")
{
  TestUtils test_utils;

  QList<IsotopeQSPtr> isotopes;
  isotopes.push_back(test_utils.msp_isotopeN14);
  isotopes.push_back(test_utils.msp_isotopeN15);

  IsotopicData isotopic_data;

  WHEN("The isotopic data are filled with the two (N14,N15) isotopes")
  {
    isotopic_data.appendNewIsotopes(isotopes);

    THEN("The sizes of the containers can be checked")
    {
      REQUIRE(isotopic_data.getIsotopes().size() == 2);
      REQUIRE(isotopic_data.getUniqueSymbolsCount() == 1);
      REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().size() == 1);
      REQUIRE(isotopic_data.updateMassMaps() == 1);
    }

    AND_THEN("We test the copy constructor")
    {
      IsotopicData isotopic_data_1(isotopic_data);
      REQUIRE(isotopic_data_1 == isotopic_data);
    }

    AND_THEN("We test the assignement operator")
    {
      IsotopicData isotopic_data_1(isotopic_data);
      REQUIRE(isotopic_data_1 == isotopic_data);
    }

    AND_THEN("We replace one isotope by another and check")
    {
      isotopic_data.replace(test_utils.msp_isotopeC12,
                            test_utils.msp_isotopeN14);
      REQUIRE(isotopic_data.getIsotopes().at(0)->getName().toStdString() ==
              "nitrogen");
    }
  }
}

SCENARIO("IsotopicData can be validated in various ways", "[IsotopicData]")
{
  TestUtils test_utils;

  IsotopicData isotopic_data;

  QVector<QString> error_list;

  QList<IsotopeQSPtr> isotopes;
  isotopes.push_back(test_utils.msp_isotopeC12);
  isotopes.push_back(test_utils.msp_isotopeC13);
  isotopes.push_back(test_utils.msp_isotopeN14);
  isotopes.push_back(test_utils.msp_isotopeN15);

  isotopic_data.appendNewIsotopes(isotopes, /* update maps */ true);

  WHEN("The isotopic data are filled with the four (C12,C13,N14,N15) isotopes")
  {

    THEN("The sizes of the containers can be checked")
    {
      REQUIRE(isotopic_data.getIsotopes().size() == 4);
      REQUIRE(isotopic_data.getUniqueSymbolsCount() == 2);
      REQUIRE(isotopic_data.getUniqueSymbolsInOriginalOrder().size() == 2);
      REQUIRE(isotopic_data.updateMassMaps() == 2);
    }

    AND_THEN("The isotopic data can be validated generically")
    {
      bool result = isotopic_data.validate(&error_list);
      REQUIRE(result);
      REQUIRE(error_list.size() == 0);
    }

    AND_THEN(
      "The isotopic data can be validated by symbol (more powerful checks)")
    {
      bool result = isotopic_data.validateBySymbol("C", &error_list);
      REQUIRE(result);
      REQUIRE(error_list.size() == 0);

      result = isotopic_data.validateBySymbol("N", &error_list);
      REQUIRE(result);
      REQUIRE(error_list.size() == 0);
    }
  }
}

} // namespace libXpertMassCore
} // namespace MsXpS
