// ./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"

namespace MsXpS
{
namespace libXpertMassCore
{

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

ErrorList error_list_polymer;

SCENARIO(
  "Polymer instances can be constructed empty and initialized piecemeal until "
  "they are valid",
  "[Polymer]")
{
  test_utils_1_letter_polymer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_polymer.msp_polChemDef;

  QDateTime now_date_time = QDateTime::currentDateTime();

  GIVEN("An allocated polymer chemistry definition")
  {

    WHEN("A Polymer is allocated as a SPtr and totally unconfigured")
    {
      PolymerQSPtr polymer_sp = Polymer::createSPtr();

      THEN("The Polymer is not valid and does not validate successfully")
      {
        REQUIRE_FALSE(polymer_sp->isValid());

        REQUIRE(polymer_sp->getName().toStdString() == "");
        REQUIRE(polymer_sp->getCode().toStdString() == "");
        REQUIRE(polymer_sp->getAuthor().toStdString() == "");
        REQUIRE(polymer_sp->getFilePath().toStdString() == "");
        REQUIRE(polymer_sp->getDateTime().toStdString() == "");

        REQUIRE(polymer_sp->getSequenceCstRef().getSequence().toStdString() ==
                "");
        REQUIRE(polymer_sp->getSequenceCstRef().size() == 0);

        REQUIRE_FALSE(polymer_sp->getLeftEndModifCstRef().isValid());
        REQUIRE_FALSE(polymer_sp->getRightEndModifCstRef().isValid());

        REQUIRE_FALSE(polymer_sp->getIonizerCstRef().isValid());

        REQUIRE(polymer_sp->getCrossLinksCstRef().size() == 0);
      }

      AND_WHEN("Setting PolChemDef and other member data but not sequence")
      {
        polymer_sp->setPolChemDefCstSPtr(pol_chem_def_csp);
        polymer_sp->setName("Test name");
        polymer_sp->setCode("Test code");
        polymer_sp->setAuthor("Rusconi");
        polymer_sp->setFilePath(
          QString("%1/polymer-sequences/chicken-telokin.mxp")
            .arg(test_utils_1_letter_polymer.m_testsInputDataDir));
        QFileInfo file_info(polymer_sp->getFilePath());
        REQUIRE(file_info.exists());
        polymer_sp->setDateTime(now_date_time.toString("yyyy-MM-dd:mm:ss"));

        THEN(
          "The Polymer still is invalid because the sequence needs to be "
          "valid.")
        {
          REQUIRE_FALSE(polymer_sp->isValid());
        }

        AND_WHEN("Setting the sequence as a string")
        {
          polymer_sp->setSequence(
            test_utils_1_letter_polymer.m_telokinAsMonomerText1Letter);

          THEN(
            "The Polymer valid because the sequence at last is there and "
            "is valid.")
          {
            REQUIRE(polymer_sp->isValid());
          }
        }

        AND_WHEN("Setting the sequence as a Sequence object")
        {
          polymer_sp->getSequenceRef().clear();

          Sequence sequence(
            pol_chem_def_csp,
            test_utils_1_letter_polymer.m_telokinAsMonomerText1Letter);
          polymer_sp->setSequence(sequence);

          THEN(
            "The Polymer valid because the sequence at last is there and "
            "is valid.")
          {
            REQUIRE(polymer_sp->isValid());
          }

          AND_THEN("All the members are set correctly")
          {
            REQUIRE(polymer_sp->getPolChemDefCstSPtr() == pol_chem_def_csp);
            REQUIRE(polymer_sp->getName().toStdString() == "Test name");
            REQUIRE(polymer_sp->getCode().toStdString() == "Test code");
            REQUIRE(polymer_sp->getAuthor().toStdString() == "Rusconi");
            REQUIRE(polymer_sp->getFilePath().toStdString() ==
                    QString("%1/polymer-sequences/chicken-telokin.mxp")
                      .arg(test_utils_1_letter_polymer.m_testsInputDataDir)
                      .toStdString());
            QFileInfo file_info(polymer_sp->getFilePath());
            REQUIRE(polymer_sp->getDateTime().toStdString() ==
                    now_date_time.toString("yyyy-MM-dd:mm:ss").toStdString());
            REQUIRE(polymer_sp->size() ==
                    (std::size_t)test_utils_1_letter_polymer
                      .m_telokinAsMonomerText1Letter.size());
          }
        }
      }
    }
  }
}

SCENARIO("Polymer instances can be constructed and initialized in one go",
         "[Polymer]")
{
  test_utils_1_letter_polymer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_polymer.msp_polChemDef;

  QDateTime now_date_time = QDateTime::currentDateTime();

  GIVEN("An allocated polymer chemistry definition")
  {

    WHEN("A Polymer is allocated as a SPtr with valid parameters")
    {
      PolymerQSPtr polymer_sp = Polymer::createSPtr(
        pol_chem_def_csp, "Test name", "Test code", "Rusconi");

      polymer_sp->setFilePath(
        QString("%1/polymer-sequences/chicken-telokin.mxp")
          .arg(test_utils_1_letter_polymer.m_testsInputDataDir));
      QFileInfo file_info(polymer_sp->getFilePath());
      REQUIRE(file_info.exists());
      polymer_sp->setDateTime(now_date_time.toString("yyyy-MM-dd:mm:ss"));

      polymer_sp->setSequence(
        test_utils_1_letter_polymer.m_telokinAsMonomerText1Letter);

      THEN("The Polymer is valid and all the members are set correctly")
      {
        REQUIRE(polymer_sp->getPolChemDefCstSPtr() == pol_chem_def_csp);
        REQUIRE(polymer_sp->getName().toStdString() == "Test name");
        REQUIRE(polymer_sp->getCode().toStdString() == "Test code");
        REQUIRE(polymer_sp->getAuthor().toStdString() == "Rusconi");
        REQUIRE(polymer_sp->getFilePath().toStdString() ==
                QString("%1/polymer-sequences/chicken-telokin.mxp")
                  .arg(test_utils_1_letter_polymer.m_testsInputDataDir)
                  .toStdString());
        QFileInfo file_info(polymer_sp->getFilePath());
        REQUIRE(polymer_sp->getDateTime().toStdString() ==
                now_date_time.toString("yyyy-MM-dd:mm:ss").toStdString());
        REQUIRE(polymer_sp->size() == (std::size_t)test_utils_1_letter_polymer
                                        .m_telokinAsMonomerText1Letter.size());
      }

      THEN("The end modifications are invalid because not defined")
      {
        REQUIRE_FALSE(polymer_sp->getLeftEndModifCstRef().isValid());
        REQUIRE_FALSE(
          polymer_sp->getLeftEndModifCstRef().validate(&error_list_polymer));
        REQUIRE_FALSE(polymer_sp->getRightEndModifCstRef().isValid());
        REQUIRE_FALSE(
          polymer_sp->getRightEndModifCstRef().validate(&error_list_polymer));
      }

      AND_WHEN("Setting the left end modification by name")
      {
        REQUIRE(polymer_sp->setLeftEndModifByName("Acetylation"));

        THEN("The left end modif becomes valid")
        {
          REQUIRE(polymer_sp->getLeftEndModifCstRef().isValid());
          REQUIRE(
            polymer_sp->getLeftEndModifCstRef().validate(&error_list_polymer));
          REQUIRE(polymer_sp->getLeftEndModifCstRef().getName().toStdString() ==
                  "Acetylation");
        }

        AND_WHEN("Setting the right end modification by name")
        {
          REQUIRE(polymer_sp->setRightEndModifByName("AmidationGlu"));

          THEN("The right end modif becomes valid")
          {
            REQUIRE(polymer_sp->getRightEndModifCstRef().isValid());
            REQUIRE(polymer_sp->getRightEndModifCstRef().validate(
              &error_list_polymer));
            REQUIRE(
              polymer_sp->getRightEndModifCstRef().getName().toStdString() ==
              "AmidationGlu");
          }
        }
      }

      WHEN("Setting the Ionizer (monoprotonation)")
      {
        Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                        "\"protonation\"+H",
                        /*charge*/ 1,
                        /*level*/ 1);
        polymer_sp->setIonizer(ionizer);

        THEN("The Ionizer must be set correctly as a member datum")
        {
          REQUIRE(polymer_sp->getIonizerCstRef()
                    .getFormulaCstRef()
                    .getActionFormula(/*with_title*/ false)
                    .toStdString() == "+H");
          REQUIRE(polymer_sp->getIonizerCstRef().getLevel() == 1);
          REQUIRE(polymer_sp->getIonizerCstRef().getNominalCharge() == 1);
          REQUIRE(polymer_sp->getIonizerCstRef().charge() == 1);
        }
      }
    }
  }
}

SCENARIO("Polymer instances can be modified on their left and/or right ends",
         "[Polymer]")
{
  test_utils_1_letter_polymer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_polymer.msp_polChemDef;

  QDateTime now_date_time = QDateTime::currentDateTime();

  GIVEN("An allocated polymer chemistry definition")
  {

    WHEN(
      "A Polymer is allocated as a SPtr with valid parameters and a sequence "
      "is set")
    {
      PolymerQSPtr polymer_sp = Polymer::createSPtr(
        pol_chem_def_csp,
        "Test name",
        "Test code",
        "Rusconi",
        test_utils_1_letter_polymer.m_telokinAsMonomerText1Letter);

      THEN("The left end modifs are not yet initialized and are invalid")
      {
        REQUIRE_FALSE(polymer_sp->getLeftEndModifCstRef().isValid());
        REQUIRE_FALSE(polymer_sp->getRightEndModifCstRef().isValid());
      }

      AND_WHEN("That polymer is modified on its left end using Modif name")
      {
        polymer_sp->setLeftEndModifByName("Acetylation");

        THEN("The Modif is properly set and valid")
        {
          REQUIRE(polymer_sp->getLeftEndModifCstRef().isValid());
          ModifCstSPtr modif_csp =
            pol_chem_def_csp->getModifCstSPtrByName("Acetylation");
          REQUIRE(modif_csp != nullptr);
          REQUIRE(*modif_csp.get() == polymer_sp->getLeftEndModifCstRef());
        }

        AND_WHEN("That polymer is modified on its right end using Modif name")
        {
          polymer_sp->setRightEndModifByName("AmidationGlu");

          THEN("The Modif is properly set and valid")
          {
            REQUIRE(polymer_sp->getRightEndModifCstRef().isValid());
            ModifCstSPtr modif_csp =
              pol_chem_def_csp->getModifCstSPtrByName("AmidationGlu");
            REQUIRE(modif_csp != nullptr);
            REQUIRE(*modif_csp.get() == polymer_sp->getRightEndModifCstRef());
          }
        }
      }
    }
  }
}

SCENARIO("Polymer instances can be initialized from files on disk", "[Polymer]")
{
  test_utils_1_letter_polymer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_polymer.msp_polChemDef;

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

  GIVEN("A polymer sequence file loaded from disk")
  {
    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");

    AND_GIVEN("Calculation options 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));

      WHEN("The Polymer is asked to compute its masses")
      {
        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_WHEN(
          "An Ionizer is setup, set to the Polymer and then the latter is "
          "ionized")
        {
          Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                          "\"protonation\"+H",
                          /*charge*/ 1,
                          /*level*/ 1);

          // qDebug() << "The ionizer:" << ionizer.toString();

          polymer_sp->setIonizer(ionizer);

          polymer_sp->ionize();

          THEN("The masses are updated as expected and the Polymer is ionized")
          {
            REQUIRE_THAT(
              polymer_sp->getMass(Enums::MassType::MONO),
              Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
            REQUIRE_THAT(
              polymer_sp->getMass(Enums::MassType::AVG),
              Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));

            REQUIRE(polymer_sp->getIonizerCstRef().isIonized());
          }

          AND_WHEN("The Polymer is deionized")
          {
            polymer_sp->deionize();

            THEN(
              "The masses are updated as expected and the Polymer is no more "
              "ionized")
            {
              // qDebug() << "HERE mono" <<
              // polymer_sp->getMassRef(Enums::MassType::MONO); qDebug() << "HERE avg"
              // << polymer_sp->getMassRef(Enums::MassType::AVG);

              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));

              REQUIRE_FALSE(polymer_sp->getIonizerCstRef().isIonized());
            }

            AND_WHEN(
              "A complex Mg2+-based ionizer is set up to ionize the Polymer")
            {
              Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                              "\"magnesiumylation\"+Mg",
                              /*charge*/ 2,
                              /*level*/ 2);

              polymer_sp->setIonizer(ionizer);
              polymer_sp->ionize();

              THEN(
                "The masses are updated as expected and the Polymer is "
                "ionized")
              {

                REQUIRE_THAT(
                  polymer_sp->getMass(Enums::MassType::MONO),
                  Catch::Matchers::WithinAbs(4343.4406106311, 0.0000000001));
                REQUIRE_THAT(
                  polymer_sp->getMass(Enums::MassType::AVG),
                  Catch::Matchers::WithinAbs(4346.3596420894, 0.0000000001));

                REQUIRE(polymer_sp->getIonizerCstRef().isIonized());
                REQUIRE(polymer_sp->getIonizerCstRef().charge() == 4);
              }

              AND_WHEN(
                "Molecular masses are asked for, the polymer is deionized")
              {
                Ionizer ionizer(pol_chem_def_csp->getIsotopicDataCstSPtr(),
                                "\"magnesiumylation\"+Mg",
                                /*charge*/ 2,
                                /*level*/ 2);

                polymer_sp->setIonizer(ionizer);
                polymer_sp->ionize();

                THEN(
                  "The masses are updated as expected and the Polymer is "
                  "ionized")
                {
                  REQUIRE_THAT(
                    polymer_sp->getMass(Enums::MassType::MONO),
                    Catch::Matchers::WithinAbs(4343.4406106311, 0.0000000001));
                  REQUIRE_THAT(
                    polymer_sp->getMass(Enums::MassType::AVG),
                    Catch::Matchers::WithinAbs(4346.3596420894, 0.0000000001));

                  REQUIRE(polymer_sp->getIonizerCstRef().isIonized());
                  REQUIRE(polymer_sp->getIonizerCstRef().charge() == 4);

                  AND_THEN("Asking for molecular masses returns proper values")
                  {
                    double mol_mass_mono;
                    double mol_mass_avg;

                    polymer_sp->molecularMasses(mol_mass_mono, mol_mass_avg);

                    //  The masses *IN* the polymer do not change.
                    REQUIRE_THAT(polymer_sp->getMass(Enums::MassType::MONO),
                                 Catch::Matchers::WithinAbs(4343.4406106311,
                                                            0.0000000001));
                    REQUIRE_THAT(polymer_sp->getMass(Enums::MassType::AVG),
                                 Catch::Matchers::WithinAbs(4346.3596420894,
                                                            0.0000000001));

                    //  The masses returned are correct.
                    REQUIRE_THAT(mol_mass_mono,
                                 Catch::Matchers::WithinAbs(17325.7923591224,
                                                            0.0000000001));
                    REQUIRE_THAT(mol_mass_avg,
                                 Catch::Matchers::WithinAbs(17336.8283764289,
                                                            0.0000000001));
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

SCENARIO("Construction of a Polymer with 7 CrossLink instances right from file",
         "[Polymer]")
{
  test_utils_1_letter_polymer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_polymer.msp_polChemDef;

  QString polymer_file_path = QString("%1/polymer-sequences/%2")
                                .arg(TESTS_INPUT_DIR)
                                .arg("kunitz-inhibitor-human-cross-links.mxp");

  GIVEN("A Polymer instance created by loading an XML file")
  {
    PolymerQSPtr polymer_sp = Polymer::createSPtr(pol_chem_def_csp);
    REQUIRE(polymer_sp->renderXmlPolymerFile(polymer_file_path));
    REQUIRE(polymer_sp->size() == 352);

    WHEN("Configuring calculation options NOT to account for cross-links")
    {
      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, polymer_sp->size() - 1));

      AND_WHEN("The Polymer is asked to compute its masses")
      {
        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(38973.9813242497, 0.0000000001));
          REQUIRE_THAT(
            polymer_sp->getMass(Enums::MassType::AVG),
            Catch::Matchers::WithinAbs(38999.3128988044, 0.0000000001));
        }

        AND_WHEN("Configuring calculation options to account for cross-links")
        {
          CalcOptions calc_options(
            /*deep_calculation*/ false,
            /*mass_type*/ Enums::MassType::BOTH,
            /*capping*/ Enums::CapType::BOTH,
            /*monomer_entities*/ Enums::ChemicalEntity::CROSS_LINKER,
            /*polymer_entities*/ Enums::ChemicalEntity::NONE);
          calc_options.setIndexRange(IndexRange(0, polymer_sp->size() - 1));

          AND_WHEN("The Polymer is asked to compute its masses")
          {
            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(38959.8717737979, 0.0000000001));
              REQUIRE_THAT(
                polymer_sp->getMass(Enums::MassType::AVG),
                Catch::Matchers::WithinAbs(38985.2017182470, 0.0000000001));
            }
          }
        }
      }
    }
  }
}


} // namespace libXpertMassCore
} // namespace MsXpS
