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


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


/////////////////////// IsoSpec
#include <IsoSpec++/isoSpec++.h>
#include <IsoSpec++/element_tables.h>


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


/////////////////////// Local includes
// #include "tests-config.h"
#include "TestUtils.hpp"
#include "MsXpS/libXpertMassCore/globals.hpp"
#include "MsXpS/libXpertMassCore/Polymer.hpp"
#include "MsXpS/libXpertMassCore/Oligomer.hpp"


namespace MsXpS
{
namespace libXpertMassCore
{

TestUtils test_utils_1_letter_oligomer("protein-1-letter", 1);

ErrorList error_list_oligomer;

SCENARIO("Oligomer instances are initialized with a reference Polymer",
         "[Oligomer]")
{
  test_utils_1_letter_oligomer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_oligomer.msp_polChemDef;

  QString polymer_file_path = QString("%1/polymer-sequences/%2")
                                .arg(TESTS_INPUT_DIR)
                                .arg("chicken-telokin.mxp");

  PolymerQSPtr polymer_sp = Polymer::createSPtr(pol_chem_def_csp);

  REQUIRE(polymer_sp->renderXmlPolymerFile(polymer_file_path));

  REQUIRE(polymer_sp->isLeftEndModified());
  REQUIRE(polymer_sp->getLeftEndModifCstRef().getName().toStdString() ==
          "Acetylation");
  REQUIRE(polymer_sp->getRightEndModifCstRef().getName().toStdString() ==
          "AmidationGlu");
  REQUIRE(polymer_sp->isRightEndModified());
  REQUIRE(polymer_sp->size() == 157);
  REQUIRE(polymer_sp->getSequenceCstRef().hasModifiedMonomer(0, 156));
  std::vector<std::size_t> indices =
    polymer_sp->getSequenceCstRef().modifiedMonomerIndices(0, 156);
  REQUIRE(indices.size() == 1);
  REQUIRE(polymer_sp->getSequenceCstRef()
            .getMonomerCstRPtrAt(indices.front())
            ->getModifsCstRef()
            .front()
            ->getName()
            .toStdString() == "Phosphorylation");

  GIVEN("A polymer sequence file loaded from disk")
  {

    WHEN("Masses are calculated with default parameters")
    {
      CalcOptions calc_options(/*deep_calculation*/ false,
                               /*mass_type*/ Enums::MassType::BOTH,
                               /*capping*/ Enums::CapType::BOTH,
                               /*monomer_entities*/ Enums::ChemicalEntity::NONE,
                               /*polymer_entities*/ Enums::ChemicalEntity::NONE);
      calc_options.setIndexRange(IndexRange(0, 156));

      REQUIRE(polymer_sp->calculateMasses(calc_options, /*reset*/ true));

      THEN("The masses are checked and are as expected")
      {
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::MONO),
          Catch::Matchers::WithinAbs(17325.7923591224, 0.0000000001));
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::AVG),
          Catch::Matchers::WithinAbs(17336.8283764289, 0.0000000001));
      }
    }

    AND_GIVEN("An Oligomer and an Ionizer")
    {
      Oligomer oligomer(polymer_sp, "Oligomername", "OligomerDescription");

      Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                      "\"protonation\"+H",
                      /*charge*/ 1,
                      /*level*/ 1);
      oligomer.setIonizer(ionizer);

      THEN("The member data should be set correctly")
      {
        REQUIRE(oligomer.getPolymerCstSPtr() == polymer_sp);
        REQUIRE(oligomer.getName().toStdString() == "Oligomername");
        REQUIRE(oligomer.getDescription().toStdString() ==
                "OligomerDescription");

        Ionizer the_ionizer = oligomer.getIonizerCstRef();

        REQUIRE(the_ionizer == ionizer);
        REQUIRE_FALSE(oligomer.getIonizerCstRef().isIonized());
        REQUIRE_FALSE(oligomer.isModified());
        REQUIRE_FALSE(oligomer.isModified(/*deep*/ true));
        REQUIRE(oligomer.modifiedMonomerCount() == 0);
      }

      THEN(
        "Masses should not be calculatable with default parameters (no "
        "IndexRange defined))")
      {
        REQUIRE_FALSE(oligomer.calculateMasses());

        REQUIRE_THAT(oligomer.getMass(Enums::MassType::MONO),
                     Catch::Matchers::WithinAbs(0, 0.0000000001));
        REQUIRE_THAT(oligomer.getMass(Enums::MassType::AVG),
                     Catch::Matchers::WithinAbs(0, 0.0000000001));
      }

      WHEN(
        "Defining proper IndexRanges and calculating masses with the "
        "default CalcOptions member in the Oligomer and the set Ionizer")
      {
        std::size_t polymer_size = polymer_sp->size();
        REQUIRE(polymer_size == 157);

        IndexRange whole_polymer_index_range(0, 156);
        oligomer.getCalcOptionsRef().setIndexRange(whole_polymer_index_range);

        REQUIRE(oligomer.calculateMasses());

        THEN("Masses should be calculated correctly")
        {
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::MONO),
            Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::AVG),
            Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
        }

        AND_WHEN(
          "Ionizing the Oligomer should be performed by first deionizing it "
          "since it is already ionized")
        {
          REQUIRE(oligomer.ionize() == Enums::IonizationOutcome::IONIZED);

          THEN("Masses should not be modified")
          {
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::MONO),
              Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::AVG),
              Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
          }
        }
      }
    }
  }
}

