// --------------------------------------------------------------------------
//                   OpenMS -- Open-Source Mass Spectrometry
// --------------------------------------------------------------------------
// Copyright The OpenMS Team -- Eberhard Karls University Tuebingen,
// ETH Zurich, and Freie Universitaet Berlin 2002-2020.
//
// This software is released under a three-clause BSD license:
//  * Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
//  * Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
//  * Neither the name of any author or any participating institution
//    may be used to endorse or promote products derived from this software
//    without specific prior written permission.
// For a full list of authors, refer to the file AUTHORS.
// --------------------------------------------------------------------------
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL ANY OF THE AUTHORS OR THE CONTRIBUTING
// INSTITUTIONS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
// OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
// OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
// ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// --------------------------------------------------------------------------
// $Maintainer: Timo Sachsenberg$
// $Authors: Erhan Kenar, Chris Bielow $
// --------------------------------------------------------------------------

#include <OpenMS/CONCEPT/ClassTest.h>
#include <OpenMS/test_config.h>

///////////////////////////
#include <OpenMS/ANALYSIS/ID/AccurateMassSearchEngine.h>
#include <OpenMS/CONCEPT/FuzzyStringComparator.h>
#include <OpenMS/CONCEPT/Constants.h>
#include <OpenMS/FORMAT/ConsensusXMLFile.h>
#include <OpenMS/FORMAT/FeatureXMLFile.h>
#include <OpenMS/FORMAT/MzTab.h>
#include <OpenMS/FORMAT/MzTabFile.h>
#include <OpenMS/KERNEL/Feature.h>
#include <OpenMS/KERNEL/ConsensusFeature.h>
#include <OpenMS/KERNEL/FeatureMap.h>
#include <OpenMS/KERNEL/ConsensusMap.h>

#include <OpenMS/KERNEL/MSSpectrum.h>
#include <OpenMS/KERNEL/MSExperiment.h>

///////////////////////////

using namespace OpenMS;
using namespace std;

START_TEST(AccurateMassSearchEngine, "$Id$")

/////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////

AccurateMassSearchEngine* ptr = nullptr;
AccurateMassSearchEngine* null_ptr = nullptr;
START_SECTION(AccurateMassSearchEngine())
{
    ptr = new AccurateMassSearchEngine();
    TEST_NOT_EQUAL(ptr, null_ptr)
}
END_SECTION

START_SECTION(virtual ~AccurateMassSearchEngine())
{
    delete ptr;
}
END_SECTION

START_SECTION([EXTRA]AdductInfo)
{
  EmpiricalFormula ef_empty;
  // make sure an empty formula has no weight (we rely on that in AdductInfo's getMZ() and getNeutralMass()
  TEST_EQUAL(ef_empty.getMonoWeight(), 0)

  // now we test if converting from neutral mass to m/z and back recovers the input value using different adducts
  {
  // testing M;-2  // intrinsic doubly negative charge
    AdductInfo ai("TEST_INTRINSIC", ef_empty, -2, 1);
    double neutral_mass=1000; // some mass...
    double mz = ai.getMZ(neutral_mass);
    double neutral_mass_recon = ai.getNeutralMass(mz);
    TEST_REAL_SIMILAR(neutral_mass, neutral_mass_recon);
  }
  { // testing M+Na+H;+2
    EmpiricalFormula simpleAdduct("HNa");
    AdductInfo ai("TEST_WITHADDUCT", simpleAdduct, 2, 1);
    double neutral_mass=1000; // some mass...
    double mz = ai.getMZ(neutral_mass);
    double neutral_mass_recon = ai.getNeutralMass(mz);
    TEST_REAL_SIMILAR(neutral_mass, neutral_mass_recon);
  }

}
END_SECTION

Param ams_param;
ams_param.setValue("db:mapping", ListUtils::create<String>(String(OPENMS_GET_TEST_DATA_PATH("reducedHMDBMapping.tsv"))));
ams_param.setValue("db:struct", ListUtils::create<String>(String(OPENMS_GET_TEST_DATA_PATH("reducedHMDB2StructMapping.tsv"))));
ams_param.setValue("keep_unidentified_masses", "true");
ams_param.setValue("mzTab:exportIsotopeIntensities", "true");
AccurateMassSearchEngine ams;
ams.setParameters(ams_param);

START_SECTION(void init())
  NOT_TESTABLE // tested below
END_SECTION

