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


/////////////////////// libXpertMassCore includes
#include "MsXpS/libXpertMassCore/Formula.hpp"
#include "MsXpS/libXpertMassCore/Ionizer.hpp"
#include "MsXpS/libXpertMassCore/IsotopicDataLibraryHandler.hpp"
#include "MsXpS/libXpertMassCore/Sequence.hpp"
#include "MsXpS/libXpertMassCore/Monomer.hpp"

/////////////////////// Local includes
#include "tests-config.h"
#include "TestUtils.hpp"

namespace MsXpS
{

namespace libXpertMassCore
{

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

SCENARIO("An Ionizer is used to attach charges to chemical entities",
         "[Ionizer]")
{

  test_utils_1_letter_ionizer.initializeXpertmassLibrary();

  IsotopicDataLibraryHandler isotopic_data_lib_handler;
  isotopic_data_lib_handler.setFileName("the_file_name.dat");
  qsizetype count_non_isotope_skipped_items = 0;
  qsizetype loaded_isotope_count =
    isotopic_data_lib_handler.loadData(count_non_isotope_skipped_items);
  IsotopicDataCstSPtr isotopic_data_csp =
    isotopic_data_lib_handler.getIsotopicData();
  REQUIRE(loaded_isotope_count + count_non_isotope_skipped_items ==
          IsoSpec::isospec_number_of_isotopic_entries);
  REQUIRE(isotopic_data_csp->size() == loaded_isotope_count);

  GIVEN("Construction of an empty Ionizer")
  {
    Ionizer ionizer;

    THEN(
      "The member data are set to emtpy/0/false and the Ionizer is *not* valid")
    {
      REQUIRE(ionizer.getIsotopicDataCstSPtr() == nullptr);
      REQUIRE(ionizer.getFormulaCstRef().getActionFormula().toStdString() ==
              "");
      REQUIRE(ionizer.getNominalCharge() == 0);
      REQUIRE(ionizer.getLevel() == 0);
      REQUIRE(ionizer.charge() == 0);
      REQUIRE(ionizer.isValid() == false);
    }

    AND_GIVEN("Configuring MALDI-type ionization with setters")
    {
      ionizer.setIsotopicDataCstSPtr(isotopic_data_csp);
      ionizer.setFormula("\"Protonation\"+H");
      ionizer.setNominalCharge(1);
      ionizer.setLevel(1);

      THEN("Member data are updated accordingly")
      {
        REQUIRE(ionizer.getIsotopicDataCstSPtr() == isotopic_data_csp);
        REQUIRE(ionizer.getFormulaCstRef().getTitle().toStdString() ==
                "Protonation");
        REQUIRE(ionizer.getFormulaCstRef().getActionFormula().toStdString() ==
                "+H");
        REQUIRE(ionizer.getNominalCharge() == 1);
        REQUIRE(ionizer.getLevel() == 1);
      }

      GIVEN(
        "Construction of a new copy of that Ionizer by the copy constructor")
      {
        Ionizer new_ionizer(ionizer);

        THEN("The members should be identical to the former Ionizer")
        {
          REQUIRE(new_ionizer.getIsotopicDataCstSPtr() == isotopic_data_csp);
          REQUIRE(new_ionizer.getFormulaCstRef().getTitle().toStdString() ==
                  "Protonation");
          REQUIRE(
            new_ionizer.getFormulaCstRef().getActionFormula().toStdString() ==
            "+H");
          REQUIRE(new_ionizer.getNominalCharge() == 1);
          REQUIRE(new_ionizer.getLevel() == 1);
        }

        AND_THEN("Checking for (in)equality should work")
        {
          REQUIRE(ionizer == ionizer);
          REQUIRE_FALSE(ionizer != ionizer);

          REQUIRE(new_ionizer == ionizer);
          REQUIRE_FALSE(new_ionizer != ionizer);
        }
      }

      GIVEN(
        "Construction of a new copy of that Ionizer using data from the "
        "first one")
      {
        Ionizer new_ionizer(ionizer.getIsotopicDataCstSPtr(),
                            ionizer.getFormulaCstRef(),
                            ionizer.getNominalCharge(),
                            ionizer.getLevel());

        THEN("The members should be identical to the former Ionizer")
        {
          REQUIRE(new_ionizer.getFormulaCstRef().getTitle().toStdString() ==
                  "Protonation");
          REQUIRE(
            new_ionizer.getFormulaCstRef().getActionFormula().toStdString() ==
            "+H");
          REQUIRE(new_ionizer.getNominalCharge() == 1);
          REQUIRE(new_ionizer.getLevel() == 1);
        }

        AND_THEN("Checking for (in)equality should work")
        {
          REQUIRE((ionizer = ionizer) == ionizer);
          REQUIRE(ionizer == ionizer);
          REQUIRE_FALSE(ionizer != ionizer);

          REQUIRE(new_ionizer == ionizer);
          REQUIRE_FALSE(new_ionizer != ionizer);
        }
      }

      GIVEN("Construction of a new copy of that Ionizer by assignment")
      {
        Ionizer new_ionizer;
        new_ionizer = ionizer;

        THEN("The members should be identical to the former Ionizer")
        {
          REQUIRE(new_ionizer.getFormulaCstRef().getTitle().toStdString() ==
                  "Protonation");
          REQUIRE(
            new_ionizer.getFormulaCstRef().getActionFormula().toStdString() ==
            "+H");
          REQUIRE(new_ionizer.getNominalCharge() == 1);
          REQUIRE(new_ionizer.getLevel() == 1);
        }

        AND_THEN("Checking for (in)equality should work")
        {
          REQUIRE(new_ionizer == ionizer);
          REQUIRE_FALSE(new_ionizer != ionizer);

          REQUIRE(new_ionizer == ionizer);
          REQUIRE_FALSE(new_ionizer != ionizer);
        }
      }
    }
  }
}

SCENARIO(
  "An Ionizer correctly initialized should validate against reference data",
  "[Ionizer]")
{
  test_utils_1_letter_ionizer.initializeXpertmassLibrary();

  IsotopicDataLibraryHandler isotopic_data_lib_handler;
  isotopic_data_lib_handler.setFileName("the_file_name.dat");
  qsizetype count_non_isotope_skipped_items = 0;
  qsizetype loaded_isotope_count =
    isotopic_data_lib_handler.loadData(count_non_isotope_skipped_items);
  IsotopicDataCstSPtr isotopic_data_csp =
    isotopic_data_lib_handler.getIsotopicData();
  REQUIRE(loaded_isotope_count + count_non_isotope_skipped_items ==
          IsoSpec::isospec_number_of_isotopic_entries);
  REQUIRE(isotopic_data_csp->size() == loaded_isotope_count);

  GIVEN("An empty Ionizer")
  {
    Ionizer ionizer;

    WHEN("That Ionizer is initialized without IsotopicData")
    {
      ionizer.setFormula("\"Protonation\"+H");
      ionizer.setNominalCharge(1);
      ionizer.setLevel(1);

      THEN("Its member data are updated")
      {
        REQUIRE(ionizer.getIsotopicDataCstSPtr() == nullptr);
        REQUIRE(ionizer.getFormulaCstRef().getTitle().toStdString() ==
                "Protonation");
        REQUIRE(ionizer.getFormulaCstRef().getActionFormula().toStdString() ==
                "+H");
        REQUIRE(ionizer.getNominalCharge() == 1);
        REQUIRE(ionizer.getLevel() == 1);
      }

      AND_THEN("Without IsotopicData the Ionizer is not valid")
      {
        ErrorList error_list;
        REQUIRE_FALSE(ionizer.validate(&error_list));
        REQUIRE_FALSE(ionizer.isValid());
      }

      AND_WHEN("The IsotopicData are set")
      {
        ionizer.setIsotopicDataCstSPtr(isotopic_data_csp);

        THEN("Without IsotopicData the Ionizer is not valid")
        {
          ErrorList error_list;
          REQUIRE(ionizer.validate(&error_list));
          REQUIRE(ionizer.isValid());
        }
      }
    }
  }
}

SCENARIO("An Ionizer can output a text string describing itself", "[Ionizer]")
{
  test_utils_1_letter_ionizer.initializeXpertmassLibrary();

  IsotopicDataLibraryHandler isotopic_data_lib_handler;
  isotopic_data_lib_handler.setFileName("the_file_name.dat");
  qsizetype count_non_isotope_skipped_items = 0;
  qsizetype loaded_isotope_count =
    isotopic_data_lib_handler.loadData(count_non_isotope_skipped_items);
  IsotopicDataCstSPtr isotopic_data_csp =
    isotopic_data_lib_handler.getIsotopicData();
  REQUIRE(loaded_isotope_count + count_non_isotope_skipped_items ==
          IsoSpec::isospec_number_of_isotopic_entries);
  REQUIRE(isotopic_data_csp->size() == loaded_isotope_count);

  GIVEN("An empty Ionizer")
  {
    Ionizer ionizer;

    WHEN("That Ionizer is fully initialized")
    {
      ionizer.setIsotopicDataCstSPtr(isotopic_data_csp);
      ionizer.setFormula("\"Protonation\"+H");
      ionizer.setNominalCharge(1);
      ionizer.setLevel(1);

      AND_WHEN("The Ionizer is output as a string without the title")
      {
        QString expected_string =
          "Ionizer destination state. formula: +H - nominal "
          "charge: 1 - "
          "level: 1 - is valid: 1\n"
          "Ionizer current state. formula:  - nominal charge: 0 - level: 0 - "
          "charge: 0 - "
          "was valid: 1\n";

        QString output = ionizer.toString(/*with_title*/ false);

        THEN("That string is tested")
        {
          REQUIRE(output.toStdString() ==expected_string.toStdString());
        }
      }

      AND_WHEN("The Ionizer is output as a string with the title")
      {
        QString output = ionizer.toString(/*with_title*/ true);

        THEN("That string is tested")
        {
          QString expected_string =
            "Ionizer destination state. formula: \"Protonation\"+H - nominal "
            "charge: 1 - "
            "level: 1 - is valid: 1\n"
            "Ionizer current state. formula:  - nominal charge: 0 - level: 0 - "
            "charge: 0 - "
            "was valid: 1\n";

          REQUIRE(output.toStdString() == expected_string.toStdString());
        }
      }
    }
  }
}

SCENARIO(
  "An Ionizer can be initialized with an XML element if it has IsotopicData "
  "available",
  "[Ionizer]")
{
  test_utils_1_letter_ionizer.initializeXpertmassLibrary();

  IsotopicDataLibraryHandler isotopic_data_lib_handler;
  isotopic_data_lib_handler.setFileName("the_file_name.dat");
  qsizetype count_non_isotope_skipped_items = 0;
  qsizetype loaded_isotope_count =
    isotopic_data_lib_handler.loadData(count_non_isotope_skipped_items);
  IsotopicDataCstSPtr isotopic_data_csp =
    isotopic_data_lib_handler.getIsotopicData();
  REQUIRE(loaded_isotope_count + count_non_isotope_skipped_items ==
          IsoSpec::isospec_number_of_isotopic_entries);
  REQUIRE(isotopic_data_csp->size() == loaded_isotope_count);

  // int ionizerule_element_index = 0;
  // int formula_element_index    = 1;
  // int formula_text_index       = 2;
  // int charge_element_index     = 3;
  // int charge_text_index        = 4;
  // int level_element_index      = 5;
  // int level_text_index         = 6;

  QStringList dom_strings{
    "ionizerule", "formula", "\"Protonation\"+H", "charge", "1", "level", "1"};

  QDomDocument document =
    test_utils_1_letter_ionizer.craftIonizeruleDomDocument(dom_strings);

  QDomElement ionizerule_element =
    document.elementsByTagName(dom_strings[0]).item(0).toElement();

  qDebug() << "The document:" << document.toString();

  WHEN(
    "An empty Ionizer instance is initialized by rendering in it the XML "
    "element")
  {
    Ionizer ionizer(isotopic_data_csp);

    qDebug() << "Now rendering the XML element.";

    REQUIRE(ionizer.renderXmlIonizeRuleElement(ionizerule_element) == true);

    THEN("Its member data are initialized")
    {
      REQUIRE(ionizer.getFormulaCstRef().getTitle().toStdString() ==
              "Protonation");
      REQUIRE(ionizer.getFormulaCstRef().getActionFormula().toStdString() ==
              "+H");
      REQUIRE(ionizer.getNominalCharge() == 1);
      REQUIRE(ionizer.getLevel() == 1);
    }

    WHEN("The Ionizer is output as an XML element string")
    {
      QString output_xml_string =
        ionizer.formatXmlIonizeRuleElement(0, Utils::xmlIndentationToken);

      QString theoretically_expected = "";
      theoretically_expected += "<ionizerule>\n";
      theoretically_expected += " <formula>\"Protonation\"+H</formula>\n";
      theoretically_expected += " <charge>1</charge>\n";
      theoretically_expected += " <level>1</level>\n";
      theoretically_expected += "</ionizerule>\n";

      THEN("That string matches the expected string")
      {
        REQUIRE(output_xml_string.toStdString() ==
                theoretically_expected.toStdString());
      }
    }
  }
}

SCENARIO("An Ionizer documents itself as an XML element", "[Ionizer]")
{
  test_utils_1_letter_ionizer.initializeXpertmassLibrary();

  IsotopicDataLibraryHandler isotopic_data_lib_handler;
  isotopic_data_lib_handler.setFileName("the_file_name.dat");
  qsizetype count_non_isotope_skipped_items = 0;
  qsizetype loaded_isotope_count =
    isotopic_data_lib_handler.loadData(count_non_isotope_skipped_items);
  IsotopicDataCstSPtr isotopic_data_csp =
    isotopic_data_lib_handler.getIsotopicData();
  REQUIRE(loaded_isotope_count + count_non_isotope_skipped_items ==
          IsoSpec::isospec_number_of_isotopic_entries);
  REQUIRE(isotopic_data_csp->size() == loaded_isotope_count);

  GIVEN("A fully initialized Ionizer")
  {
    Ionizer ionizer(isotopic_data_csp, "\"Protonation\"+H", 1, 1);

    THEN("Its member data are updated")
    {
      REQUIRE(ionizer.getFormulaCstRef().getTitle().toStdString() ==
              "Protonation");
      REQUIRE(ionizer.getFormulaCstRef().getActionFormula().toStdString() ==
              "+H");
      REQUIRE(ionizer.getNominalCharge() == 1);
      REQUIRE(ionizer.getLevel() == 1);
      REQUIRE(ionizer.charge() == 1);
    }

    WHEN("The Ionizer is output as an XML element string")
    {
      QString output_xml_string = ionizer.formatXmlIonizeRuleElement(0, " ");

      QString theoretically_expected = "";
      theoretically_expected += "<ionizerule>\n";
      theoretically_expected += " <formula>\"Protonation\"+H</formula>\n";
      theoretically_expected += " <charge>1</charge>\n";
      theoretically_expected += " <level>1</level>\n";
      theoretically_expected += "</ionizerule>\n";

      THEN("That string matches the expected string")
      {
        REQUIRE(output_xml_string.toStdString() ==
                theoretically_expected.toStdString());
      }
    }
  }
}

SCENARIO("An Ionizer ionizes an unionized analyte", "[Ionizer]")
{
  test_utils_1_letter_ionizer.initializeXpertmassLibrary();

  PolChemDefCstSPtr pol_chem_def_csp =
    test_utils_1_letter_ionizer.msp_polChemDef;
  IsotopicDataCstSPtr isotopic_data_csp =
    pol_chem_def_csp->getIsotopicDataCstSPtr();

  REQUIRE(pol_chem_def_csp->getCodeLength() == 1);
  REQUIRE(pol_chem_def_csp->getMonomersCstRef().size() == 21);
  REQUIRE(pol_chem_def_csp->getModifsCstRef().size() == 26);
  // REQUIRE(pol_chem_def_csp->crossLinkerList().size() == 2);
  // REQUIRE(pol_chem_def_csp->cleaveSpecList().size() == 8);
  // REQUIRE(pol_chem_def_csp->fragSpecList().size() == 7);

  ErrorList error_list;

  GIVEN(
    "A fully initialized Ionizer and initial masses for an unionized analyte")
  {
    Ionizer mono_protonator(isotopic_data_csp, "\"Protonation\"+H", 1, 1);
    REQUIRE(mono_protonator.validate(&error_list));

    double analyte_mono = 17325.7923591224;
    double analyte_avg  = 17336.8283764289;

    WHEN("The analyte is monoprotonated")
    {
      REQUIRE(mono_protonator.ionize(analyte_mono, analyte_avg) ==
              Enums::IonizationOutcome::IONIZED);

      THEN("The masses become m/z values")
      {
        REQUIRE_THAT(
          analyte_mono,
          Catch::Matchers::WithinAbs(17326.8001841546, 0.0000000001));
        REQUIRE_THAT(
          analyte_avg,
          Catch::Matchers::WithinAbs(17337.8363178973, 0.0000000001));

        AND_THEN("The analyte has to be ionized")
        {
          REQUIRE(mono_protonator.isIonized());
        }
      }

      AND_WHEN("The analyte is deionized")
      {
        REQUIRE(mono_protonator.deionize(analyte_mono, analyte_avg) ==
                Enums::IonizationOutcome::DEIONIZED);

        THEN("The masses become molecular masses again")
        {
          REQUIRE_THAT(
            analyte_mono,
            Catch::Matchers::WithinAbs(17325.7923591224, 0.0000000001));
          REQUIRE_THAT(
            analyte_avg,
            Catch::Matchers::WithinAbs(17336.8283764289, 0.0000000001));

          AND_THEN("The analyte has to be deionized")
          {
            REQUIRE_FALSE(mono_protonator.isIonized());
          }
        }
      }
    }
  }
}

} // namespace libXpertMassCore
} // namespace MsXpS