SCENARIO("Oligomer instances can be ionized,  deionized and reionized",
         "[Oligomer]")
{
  test_utils_1_letter_oligomer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_oligomer.msp_polChemDef;

  QString polymer_file_path = QString("%1/polymer-sequences/%2")
                                .arg(TESTS_INPUT_DIR)
                                .arg("chicken-telokin.mxp");
  PolymerQSPtr polymer_sp = Polymer::createSPtr(pol_chem_def_csp);

  REQUIRE(polymer_sp->renderXmlPolymerFile(polymer_file_path));

  REQUIRE(polymer_sp->isLeftEndModified());
  REQUIRE(polymer_sp->getLeftEndModifCstRef().getName().toStdString() ==
          "Acetylation");
  REQUIRE(polymer_sp->getRightEndModifCstRef().getName().toStdString() ==
          "AmidationGlu");
  REQUIRE(polymer_sp->isRightEndModified());
  REQUIRE(polymer_sp->size() == 157);
  REQUIRE(polymer_sp->getSequenceCstRef().hasModifiedMonomer(0, 156));
  std::vector<std::size_t> indices =
    polymer_sp->getSequenceCstRef().modifiedMonomerIndices(0, 156);
  REQUIRE(indices.size() == 1);
  REQUIRE(polymer_sp->getSequenceCstRef()
            .getMonomerCstRPtrAt(indices.front())
            ->getModifsCstRef()
            .front()
            ->getName()
            .toStdString() == "Phosphorylation");

  GIVEN("A polymer sequence file loaded from disk")
  {
    WHEN("Masses are calculated with default parameters")
    {
      CalcOptions calc_options(/*deep_calculation*/ false,
                               /*mass_type*/ Enums::MassType::BOTH,
                               /*capping*/ Enums::CapType::BOTH,
                               /*monomer_entities*/ Enums::ChemicalEntity::NONE,
                               /*polymer_entities*/ Enums::ChemicalEntity::NONE);
      calc_options.setIndexRange(IndexRange(0, 156));

      REQUIRE(polymer_sp->calculateMasses(calc_options, /*reset*/ true));

      THEN("The masses are checked and are as expected")
      {
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::MONO),
          Catch::Matchers::WithinAbs(17325.7923591224, 0.0000000001));
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::AVG),
          Catch::Matchers::WithinAbs(17336.8283764289, 0.0000000001));
      }
    }

    AND_GIVEN("An Oligomer and an Ionizer")
    {
      Oligomer oligomer(polymer_sp, "Oligomername", "OligomerDescription");

      Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                      "\"protonation\"+H",
                      /*charge*/ 1,
                      /*level*/ 1);
      oligomer.setIonizer(ionizer);

      THEN("The member data should be set correctly")
      {
        REQUIRE(oligomer.getPolymerCstSPtr() == polymer_sp);
        REQUIRE(oligomer.getName().toStdString() == "Oligomername");
        REQUIRE(oligomer.getDescription().toStdString() ==
                "OligomerDescription");

        Ionizer the_ionizer = oligomer.getIonizerCstRef();

        REQUIRE(the_ionizer == ionizer);
        REQUIRE_FALSE(oligomer.getIonizerCstRef().isIonized());
        REQUIRE_FALSE(oligomer.isModified());
        REQUIRE_FALSE(oligomer.isModified(/*deep*/ true));
        REQUIRE(oligomer.modifiedMonomerCount() == 0);
      }

      THEN(
        "Masses should not be calculatable with default parameters (no "
        "IndexRange defined))")
      {
        REQUIRE_FALSE(oligomer.calculateMasses());

        REQUIRE_THAT(oligomer.getMass(Enums::MassType::MONO),
                     Catch::Matchers::WithinAbs(0, 0.0000000001));
        REQUIRE_THAT(oligomer.getMass(Enums::MassType::AVG),
                     Catch::Matchers::WithinAbs(0, 0.0000000001));
      }

      WHEN(
        "Defining proper IndexRanges and calculating masses with the "
        "default CalcOptions member in the Oligomer and the set Ionizer")
      {
        std::size_t polymer_size = polymer_sp->size();
        REQUIRE(polymer_size == 157);

        IndexRange whole_polymer_index_range(0, 156);
        oligomer.getCalcOptionsRef().setIndexRange(whole_polymer_index_range);

        REQUIRE(oligomer.calculateMasses());

        THEN("Masses should be calculated correctly")
        {
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::MONO),
            Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::AVG),
            Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
        }

        AND_WHEN(
          "Ionizing the Oligomer should be performed by first deionizing it "
          "since it is already ionized")
        {
          REQUIRE(oligomer.ionize() == Enums::IonizationOutcome::IONIZED);

          THEN("Masses should not be modified")
          {
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::MONO),
              Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::AVG),
              Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
          }
        }

        AND_WHEN("Reionizing the Oligomer differently")
        {
          REQUIRE(oligomer.deionize() == Enums::IonizationOutcome::DEIONIZED);

          Ionizer &the_ionizer = oligomer.getIonizerRef();
          the_ionizer.setLevel(2);

          REQUIRE(oligomer.ionize() == Enums::IonizationOutcome::IONIZED);
          REQUIRE(oligomer.ionize() == Enums::IonizationOutcome::IONIZED);

          THEN("Masses should be updated correctly")
          {
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::MONO),
              Catch::Matchers::WithinAbs(8663.9040045934, 0.0000000001));
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::AVG),
              Catch::Matchers::WithinAbs(8669.4221296828, 0.0000000001));
          }

          AND_WHEN("Reionizing the Oligomer differently")
          {
            REQUIRE(oligomer.deionize() == Enums::IonizationOutcome::DEIONIZED);

            Ionizer &the_ionizer = oligomer.getIonizerRef();
            the_ionizer.setFormula("+Mg");
            the_ionizer.setNominalCharge(2);
            the_ionizer.setLevel(2);

            REQUIRE(oligomer.ionize() == Enums::IonizationOutcome::IONIZED);

            THEN("Masses should be updated correctly")
            {
              REQUIRE_THAT(
                oligomer.getMass(Enums::MassType::MONO),
                Catch::Matchers::WithinAbs(4343.4406106311, 0.0000000001));
              REQUIRE_THAT(
                oligomer.getMass(Enums::MassType::AVG),
                Catch::Matchers::WithinAbs(4346.3596420894, 0.0000000001));
            }
          }
        }
      }
    }
  }
}