START_SECTION((void queryByMZ(const double& observed_mz, const Int& observed_charge, const String& ion_mode, std::vector<AccurateMassSearchResult>& results) const))
{
  std::vector<AccurateMassSearchResult> hmdb_results_pos;

  // test 'ams' not initialized
  TEST_EXCEPTION(Exception::IllegalArgument, ams.queryByMZ(1234, 1, "positive", hmdb_results_pos));
  ams.init();

  // test invalid scan polarity
  TEST_EXCEPTION(Exception::InvalidParameter, ams.queryByMZ(1234, 1, "this_is_an_invalid_ionmode", hmdb_results_pos));

  // test the actual query
  {
    Param ams_param_tmp = ams_param;
    ams_param_tmp.setValue("mass_error_value", 17.0);
    ams.setParameters(ams_param_tmp);
    ams.init();
    // -- positive mode
    // expected hit: C17H11N5 with neutral mass ~285.101445377
    double m = EmpiricalFormula("C17H11N5").getMonoWeight(); 
    double mz = m / 1 + EmpiricalFormula("Na").getMonoWeight() - Constants::ELECTRON_MASS_U; // assume M+Na;+1 as charge
    std::cout << "mz query mass:" << mz << "\n\n";
    // we'll get some other hits as well...
    String id_list_pos[] = {"C10H17N3O6S", "C15H16O7", "C14H14N2OS2", "C16H15NO4",
                            "C17H11N5" /* this one we want! */,
                            "C10H14NO6P", "C14H12O4", "C7H6O2"};
                         //{"C10H17N3O6S", "C15H16O7", "C14H14N2OS2", "C16H15NO4", "C17H11N5", "C10H14NO6P", "C14H12O4", "C7H6O2"};

                         // 290.05475446	C14H14N2OS2	HMDB:HMDB38641 missing

    Size id_list_pos_length(sizeof(id_list_pos)/sizeof(id_list_pos[0]));
    ams.queryByMZ(mz, 1, "positive", hmdb_results_pos);
    ams.setParameters(ams_param); // reset to default 5ppm
    ams.init();
    TEST_EQUAL(hmdb_results_pos.size(), id_list_pos_length)
    ABORT_IF(hmdb_results_pos.size() != id_list_pos_length)
    for (Size i = 0; i < id_list_pos_length; ++i)
    {
      TEST_STRING_EQUAL(hmdb_results_pos[i].getFormulaString(), id_list_pos[i])
      std::cout << hmdb_results_pos[i] << std::endl;
    }
    TEST_EQUAL(hmdb_results_pos[4].getFormulaString(), "C17H11N5"); // correct hit?
    TEST_REAL_SIMILAR(hmdb_results_pos[4].getQueryMass(), m); // was the mass correctly reconstructed internally?
    TEST_REAL_SIMILAR(abs(hmdb_results_pos[4].getMZErrorPPM()), 0.0); // ppm error within float precision? 

  }
  
  // -- negative mode 
  // expected hit: C17H20N2S with neutral mass ~284.13472	
  {
    std::vector<AccurateMassSearchResult> hmdb_results_neg;
    double m = EmpiricalFormula("C17H20N2S").getMonoWeight(); 
    double mz = m / 3 - Constants::PROTON_MASS_U; // assume M-3H;-3 as charge
    // manual check:
    // double mass_recovered = mz * 3 - EmpiricalFormula("H-3").getMonoWeight() - Constants::ELECTRON_MASS_U*3;
    ams.queryByMZ(mz, 3, "negative", hmdb_results_neg);
    ABORT_IF(hmdb_results_neg.size() != 1)
    std::cout << hmdb_results_neg[0] << std::endl;
    TEST_EQUAL(hmdb_results_neg[0].getFormulaString(), "C17H20N2S"); // correct hit?
    TEST_REAL_SIMILAR(hmdb_results_neg[0].getQueryMass(), m); // was the mass correctly reconstructed internally?
    TEST_EQUAL(abs(hmdb_results_neg[0].getMZErrorPPM()) < 0.0002, true); // ppm error within float precision? .. should be ~0.0001576..
  }
}
END_SECTION

AccurateMassSearchEngine ams_feat_test;
ams_feat_test.setParameters(ams_param);
ams_feat_test.init();
String feat_query_pos[] = {"C23H45NO4", "C20H37NO3", "C22H41NO"};