SCENARIO("Oligomer instances can be configured with different IndexRanges",
         "[Oligomer]")
{
  test_utils_1_letter_oligomer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_oligomer.msp_polChemDef;

  QString polymer_file_path = QString("%1/polymer-sequences/%2")
                                .arg(TESTS_INPUT_DIR)
                                .arg("chicken-telokin.mxp");
  PolymerQSPtr polymer_sp = Polymer::createSPtr(pol_chem_def_csp);

  REQUIRE(polymer_sp->renderXmlPolymerFile(polymer_file_path));

  REQUIRE(polymer_sp->isLeftEndModified());
  REQUIRE(polymer_sp->getLeftEndModifCstRef().getName().toStdString() ==
          "Acetylation");
  REQUIRE(polymer_sp->getRightEndModifCstRef().getName().toStdString() ==
          "AmidationGlu");
  REQUIRE(polymer_sp->isRightEndModified());
  REQUIRE(polymer_sp->size() == 157);
  REQUIRE(polymer_sp->getSequenceCstRef().hasModifiedMonomer(0, 156));
  std::vector<std::size_t> indices =
    polymer_sp->getSequenceCstRef().modifiedMonomerIndices(0, 156);
  REQUIRE(indices.size() == 1);
  REQUIRE(polymer_sp->getSequenceCstRef()
            .getMonomerCstRPtrAt(indices.front())
            ->getModifsCstRef()
            .front()
            ->getName()
            .toStdString() == "Phosphorylation");

  GIVEN("A polymer sequence file loaded from disk")
  {
    WHEN("Masses are calculated with default parameters")
    {
      CalcOptions calc_options(/*deep_calculation*/ false,
                               /*mass_type*/ Enums::MassType::BOTH,
                               /*capping*/ Enums::CapType::BOTH,
                               /*monomer_entities*/ Enums::ChemicalEntity::NONE,
                               /*polymer_entities*/ Enums::ChemicalEntity::NONE);
      calc_options.setIndexRange(IndexRange(0, 156));

      REQUIRE(polymer_sp->calculateMasses(calc_options, /*reset*/ true));

      THEN("The masses are checked and are as expected")
      {
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::MONO),
          Catch::Matchers::WithinAbs(17325.7923591224, 0.0000000001));
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::AVG),
          Catch::Matchers::WithinAbs(17336.8283764289, 0.0000000001));
      }
    }

    AND_GIVEN("An Oligomer, a IndexRange and an Ionizer (not ionized yet)")
    {
      Oligomer oligomer(polymer_sp, "Oligomername", "OligomerDescription");

      Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                      "\"protonation\"+H",
                      /*charge*/ 1,
                      /*level*/ 1);
      oligomer.setIonizer(ionizer);

      IndexRange first_polymer_sequence_range(0, 47);
      oligomer.getCalcOptionsRef().setIndexRange(first_polymer_sequence_range);

      WHEN("The masses are calculated with ionization")
      {
        REQUIRE(oligomer.calculateMasses());

        THEN("Masses should check positively)")
        {
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::MONO),
            Catch::Matchers::WithinAbs(5182.4811523289, 0.0000000001));
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::AVG),
            Catch::Matchers::WithinAbs(5185.7812440907, 0.0000000001));
        }

        AND_WHEN(
          "Ionizing the Oligomer should be performed by first deionizing it "
          "since it is already ionized")
        {
          REQUIRE(oligomer.ionize() == Enums::IonizationOutcome::IONIZED);

          THEN("Masses should not change")
          {
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::MONO),
              Catch::Matchers::WithinAbs(5182.4811523289, 0.0000000001));
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::AVG),
              Catch::Matchers::WithinAbs(5185.7812440907, 0.0000000001));
          }
        }

        AND_WHEN("A new sequence range is added")
        {
          IndexRange second_polymer_sequence_range(80, 111);
          oligomer.getCalcOptionsRef()
            .getIndexRangeCollectionRef()
            .appendIndexRange(second_polymer_sequence_range);

          // Need to reset the current ionization state.
          ionizer.forceCurrentStateLevel(0);
          oligomer.setIonizer(ionizer);

          REQUIRE(oligomer.calculateMasses());

          THEN("Masses should check positively, with ionization accounted for")
          {
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::MONO),
              Catch::Matchers::WithinAbs(8825.0174030068, 0.0000000001));
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::AVG),
              Catch::Matchers::WithinAbs(8830.5762151642, 0.0000000001));
          }
        }
      }

      WHEN("The IndexRanges are cleared")
      {
        oligomer.getCalcOptionsRef().getIndexRangeCollectionRef().clear();

        REQUIRE_FALSE(oligomer.calculateMasses());

        THEN("The masses are not calculated anymore")
        {
          REQUIRE_THAT(oligomer.getMass(Enums::MassType::MONO),
                       Catch::Matchers::WithinAbs(0, 0.0000000001));
          REQUIRE_THAT(oligomer.getMass(Enums::MassType::AVG),
                       Catch::Matchers::WithinAbs(0, 0.0000000001));
        }

        AND_WHEN("Start and stop indices are set individually")
        {
          oligomer.getCalcOptionsRef()
            .getIndexRangeCollectionRef()
            .setIndexRange(0, 156);

          REQUIRE(oligomer.calculateMasses());

          THEN("The masses are not calculated anymore")
          {
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::MONO),
              Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::AVG),
              Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
          }
        }

        WHEN("The IndexRanges are cleared")
        {
          oligomer.getCalcOptionsRef().getIndexRangeCollectionRef().clear();

          REQUIRE_FALSE(oligomer.calculateMasses());

          THEN("The masses are not calculated anymore")
          {
            REQUIRE_THAT(oligomer.getMass(Enums::MassType::MONO),
                         Catch::Matchers::WithinAbs(0, 0.0000000001));
            REQUIRE_THAT(oligomer.getMass(Enums::MassType::AVG),
                         Catch::Matchers::WithinAbs(0, 0.0000000001));

            AND_WHEN("Start and stop indices are set individually")
            {
              oligomer.getCalcOptionsRef()
                .getIndexRangeCollectionRef()
                .setIndexRange(0, 156);

              REQUIRE(oligomer.calculateMasses());

              THEN("The masses are not calculated anymore")
              {
                REQUIRE_THAT(
                  oligomer.getMass(Enums::MassType::MONO),
                  Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
                REQUIRE_THAT(
                  oligomer.getMass(Enums::MassType::AVG),
                  Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
              }
            }
          }
        }
      }
    }
  }
}

SCENARIO(
  "Oligomer instances can have the mass calculation configured in different "
  "ways",
  "[Oligomer]")
{
  test_utils_1_letter_oligomer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_oligomer.msp_polChemDef;

  QString polymer_file_path = QString("%1/polymer-sequences/%2")
                                .arg(TESTS_INPUT_DIR)
                                .arg("chicken-telokin.mxp");
  PolymerQSPtr polymer_sp = Polymer::createSPtr(pol_chem_def_csp);

  REQUIRE(polymer_sp->renderXmlPolymerFile(polymer_file_path));

  REQUIRE(polymer_sp->isLeftEndModified());
  REQUIRE(polymer_sp->getLeftEndModifCstRef().getName().toStdString() ==
          "Acetylation");
  REQUIRE(polymer_sp->getRightEndModifCstRef().getName().toStdString() ==
          "AmidationGlu");
  REQUIRE(polymer_sp->isRightEndModified());
  REQUIRE(polymer_sp->size() == 157);
  REQUIRE(polymer_sp->getSequenceCstRef().hasModifiedMonomer(0, 156));
  std::vector<std::size_t> indices =
    polymer_sp->getSequenceCstRef().modifiedMonomerIndices(0, 156);
  REQUIRE(indices.size() == 1);
  REQUIRE(polymer_sp->getSequenceCstRef()
            .getMonomerCstRPtrAt(indices.front())
            ->getModifsCstRef()
            .front()
            ->getName()
            .toStdString() == "Phosphorylation");

  GIVEN("A polymer sequence file loaded from disk")
  {
    WHEN("Masses are calculated with default parameters")
    {
      CalcOptions calc_options(/*deep_calculation*/ false,
                               /*mass_type*/ Enums::MassType::BOTH,
                               /*capping*/ Enums::CapType::BOTH,
                               /*monomer_entities*/ Enums::ChemicalEntity::NONE,
                               /*polymer_entities*/ Enums::ChemicalEntity::NONE);
      calc_options.setIndexRange(IndexRange(0, 156));

      REQUIRE(polymer_sp->calculateMasses(calc_options, /*reset*/ true));

      THEN("The masses are checked and are as expected")
      {
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::MONO),
          Catch::Matchers::WithinAbs(17325.7923591224, 0.0000000001));
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::AVG),
          Catch::Matchers::WithinAbs(17336.8283764289, 0.0000000001));
      }
    }

    AND_GIVEN("An Oligomer, a IndexRange and an Ionizer")
    {
      Oligomer oligomer(polymer_sp, "Oligomername", "OligomerDescription");

      Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                      "\"protonation\"+H",
                      /*charge*/ 1,
                      /*level*/ 1);
      oligomer.setIonizer(ionizer);

      IndexRange first_polymer_sequence_range(0, 156);
      oligomer.getCalcOptionsRef().setIndexRange(first_polymer_sequence_range);

      WHEN(
        "The masses are calculated with ionization and with default "
        "calculation options (both caps, no modifs for example)")
      {
        REQUIRE(oligomer.calculateMasses());

        THEN("The masses are calculated correctly")
        {
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::MONO),
            Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::AVG),
            Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
        }

        WHEN(
          "The calculation options are modified to account for monomer "
          "modifications")
        {
          CalcOptions &calc_options_ref = oligomer.getCalcOptionsRef();
          calc_options_ref.setMonomerEntities(Enums::ChemicalEntity::MODIF);
          calc_options_ref.setDeepCalculation(true);

          // qDebug() << "flags:"
          //          << static_cast<int>(calc_options_ref.getMonomerEntities())
          //          <<
          //          chemicalEntityMap[calc_options_ref.getMonomerEntities()];

          // Reset the ionizer current state to not ionized so that
          // ionization works ok.
          Ionizer &the_ionizer = oligomer.getIonizerRef();
          the_ionizer.forceCurrentStateLevel(0);

          REQUIRE(oligomer.calculateMasses());

          THEN("The masses are calculated correctly")
          {
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::MONO),
              Catch::Matchers::WithinAbs(17406.7665150461, 0.0000000001));
            REQUIRE_THAT(
              oligomer.getMass(Enums::MassType::AVG),
              Catch::Matchers::WithinAbs(17417.8162475156, 0.0000000001));
          }

          AND_WHEN(
            "The calculation options are modified to account for polymer left "
            "end modification")
          {
            CalcOptions &calc_options_ref = oligomer.getCalcOptionsRef();
            calc_options_ref.setPolymerEntities(Enums::ChemicalEntity::LEFT_END_MODIF);
            calc_options_ref.setDeepCalculation(true);

            // qDebug()
            //   << "flags:"
            //   << static_cast<int>(calc_options_ref.getMonomerEntities())
            //   << chemicalEntityMap[calc_options_ref.getMonomerEntities()];
            //
            // qDebug()
            //   << "flags:"
            //   << static_cast<int>(calc_options_ref.getPolymerEntities())
            //   << chemicalEntityMap[calc_options_ref.getPolymerEntities()];

            // Reset the ionizer current state to not ionized so that
            // ionization works ok.
            Ionizer &the_ionizer = oligomer.getIonizerRef();
            the_ionizer.forceCurrentStateLevel(0);

            REQUIRE(oligomer.calculateMasses());

            THEN("The masses are calculated correctly")
            {
              REQUIRE_THAT(
                oligomer.getMass(Enums::MassType::MONO),
                Catch::Matchers::WithinAbs(17448.7770797308, 0.0000000001));
              REQUIRE_THAT(
                oligomer.getMass(Enums::MassType::AVG),
                Catch::Matchers::WithinAbs(17459.8531876702, 0.0000000001));
            }

            AND_WHEN(
              "The calculation options are modified to account for polymer "
              "right end modification")
            {
              CalcOptions &calc_options_ref = oligomer.getCalcOptionsRef();
              calc_options_ref.setPolymerEntities(
                Enums::ChemicalEntity::RIGHT_END_MODIF);
              calc_options_ref.setDeepCalculation(true);

              // qDebug()
              //   << "flags:"
              //   << static_cast<int>(calc_options_ref.getMonomerEntities())
              //   << chemicalEntityMap[calc_options_ref.getMonomerEntities()];
              //
              // qDebug()
              //   << "flags:"
              //   << static_cast<int>(calc_options_ref.getPolymerEntities())
              //   << chemicalEntityMap[calc_options_ref.getPolymerEntities()];

              // Reset the ionizer current state to not ionized so that
              // ionization works ok.
              Ionizer &the_ionizer = oligomer.getIonizerRef();
              the_ionizer.forceCurrentStateLevel(0);

              REQUIRE(oligomer.calculateMasses());

              THEN("The masses are calculated correctly")
              {
                REQUIRE_THAT(
                  oligomer.getMass(Enums::MassType::MONO),
                  Catch::Matchers::WithinAbs(17405.7824994624, 0.0000000001));
                REQUIRE_THAT(
                  oligomer.getMass(Enums::MassType::AVG),
                  Catch::Matchers::WithinAbs(17416.8314854577, 0.0000000001));
              }
            }
          }
        }
      }
    }
  }
}