START_SECTION((void queryByFeature(const Feature& feature, const Size& feature_index, const String& ion_mode, std::vector<AccurateMassSearchResult>& results) const))
{
  Feature test_feat;
  test_feat.setRT(300.0);
  test_feat.setMZ(399.33486);
  test_feat.setIntensity(100.0);
  test_feat.setMetaValue("num_of_masstraces", 3);
  test_feat.setCharge(1.0);

  vector<double> masstrace_intenstiy = {100.0, 26.1, 4.0};
  test_feat.setMetaValue("masstrace_intensity", masstrace_intenstiy);

  //test_feat.setMetaValue("masstrace_intensity_0", 100.0);
  //test_feat.setMetaValue("masstrace_intensity_1", 26.1);
  //test_feat.setMetaValue("masstrace_intensity_2", 4.0);

  std::vector<AccurateMassSearchResult> results;
  
  // invalid scan_polarity
  TEST_EXCEPTION(Exception::InvalidParameter, ams_feat_test.queryByFeature(test_feat, 0, "invalid_scan_polatority", results));
  
  // actual test
  ams_feat_test.queryByFeature(test_feat, 0, "positive", results);

  TEST_EQUAL(results.size(), 3)

  for (Size i = 0; i < results.size(); ++i)
  {
    TEST_REAL_SIMILAR(results[i].getObservedRT(), 300.0)
    TEST_REAL_SIMILAR(results[i].getObservedIntensity(), 100.0)
  }

  Size feat_query_size(sizeof(feat_query_pos)/sizeof(feat_query_pos[0]));

  ABORT_IF(results.size() != feat_query_size)
  for (Size i = 0; i < feat_query_size; ++i)
  {
    TEST_STRING_EQUAL(results[i].getFormulaString(), feat_query_pos[i])
  }
}
END_SECTION


START_SECTION((void queryByConsensusFeature(const ConsensusFeature& cfeat, const Size& cf_index, const Size& number_of_maps, const String& ion_mode, std::vector<AccurateMassSearchResult>& results) const))
{
  ConsensusFeature cons_feat;
  cons_feat.setRT(300.0);
  cons_feat.setMZ(399.33486);
  cons_feat.setIntensity(100.0);
  cons_feat.setCharge(1.0);

  FeatureHandle fh1, fh2, fh3;
  fh1.setRT(300.0);
  fh1.setMZ(399.33485);
  fh1.setIntensity(100.0);
  fh1.setCharge(1.0);
  fh1.setMapIndex(0);

  fh2.setRT(310.0);
  fh2.setMZ(399.33486);
  fh2.setIntensity(300.0);
  fh2.setCharge(1.0);
  fh2.setMapIndex(1);

  fh3.setRT(290.0);
  fh3.setMZ(399.33487);
  fh3.setIntensity(500.0);
  fh3.setCharge(1.0);
  fh3.setMapIndex(2);

  cons_feat.insert(fh1);
  cons_feat.insert(fh2);
  cons_feat.insert(fh3);
  cons_feat.computeConsensus();
  
  std::vector<AccurateMassSearchResult> results;

  TEST_EXCEPTION(Exception::InvalidParameter, ams_feat_test.queryByConsensusFeature(cons_feat, 0, 3, "blabla", results)); // invalid scan_polarity
  ams_feat_test.queryByConsensusFeature(cons_feat, 0, 3, "positive", results);

  TEST_EQUAL(results.size(), 3)

  for (Size i = 0; i < results.size(); ++i)
  {
      TEST_REAL_SIMILAR(results[i].getObservedRT(), 300.0)
      TEST_REAL_SIMILAR(results[i].getObservedIntensity(), 0.0)
  }

  // std::cout << cons_feat.getMZ() << " " << results.size() << std::endl;

  for (Size i = 0; i < results.size(); ++i)
  {
    std::vector<double> indiv_ints = results[i].getIndividualIntensities();
    TEST_EQUAL(indiv_ints.size(), 3)

    ABORT_IF(indiv_ints.size() != 3)
    TEST_REAL_SIMILAR(indiv_ints[0], fh1.getIntensity());
    TEST_REAL_SIMILAR(indiv_ints[1], fh2.getIntensity());
    TEST_REAL_SIMILAR(indiv_ints[2], fh3.getIntensity());
  }

  Size feat_query_size(sizeof(feat_query_pos)/sizeof(feat_query_pos[0]));

  ABORT_IF(results.size() != feat_query_size)
  for (Size i = 0; i < feat_query_size; ++i)
  {
    TEST_STRING_EQUAL(results[i].getFormulaString(), feat_query_pos[i])
  }
}
END_SECTION