SCENARIO(
  "Oligomer instances can return Monomer instances at different Sequence "
  "locations",
  "[Oligomer]")
{
  test_utils_1_letter_oligomer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_oligomer.msp_polChemDef;

  QString polymer_file_path = QString("%1/polymer-sequences/%2")
                                .arg(TESTS_INPUT_DIR)
                                .arg("chicken-telokin.mxp");
  PolymerQSPtr polymer_sp = Polymer::createSPtr(pol_chem_def_csp);

  REQUIRE(polymer_sp->renderXmlPolymerFile(polymer_file_path));

  REQUIRE(polymer_sp->isLeftEndModified());
  REQUIRE(polymer_sp->getLeftEndModifCstRef().getName().toStdString() ==
          "Acetylation");
  REQUIRE(polymer_sp->getRightEndModifCstRef().getName().toStdString() ==
          "AmidationGlu");
  REQUIRE(polymer_sp->isRightEndModified());
  REQUIRE(polymer_sp->size() == 157);
  REQUIRE(polymer_sp->getSequenceCstRef().hasModifiedMonomer(0, 156));
  std::vector<std::size_t> indices =
    polymer_sp->getSequenceCstRef().modifiedMonomerIndices(0, 156);
  REQUIRE(indices.size() == 1);
  REQUIRE(polymer_sp->getSequenceCstRef()
            .getMonomerCstRPtrAt(indices.front())
            ->getModifsCstRef()
            .front()
            ->getName()
            .toStdString() == "Phosphorylation");

  GIVEN("A polymer sequence file loaded from disk")
  {

    WHEN("Masses are calculated with default parameters")
    {
      CalcOptions calc_options(/*deep_calculation*/ false,
                               /*mass_type*/ Enums::MassType::BOTH,
                               /*capping*/ Enums::CapType::BOTH,
                               /*monomer_entities*/ Enums::ChemicalEntity::NONE,
                               /*polymer_entities*/ Enums::ChemicalEntity::NONE);
      calc_options.setIndexRange(IndexRange(0, 156));

      REQUIRE(polymer_sp->calculateMasses(calc_options, /*reset*/ true));

      THEN("The masses are checked and are as expected")
      {
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::MONO),
          Catch::Matchers::WithinAbs(17325.7923591224, 0.0000000001));
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::AVG),
          Catch::Matchers::WithinAbs(17336.8283764289, 0.0000000001));
      }
    }

    AND_GIVEN("An Oligomer, a IndexRange and an Ionizer")
    {
      Oligomer oligomer(polymer_sp, "Oligomername", "OligomerDescription");

      Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                      "\"protonation\"+H",
                      /*charge*/ 1,
                      /*level*/ 1);
      oligomer.setIonizer(ionizer);

      IndexRange first_polymer_sequence_range(0, 156);
      oligomer.getCalcOptionsRef().setIndexRange(first_polymer_sequence_range);

      WHEN(
        "The masses are calculated with ionization and with default "
        "calculation options (both caps, no modifs for example)")
      {
        REQUIRE(oligomer.calculateMasses());

        THEN("The masses are calculated correctly")
        {
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::MONO),
            Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::AVG),
            Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
        }

        AND_WHEN("Specific Monomer instances are requested")
        {
          MonomerSPtr monomer_left_end_csp =
            oligomer.getLeftEndMonomerCstSPtr();
          MonomerSPtr monomer_right_end_csp =
            oligomer.getRightEndMonomerCstSPtr();

          THEN("The returned Monomer instances are the right one")
          {
            REQUIRE(monomer_left_end_csp->getCode().toStdString() == "M");
            REQUIRE(monomer_right_end_csp->getCode().toStdString() == "E");
          }
        }

        AND_WHEN("Specific other Monomer instances are requested")
        {
          MonomerSPtr monomer_left_end_csp  = oligomer.getMonomerCstSPtrAt(0);
          MonomerSPtr monomer_middle_csp    = oligomer.getMonomerCstSPtrAt(70);
          MonomerSPtr monomer_right_end_csp = oligomer.getMonomerCstSPtrAt(156);

          THEN("The returned Monomer instances are the right one")
          {
            REQUIRE(monomer_left_end_csp->getCode().toStdString() == "M");
            REQUIRE(monomer_right_end_csp->getCode().toStdString() == "E");
            REQUIRE(monomer_middle_csp->getCode().toStdString() == "Y");
          }
        }
      }
    }
  }
}