FuzzyStringComparator fsc;
// fsc.setAcceptableAbsolute((3.04011223650013 - 3.04011223637974)*1.1); // 1.3242891228060217e-10
// also Linux may give slightly different results depending on optimization level (O0 vs O1) 
// note that the default value for TEST_REAL_SIMILAR is 1e-5, see ./source/CONCEPT/ClassTest.cpp
fsc.setAcceptableAbsolute(1e-8);
StringList sl;
sl.push_back("xml-stylesheet");
sl.push_back("IdentificationRun");
fsc.setWhitelist(sl);

START_SECTION((void run(FeatureMap&, MzTab&) const))
{
  FeatureMap exp_fm;
  FeatureXMLFile().load(OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_input1.featureXML"), exp_fm);
  {
    MzTab test_mztab;
    ams_feat_test.run(exp_fm, test_mztab);

    // test annotation of input
    String tmp_file;
    NEW_TMP_FILE(tmp_file);
    FeatureXMLFile ff;
    ff.store(tmp_file, exp_fm);
    TEST_EQUAL(fsc.compareFiles(tmp_file, OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_output1.featureXML")), true);

    String tmp_mztab_file;
    NEW_TMP_FILE(tmp_mztab_file);
    MzTabFile().store(tmp_mztab_file, test_mztab);
    TEST_EQUAL(fsc.compareFiles(tmp_mztab_file, OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_output1_featureXML.mzTab")), true);
    
    // test use of adduct information
    Param ams_param_tmp = ams_param;
    ams_param_tmp.setValue("use_feature_adducts", "true");
      
    AccurateMassSearchEngine ams_feat_test2;
    ams_feat_test2.setParameters(ams_param_tmp);
    ams_feat_test2.init();

    FeatureMap exp_fm2;
    FeatureXMLFile().load(OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_input1.featureXML"), exp_fm2);
    MzTab test_mztab2;
    ams_feat_test2.run(exp_fm2, test_mztab2);

    String tmp_mztab_file2;
    NEW_TMP_FILE(tmp_mztab_file2);
    MzTabFile().store(tmp_mztab_file2, test_mztab2);
    TEST_EQUAL(fsc.compareFiles(tmp_mztab_file2, OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_output2_featureXML.mzTab")), true);
  }
}
END_SECTION


START_SECTION((void run(ConsensusMap&, MzTab&) const))
  ConsensusMap exp_cm;
  ConsensusXMLFile().load(OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_input1.consensusXML"), exp_cm);
  MzTab test_mztab2;
  ams_feat_test.run(exp_cm, test_mztab2);

  // test annotation of input
  String tmp_file;
  NEW_TMP_FILE(tmp_file);
  ConsensusXMLFile ff;
  ff.store(tmp_file, exp_cm);
  TEST_EQUAL(fsc.compareFiles(tmp_file, OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_output1.consensusXML")), true);

  String tmp_mztab_file;
  NEW_TMP_FILE(tmp_mztab_file);
  MzTabFile().store(tmp_mztab_file, test_mztab2);
  TEST_EQUAL(fsc.compareFiles(tmp_mztab_file, OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_output1_consensusXML.mzTab")), true);
END_SECTION

START_SECTION([EXTRA] template <typename MAPTYPE> void resolveAutoMode_(const MAPTYPE& map))
  FeatureMap exp_fm;
  FeatureXMLFile().load(OPENMS_GET_TEST_DATA_PATH("AccurateMassSearchEngine_input1.featureXML"), exp_fm);
  FeatureMap fm_p = exp_fm;
  AccurateMassSearchEngine ams;
  MzTab mzt;
  Param p;
  p.setValue("ionization_mode","auto");
  p.setValue("db:mapping", ListUtils::create<String>(String(OPENMS_GET_TEST_DATA_PATH("reducedHMDBMapping.tsv"))));
  p.setValue("db:struct", ListUtils::create<String>(String(OPENMS_GET_TEST_DATA_PATH("reducedHMDB2StructMapping.tsv"))));
  ams.setParameters(p);
  ams.init();

  TEST_EXCEPTION(Exception::InvalidParameter, ams.run(fm_p, mzt)); // 'fm_p' has no scan_polarity meta value
  fm_p[0].setMetaValue("scan_polarity", "something;somethingelse");
  TEST_EXCEPTION(Exception::InvalidParameter, ams.run(fm_p, mzt)); // 'fm_p' scan_polarity meta value wrong

  fm_p[0].setMetaValue("scan_polarity", "positive"); // should run ok
  ams.run(fm_p, mzt);

  fm_p[0].setMetaValue("scan_polarity", "negative"); // should run ok
  ams.run(fm_p, mzt);
END_SECTION

/////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////
END_TEST