SCENARIO("Oligomer instances can be set CrossLink instances", "[Oligomer]")
{
  test_utils_1_letter_oligomer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_oligomer.msp_polChemDef;

  QString polymer_file_path = QString("%1/polymer-sequences/%2")
                                .arg(TESTS_INPUT_DIR)
                                .arg("chicken-telokin.mxp");
  PolymerQSPtr polymer_sp = Polymer::createSPtr(pol_chem_def_csp);

  REQUIRE(polymer_sp->renderXmlPolymerFile(polymer_file_path));

  REQUIRE(polymer_sp->isLeftEndModified());
  REQUIRE(polymer_sp->getLeftEndModifCstRef().getName().toStdString() ==
          "Acetylation");
  REQUIRE(polymer_sp->getRightEndModifCstRef().getName().toStdString() ==
          "AmidationGlu");
  REQUIRE(polymer_sp->isRightEndModified());
  REQUIRE(polymer_sp->size() == 157);
  REQUIRE(polymer_sp->getSequenceCstRef().hasModifiedMonomer(0, 156));
  std::vector<std::size_t> indices =
    polymer_sp->getSequenceCstRef().modifiedMonomerIndices(0, 156);
  REQUIRE(indices.size() == 1);
  REQUIRE(polymer_sp->getSequenceCstRef()
            .getMonomerCstRPtrAt(indices.front())
            ->getModifsCstRef()
            .front()
            ->getName()
            .toStdString() == "Phosphorylation");

  GIVEN("A polymer sequence file loaded from disk")
  {

    WHEN("Masses are calculated with default parameters")
    {
      CalcOptions calc_options(/*deep_calculation*/ false,
                               /*mass_type*/ Enums::MassType::BOTH,
                               /*capping*/ Enums::CapType::BOTH,
                               /*monomer_entities*/ Enums::ChemicalEntity::NONE,
                               /*polymer_entities*/ Enums::ChemicalEntity::NONE);
      calc_options.setIndexRange(IndexRange(0, 156));

      REQUIRE(polymer_sp->calculateMasses(calc_options, /*reset*/ true));

      THEN("The masses are checked and are as expected")
      {
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::MONO),
          Catch::Matchers::WithinAbs(17325.7923591224, 0.0000000001));
        REQUIRE_THAT(
          polymer_sp->getMass(Enums::MassType::AVG),
          Catch::Matchers::WithinAbs(17336.8283764289, 0.0000000001));
      }
    }

    AND_GIVEN("An Oligomer, a IndexRange and an Ionizer")
    {
      Oligomer oligomer(polymer_sp, "Oligomername", "OligomerDescription");

      Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                      "\"protonation\"+H",
                      /*charge*/ 1,
                      /*level*/ 1);
      oligomer.setIonizer(ionizer);

      IndexRange first_polymer_sequence_range(0, 156);
      oligomer.getCalcOptionsRef().setIndexRange(first_polymer_sequence_range);

      WHEN(
        "The masses are calculated with ionization and with default "
        "calculation options (both caps, no modifs for example)")
      {
        REQUIRE(oligomer.calculateMasses());

        THEN("The masses are calculated correctly")
        {
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::MONO),
            Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
          REQUIRE_THAT(
            oligomer.getMass(Enums::MassType::AVG),
            Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));
        }

        AND_WHEN("A CrossLink is set between two Monomers")
        {
          CrossLinkerCstSPtr cross_linker_disulfide_csp =
            oligomer.getPolymerCstSPtr()
              ->getPolChemDefCstSPtr()
              ->getCrossLinkerCstSPtrByName("DisulfideBond");

          REQUIRE(cross_linker_disulfide_csp != nullptr);

          CrossLinkSPtr cross_link_disulfide_sp = std::make_shared<CrossLink>(
            cross_linker_disulfide_csp, polymer_sp, "Disulfide Bond");

          REQUIRE(cross_link_disulfide_sp != nullptr);
        }
      }
    }
  }
}


} // namespace libXpertMassCore
} // namespace MsXpS
