//
//  Copyright (C) 2018-2021 Greg Landrum and other RDKit contributors
//
//   @@ All Rights Reserved @@
//  This file is part of the RDKit.
//  The contents are covered by the terms of the BSD license
//  which is included in the file license.txt, found at the root
//  of the RDKit source tree.
//

#include <catch2/catch_all.hpp>
#ifdef RDK_BUILD_THREADSAFE_SSS
#include <future>
#include <thread>
#endif

#include <GraphMol/RDKitBase.h>
#include <GraphMol/MolPickler.h>
#include <GraphMol/QueryAtom.h>
#include <GraphMol/QueryBond.h>
#include <GraphMol/QueryOps.h>
#include <GraphMol/Chirality.h>
#include <GraphMol/test_fixtures.h>
#include <GraphMol/Canon.h>
#include <GraphMol/SmilesParse/SmilesParse.h>
#include <GraphMol/SmilesParse/SmilesWrite.h>
#include <GraphMol/SmilesParse/SmartsWrite.h>
#include <GraphMol/Substruct/SubstructMatch.h>
#include <GraphMol/FileParsers/FileParsers.h>
#include <GraphMol/FileParsers/MolFileStereochem.h>
#include <GraphMol/SmilesParse/CanonicalizeStereoGroups.h>

using namespace RDKit;

TEST_CASE("Github #1972", "[SMILES][bug]") {
  SECTION("basics") {
    std::vector<std::vector<std::string>> smiles = {
        {"[C@@]1(Cl)(F)(I).Br1", "[C@@](Br)(Cl)(F)(I)"},
        {"[C@@](Cl)(F)(I)1.Br1", "[C@@](Cl)(F)(I)Br"},
        {"[C@@](Cl)1(F)(I).Br1", "[C@@](Cl)(Br)(F)(I)"},
        {"[C@@](Cl)(F)1(I).Br1", "[C@@](Cl)(F)(Br)(I)"}};
    for (const auto &pr : smiles) {
      std::unique_ptr<ROMol> m1(SmilesToMol(pr[0]));
      std::unique_ptr<ROMol> m2(SmilesToMol(pr[1]));
      REQUIRE(m1);
      REQUIRE(m2);
      auto csmi1 = MolToSmiles(*m1);
      auto csmi2 = MolToSmiles(*m2);
      CHECK(csmi1 == csmi2);
    }
  }
  SECTION("further examples") {
    std::vector<std::vector<std::string>> smiles = {
        {"[C@@]1(Cl)2(I).Br1.F2", "[C@@](Br)(Cl)(F)(I)"},
        {"[C@@](Cl)2(I)1.Br1.F2", "[C@@](Cl)(F)(I)Br"},
        {"[C@@]12(Cl)(I).Br1.F2", "[C@@](Br)(F)(Cl)(I)"},
        {"[C@@]21(Cl)(I).Br1.F2", "[C@@](F)(Br)(Cl)(I)"},
        {"[C@@](Cl)12(I).Br1.F2", "[C@@](Cl)(Br)(F)(I)"},
        {"[C@@](Cl)21(I).Br1.F2", "[C@@](Cl)(F)(Br)(I)"},
        {"[C@@](Cl)(I)21.Br1.F2", "[C@@](Cl)(I)(F)(Br)"},
        {"[C@@](Cl)(I)12.Br1.F2", "[C@@](Cl)(I)(Br)(F)"}};
    for (const auto &pr : smiles) {
      std::unique_ptr<ROMol> m1(SmilesToMol(pr[0]));
      std::unique_ptr<ROMol> m2(SmilesToMol(pr[1]));
      REQUIRE(m1);
      REQUIRE(m2);
      auto csmi1 = MolToSmiles(*m1);
      auto csmi2 = MolToSmiles(*m2);
      CHECK(csmi1 == csmi2);
    }
  }
}

TEST_CASE("Github #2029", "[SMILES][bug]") {
  SECTION("wedging") {
    std::unique_ptr<ROMol> m1(SmilesToMol("CN[C@H](Cl)C(=O)O"));
    REQUIRE(m1);
    m1->getBondWithIdx(1)->setBondDir(Bond::BEGINWEDGE);
    bool doKekule = false, allBondsExplicit = false;
    CHECK("" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(1), -1, doKekule,
                                           allBondsExplicit));
    allBondsExplicit = true;
    CHECK("-" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(1), -1, doKekule,
                                            allBondsExplicit));
  }
  SECTION("direction") {
    std::unique_ptr<ROMol> m1(SmilesToMol("C/C=C/C"));
    REQUIRE(m1);
    bool doKekule = false, allBondsExplicit = false;
    CHECK("" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(0), -1, doKekule,
                                           allBondsExplicit));
    CHECK("" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(2), -1, doKekule,
                                           allBondsExplicit));
    allBondsExplicit = true;
    CHECK("/" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(0), -1, doKekule,
                                            allBondsExplicit));
    CHECK("/" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(2), -1, doKekule,
                                            allBondsExplicit));
  }
  SECTION("aromatic double bonds") {
    std::unique_ptr<RWMol> m1(SmilesToMol("c1ccccc1"));
    REQUIRE(m1);
    bool markAtomsBonds = false;
    MolOps::Kekulize(*m1, markAtomsBonds);
    bool doKekule = false, allBondsExplicit = false;
    CHECK("" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(0), -1, doKekule,
                                           allBondsExplicit));
    CHECK("" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(1), -1, doKekule,
                                           allBondsExplicit));
    allBondsExplicit = true;
    CHECK("=" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(0), -1, doKekule,
                                            allBondsExplicit));
    CHECK("-" == SmilesWrite::GetBondSmiles(m1->getBondWithIdx(1), -1, doKekule,
                                            allBondsExplicit));
  }
}

TEST_CASE("Smiles literals", "[SMILES]") {
  auto mol = "c1ccccc1"_smiles;
  REQUIRE(mol);
  CHECK(6 == mol->getNumAtoms());
  auto fail1 = "c1ccccc"_smiles;
  REQUIRE(!fail1);
  auto fail2 = "c1cccn1"_smiles;
  REQUIRE(!fail2);
}

TEST_CASE("Smarts literals", "[Smarts]") {
  auto mol = "c1ccc[c,n]c1"_smarts;
  REQUIRE(mol);
  CHECK(6 == mol->getNumAtoms());
  auto fail1 = "c1ccccc"_smarts;
  REQUIRE(!fail1);
  auto mol2 = "c1cccn1"_smarts;
  REQUIRE(mol2);
}

TEST_CASE(
    "github #2197 and #2237: handling of aromatic main group atoms in SMARTS",
    "[Smarts]") {
  std::vector<std::string> smarts = {
      "[si]1ccccc1",
      "[as]1ccccc1",
      "[se]1ccccc1",
      "[te]1ccccc1",

  };
  SECTION("#2197") {
    for (const auto &sma : smarts) {
      std::unique_ptr<ROMol> mol(SmartsToMol(sma));
      REQUIRE(mol);
      CHECK(6 == mol->getNumAtoms());
      REQUIRE(mol->getAtomWithIdx(0)->hasQuery());
      REQUIRE(static_cast<QueryAtom *>(mol->getAtomWithIdx(0))
                  ->getQuery()
                  ->getDescription() == "AtomType");
    }
  }
  SECTION("#2237") {
    for (const auto &sma : smarts) {
      std::unique_ptr<ROMol> mol(SmartsToMol(sma));
      REQUIRE(mol);
      REQUIRE(MolToSmarts(*mol) == sma);
    }
  }
}

TEST_CASE("github #2257: writing cxsmiles", "[smiles][cxsmiles]") {
  SECTION("basics") {
    auto mol = "OCC"_smiles;
    REQUIRE(mol);
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CCO");
  }
  SECTION("atom labels") {
    auto mol = "CCC |$R1;;R2$|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getAtomWithIdx(0)->getProp<std::string>(
              common_properties::atomLabel) == "R1");
    CHECK(mol->getAtomWithIdx(2)->getProp<std::string>(
              common_properties::atomLabel) == "R2");
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CCC |$R1;;R2$|");
  }
  SECTION("atom ordering") {
    auto mol = "OC(F)C |$R1;;R2;R3$|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getAtomWithIdx(0)->getProp<std::string>(
              common_properties::atomLabel) == "R1");
    CHECK(mol->getAtomWithIdx(2)->getProp<std::string>(
              common_properties::atomLabel) == "R2");
    CHECK(mol->getAtomWithIdx(3)->getProp<std::string>(
              common_properties::atomLabel) == "R3");
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CC(O)F |$R3;;R1;R2$|");
  }
  SECTION("atom values") {
    auto mol = "COCC |$_AV:;bar;;foo$|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getAtomWithIdx(3)->getProp<std::string>(
              common_properties::molFileValue) == "foo");
    CHECK(mol->getAtomWithIdx(1)->getProp<std::string>(
              common_properties::molFileValue) == "bar");
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CCOC |$_AV:foo;;bar;$|");
  }

  SECTION("radicals") {
    auto mol = "[Fe]N([O])[O] |^1:2,3|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getAtomWithIdx(1)->getNumRadicalElectrons() == 0);
    CHECK(mol->getAtomWithIdx(2)->getNumRadicalElectrons() == 1);
    CHECK(mol->getAtomWithIdx(3)->getNumRadicalElectrons() == 1);

    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "[O]N([O])[Fe] |^1:0,2|");
  }
  SECTION("radicals2") {
    auto mol = "[CH]C[CH2] |^1:2,^2:0|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getAtomWithIdx(1)->getNumRadicalElectrons() == 0);
    CHECK(mol->getAtomWithIdx(2)->getNumRadicalElectrons() == 1);
    CHECK(mol->getAtomWithIdx(0)->getNumRadicalElectrons() == 2);

    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "[CH]C[CH2] |^1:2,^2:0|");
  }
  SECTION("coordinates") {
    auto mol = "OC |(0,.75,;0,-.75,)|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getNumConformers() == 1);

    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CO |(0,-0.75,;0,0.75,)|");
  }
  SECTION("coordinates3d") {
    auto mol = "OC |(0,.75,0.1;0,-.75,-0.1)|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getNumConformers() == 1);

    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CO |(0,-0.75,-0.1;0,0.75,0.1)|");
  }
  SECTION("atom props") {
    auto mol = "N1CC1C |atomProp:0.p2.v2:0.p1.v1:1.p2.v2:1.p1.v1;2;3|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getNumAtoms() == 4);
    CHECK(mol->getAtomWithIdx(0)->hasProp("p1"));
    CHECK(mol->getAtomWithIdx(0)->getProp<std::string>("p1") == "v1");
    CHECK(mol->getAtomWithIdx(0)->hasProp("p2"));
    CHECK(mol->getAtomWithIdx(0)->getProp<std::string>("p2") == "v2");
    CHECK(mol->getAtomWithIdx(1)->hasProp("p2"));
    CHECK(mol->getAtomWithIdx(1)->getProp<std::string>("p2") == "v2");
    CHECK(mol->getAtomWithIdx(1)->hasProp("p1"));
    CHECK(mol->getAtomWithIdx(1)->getProp<std::string>("p1") == "v1;2;3");

    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CC1CN1 |atomProp:2.p2.v2:2.p1.v1;2;3:3.p2.v2:3.p1.v1|");
  }
  SECTION("atom props and values") {
    //"CN |$_AV:atomv0;atomv1$,atomProp:0.p2.v2:1.p2.v1|";
    auto mol = "CN |atomProp:0.p2.v2:1.p1.v1,$_AV:val1;val2$|"_smiles;
    REQUIRE(mol);
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CN |$_AV:val1;val2$,atomProp:0.p2.v2:1.p1.v1|");
  }
  SECTION("enhanced stereo 1") {
    auto mol = "C[C@H](F)[C@H](C)[C@@H](C)Br |a:1,o1:3,5|"_smiles;
    REQUIRE(mol);
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "C[C@H](F)[C@@H](C)[C@H](C)Br |a:1,o1:3,5|");
  }

  SECTION("enhanced stereo 2") {
    auto mol = "C[C@H](O)[C@H](CC)F |o1:1,3|"_smiles;
    REQUIRE(mol);
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CC[C@H](F)[C@H](C)O |o1:2,4|");
  }

  SECTION("enhanced stereo 3") {
    auto mol =
        "C[C@@H]1N[C@H](C)[C@@H]([C@H](C)[C@@H]1C)C1[C@@H](C)O[C@@H](C)[C@@H](C)[C@H]1C |a:5,o1:1,8,o2:14,16,&1:11,18,&2:3,6,r|"_smiles;
    REQUIRE(mol);
    auto smi = MolToCXSmiles(*mol);
    CHECK(
        smi ==
        "C[C@H]1N[C@H](C)[C@H](C2[C@H](C)O[C@H](C)[C@H](C)[C@@H]2C)[C@H](C)[C@H]1C |a:5,o1:1,18,o2:10,12,&1:3,16,&2:7,14|");
  }

  SECTION("enhanced stereo 4") {
    auto mol = "C[C@@H]1CCO[C@H](C)C1 |a:1,5,r|"_smiles;
    REQUIRE(mol);
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "C[C@@H]1CCO[C@H](C)C1 |a:1,5|");
  }

  SECTION("enhanced stereo with other properties") {
    auto mol = "CC[C@H](C)O |atomProp:3.p2.v2,o1:2|"_smiles;
    REQUIRE(mol);
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "CC[C@H](C)O |atomProp:3.p2.v2,o1:2|");
  }

  SECTION("mol fragments1") {
    auto mol = "Cl.OC |(1,0,0;0,.75,0.1;0,-.75,-0.1)|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getNumConformers() == 1);

    std::vector<int> atomsToUse = {1, 2};
    auto smi = MolFragmentToCXSmiles(*mol, atomsToUse);
    CHECK(smi == "CO |(0,-0.75,-0.1;0,0.75,0.1)|");
  }
  SECTION("mol fragments2") {
    auto mol = "Cl.N1CC1C |atomProp:1.p2.v1:1.p1.v1:2.p2.v2:2.p1.v2|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getNumAtoms() == 5);
    CHECK(!mol->getAtomWithIdx(0)->hasProp("p1"));
    CHECK(mol->getAtomWithIdx(1)->hasProp("p1"));
    CHECK(mol->getAtomWithIdx(1)->getProp<std::string>("p1") == "v1");

    std::vector<int> atomsToUse = {1, 2, 3, 4};
    auto smi = MolFragmentToCXSmiles(*mol, atomsToUse);
    CHECK(smi == "CC1CN1 |atomProp:2.p2.v2:2.p1.v2:3.p2.v1:3.p1.v1|");
  }

  SECTION("mol fragments3") {
    auto mol = "Cl.[CH]C[CH2] |^1:3,^2:1|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getAtomWithIdx(2)->getNumRadicalElectrons() == 0);
    CHECK(mol->getAtomWithIdx(3)->getNumRadicalElectrons() == 1);
    CHECK(mol->getAtomWithIdx(1)->getNumRadicalElectrons() == 2);

    std::vector<int> atomsToUse = {1, 2, 3};
    auto smi = MolFragmentToCXSmiles(*mol, atomsToUse);
    CHECK(smi == "[CH]C[CH2] |^1:2,^2:0|");
  }
}

TEST_CASE("Github #2148", "[bug][Smiles][Smarts]") {
  SECTION("SMILES") {
    auto mol = "C(=C\\F)\\4.O=C1C=4CCc2ccccc21"_smiles;
    REQUIRE(mol);
    REQUIRE(mol->getBondBetweenAtoms(0, 5));
    CHECK(mol->getBondBetweenAtoms(0, 5)->getBondType() == Bond::DOUBLE);
  }
  SECTION("SMILES edges") {
    auto m1 = "C/C=C/C"_smiles;
    REQUIRE(m1);
    CHECK(m1->getBondBetweenAtoms(2, 1)->getBondType() == Bond::DOUBLE);
    CHECK(m1->getBondBetweenAtoms(2, 1)->getStereo() != Bond::STEREONONE);

    {
      std::vector<std::string> smis = {"C1=C/C.C/1", "C/1=C/C.C1",
                                       "C-1=C/C.C/1", "C/1=C/C.C-1"};
      for (auto smi : smis) {
        std::unique_ptr<RWMol> mol(SmilesToMol(smi));
        REQUIRE(mol);
        CHECK(mol->getBondBetweenAtoms(0, 3)->getBondType() == Bond::SINGLE);
        CHECK(mol->getBondBetweenAtoms(0, 3)->getBondDir() != Bond::NONE);
        CHECK(mol->getBondBetweenAtoms(0, 1)->getBondType() == Bond::DOUBLE);
        CHECK(mol->getBondBetweenAtoms(0, 1)->getStereo() != Bond::STEREONONE);
      }
    }
  }

  SECTION("Writing SMILES") {
    auto mol = "C/C=c1/ncc(=C)cc1"_smiles;
    REQUIRE(mol);
    REQUIRE(mol->getBondBetweenAtoms(1, 2));
    CHECK(mol->getBondBetweenAtoms(1, 2)->getBondType() == Bond::DOUBLE);
    CHECK(mol->getBondBetweenAtoms(1, 2)->getStereo() == Bond::STEREOE);
    auto smi = MolToSmiles(*mol);
    CHECK(smi == "C=c1cc/c(=C\\C)nc1");
  }
}

TEST_CASE("Github #2298", "[bug][Smarts][substructure]") {
  SubstructMatchParameters ps;
  ps.useQueryQueryMatches = true;
  SECTION("basics") {
    auto m1 = "[#6]"_smarts;
    REQUIRE(m1);
    CHECK(SubstructMatch(*m1, *m1, ps).size() == 1);
    auto m2 = "[C]"_smarts;
    REQUIRE(m2);
    CHECK(SubstructMatch(*m2, *m2, ps).size() == 1);
    auto m3 = "[C]"_smarts;
    REQUIRE(m3);
    CHECK(SubstructMatch(*m3, *m3, ps).size() == 1);
  }
  SECTION("a bit more complex") {
    auto m1 = "[CH0+2]"_smarts;
    REQUIRE(m1);
    CHECK(SubstructMatch(*m1, *m1, ps).size() == 1);
  }
}

TEST_CASE("dative ring closures", "[bug][smiles]") {
  SECTION("first closure1") {
    auto m1 = "N->1CCN->[Pt]1"_smiles;
    REQUIRE(m1);
    REQUIRE(m1->getBondBetweenAtoms(0, 4));
    CHECK(m1->getBondBetweenAtoms(0, 4)->getBondType() == Bond::DATIVE);
    CHECK(m1->getBondBetweenAtoms(0, 4)->getBeginAtomIdx() == 0);
  }
  SECTION("first closure2") {
    auto m1 = "[Pt]<-1CCCN1"_smiles;
    REQUIRE(m1);
    REQUIRE(m1->getBondBetweenAtoms(0, 4));
    CHECK(m1->getBondBetweenAtoms(0, 4)->getBondType() == Bond::DATIVE);
    CHECK(m1->getBondBetweenAtoms(0, 4)->getBeginAtomIdx() == 4);
  }
  SECTION("second closure1") {
    auto m1 = "N1CCN->[Pt]<-1"_smiles;
    REQUIRE(m1);
    REQUIRE(m1->getBondBetweenAtoms(0, 4));
    CHECK(m1->getBondBetweenAtoms(0, 4)->getBondType() == Bond::DATIVE);
    CHECK(m1->getBondBetweenAtoms(0, 4)->getBeginAtomIdx() == 0);
  }
  SECTION("second closure2") {
    auto m1 = "[Pt]1CCCN->1"_smiles;
    REQUIRE(m1);
    REQUIRE(m1->getBondBetweenAtoms(0, 4));
    CHECK(m1->getBondBetweenAtoms(0, 4)->getBondType() == Bond::DATIVE);
    CHECK(m1->getBondBetweenAtoms(0, 4)->getBeginAtomIdx() == 4);
  }
  SECTION("branch1") {
    auto m1 = "N(->[Pt])C"_smiles;
    REQUIRE(m1);
    REQUIRE(m1->getBondBetweenAtoms(0, 1));
    CHECK(m1->getBondBetweenAtoms(0, 1)->getBondType() == Bond::DATIVE);
    CHECK(m1->getBondBetweenAtoms(0, 1)->getBeginAtomIdx() == 0);
  }
  SECTION("branch2") {
    auto m1 = "N(->[Pt])C"_smiles;
    REQUIRE(m1);
    REQUIRE(m1->getBondBetweenAtoms(0, 1));
    CHECK(m1->getBondBetweenAtoms(0, 1)->getBondType() == Bond::DATIVE);
    CHECK(m1->getBondBetweenAtoms(0, 1)->getBeginAtomIdx() == 0);
  }
}

TEST_CASE("github#2450: getAtomSmarts() fails for free atoms", "[bug]") {
  SECTION("original report") {
    std::unique_ptr<QueryAtom> qat(new QueryAtom());
    qat->setQuery(makeAtomNumQuery(6));
    auto smarts = SmartsWrite::GetAtomSmarts(qat.get());
    CHECK(smarts == "[#6]");
  }
  SECTION("query bonds") {
    std::unique_ptr<QueryBond> qbnd(new QueryBond(Bond::AROMATIC));
    auto smarts = SmartsWrite::GetBondSmarts(qbnd.get());
    CHECK(smarts == ":");
  }
  SECTION("SMILES works too") {
    std::unique_ptr<Bond> bnd(new Bond(Bond::AROMATIC));
    auto smiles = SmilesWrite::GetBondSmiles(bnd.get());
    CHECK(smiles == ":");
  }
}

TEST_CASE("MolFragmentToSmarts", "[Smarts]") {
  SECTION("BasicFragment") {
    auto m = "CCCCCN"_smiles;
    std::vector<int> indices = {3, 4, 5};
    const auto smarts = MolFragmentToSmarts(*m, indices);
    CHECK(smarts == "[#6]-[#6]-[#7]");
  }
  SECTION("FragmentWithParity1") {
    auto m = "C[C@H](F)CCCN"_smiles;
    std::vector<int> indices = {0, 1, 2, 3};
    const auto smarts = MolFragmentToSmarts(*m, indices);
    CHECK(smarts == "[#6]-[#6@H](-[#9])-[#6]");
  }
  SECTION("FragmentWithParity2") {
    auto m = "C[C@](F)(Cl)CCCN"_smiles;
    std::vector<int> indices = {0, 1, 2, 4};
    const auto smarts = MolFragmentToSmarts(*m, indices);
    CHECK(smarts == "[#6]-[#6@@](-[#9])-[#6]");
  }
  SECTION("FragmentLosingParity") {
    auto m = "C[C@H](F)CCCN"_smiles;
    std::vector<int> indices = {0, 1, 2};
    const auto smarts = MolFragmentToSmarts(*m, indices);
    CHECK(smarts == "[#6]-[#6@H]-[#9]");
  }
  SECTION("FragmentWithSpecifiedBonds") {
    auto m = "C1CC1O"_smiles;
    std::vector<int> atomIndices = {0, 1, 2};
    std::vector<int> bondIndices = {0};
    const auto smarts = MolFragmentToSmarts(*m, atomIndices, &bondIndices);
    CHECK(smarts == "[#6]-[#6].[#6]");
  }
  SECTION("SmartsFragmentFromQueryMol") {
    auto m = "CCCC[C,N]N"_smarts;
    std::vector<int> indices = {3, 4, 5};
    const auto smarts = MolFragmentToSmarts(*m, indices);
    CHECK(smarts == "C[C,N]N");
  }
}

TEST_CASE("github #2667: MolToCXSmiles generates error for empty molecule",
          "[bug][cxsmiles]") {
  SECTION("basics") {
    auto mol = ""_smiles;
    REQUIRE(mol);
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "");
  }
}

TEST_CASE("github #2604: support range-based charge queries from SMARTS",
          "[ranges][smarts]") {
  SECTION("positive") {
    auto query = "[N+{0-1}]"_smarts;
    REQUIRE(query);
    {
      auto m1 = "CN"_smiles;
      REQUIRE(m1);
      CHECK(SubstructMatch(*m1, *query).size() == 1);
    }
    {
      auto m1 = "C[NH3+]"_smiles;
      REQUIRE(m1);
      CHECK(SubstructMatch(*m1, *query).size() == 1);
    }
    {
      auto m1 = "C[NH2+2]"_smiles;
      REQUIRE(m1);
      CHECK(SubstructMatch(*m1, *query).empty());
    }
    {
      auto m1 = "C[NH-]"_smiles;
      REQUIRE(m1);
      CHECK(SubstructMatch(*m1, *query).empty());
    }
  }
  SECTION("negative") {
    auto query = "[N-{0-1}]"_smarts;
    REQUIRE(query);
    {
      auto m1 = "CN"_smiles;
      REQUIRE(m1);
      CHECK(SubstructMatch(*m1, *query).size() == 1);
    }
    {
      auto m1 = "C[NH-]"_smiles;
      REQUIRE(m1);
      CHECK(SubstructMatch(*m1, *query).size() == 1);
    }
    {
      auto m1 = "C[N-2]"_smiles;
      REQUIRE(m1);
      CHECK(SubstructMatch(*m1, *query).empty());
    }
    {
      auto m1 = "C[NH3+]"_smiles;
      REQUIRE(m1);
      CHECK(SubstructMatch(*m1, *query).empty());
    }
  }
}

TEST_CASE("_smarts fails gracefully", "[smarts]") {
  SECTION("empty") {
    auto mol = ""_smarts;
    REQUIRE(mol);
  }
  SECTION("syntax error") {
    auto mol = "C1C"_smarts;
    REQUIRE(!mol);
  }
}

TEST_CASE(
    "github #2801: MolToSmarts may generate invalid SMARTS for bond queries",
    "[bug][smarts]") {
  SECTION("original_report") {
    auto q1 = "*~CCC"_smarts;
    REQUIRE(q1);
    Bond *qb = q1->getBondBetweenAtoms(0, 1);
    BOND_EQUALS_QUERY *bq1 = makeBondOrderEqualsQuery(qb->getBondType());
    qb->setQuery(bq1);
    BOND_EQUALS_QUERY *bq2 = makeBondIsInRingQuery();
    bq2->setNegation(true);
    qb->expandQuery(bq2, Queries::COMPOSITE_AND, true);
    std::string smarts = MolToSmarts(*q1);
    CHECK(smarts == "*!@CCC");
    std::unique_ptr<RWMol> q2(SmartsToMol(smarts));
    REQUIRE(q2);
  }
  SECTION("composite_or") {
    auto q1 = "*~CCC"_smarts;
    REQUIRE(q1);
    Bond *qb = q1->getBondBetweenAtoms(0, 1);
    BOND_EQUALS_QUERY *bq1 = makeBondOrderEqualsQuery(qb->getBondType());
    qb->setQuery(bq1);
    BOND_EQUALS_QUERY *bq2 = makeBondIsInRingQuery();
    bq2->setNegation(true);
    qb->expandQuery(bq2, Queries::COMPOSITE_OR, true);
    // this used to yield *,!@CCC
    std::string smarts = MolToSmarts(*q1);
    CHECK(smarts == "*!@CCC");
    std::unique_ptr<RWMol> q2(SmartsToMol(smarts));
    REQUIRE(q2);
  }
  SECTION("composite_lowand") {
    auto q1 = "*~CCC"_smarts;
    REQUIRE(q1);
    Bond *qb = q1->getBondBetweenAtoms(0, 1);
    BOND_EQUALS_QUERY *bq1 = makeBondOrderEqualsQuery(qb->getBondType());
    qb->setQuery(bq1);
    BOND_EQUALS_QUERY *bq2 = makeBondOrderEqualsQuery(qb->getBondType());
    qb->expandQuery(bq2, Queries::COMPOSITE_OR, true);
    BOND_EQUALS_QUERY *bq3 = makeBondIsInRingQuery();
    bq3->setNegation(true);
    qb->expandQuery(bq3, Queries::COMPOSITE_AND, true);
    std::string smarts = MolToSmarts(*q1);
    CHECK(smarts == "*!@CCC");
    std::unique_ptr<RWMol> q2(SmartsToMol(smarts));
    REQUIRE(q2);
  }
}

TEST_CASE("large rings", "[smarts]") {
  auto query = "[r24]"_smarts;
  auto m_r24 = "C1CCCCCCCCCCCCCCCCCCCCCCC1"_smiles;
  auto m_r23 = "C1CCCCCCCCCCCCCCCCCCCCCC1"_smiles;

  CHECK(SubstructMatch(*m_r23, *query).empty());
  CHECK(SubstructMatch(*m_r24, *query).size() == 24);
}

TEST_CASE("random smiles vectors", "[smiles]") {
  auto m = "C1OCC1N(CO)(Cc1ccccc1NCCl)"_smiles;
  REQUIRE(m);
  SECTION("basics") {
    std::vector<std::string> tgt = {
        "c1cc(CN(C2COC2)CO)c(cc1)NCCl", "N(CCl)c1c(CN(C2COC2)CO)cccc1",
        "N(CCl)c1ccccc1CN(C1COC1)CO", "OCN(Cc1ccccc1NCCl)C1COC1",
        "C(N(C1COC1)Cc1c(cccc1)NCCl)O"};
    unsigned int randomSeed = 0xf00d;
    auto smiV = MolToRandomSmilesVect(*m, 5, randomSeed);
    CHECK(smiV == tgt);
  }
  SECTION("options1") {
    std::vector<std::string> tgt = {
        "C1-C=C(-C-N(-C2-C-O-C-2)-C-O)-C(=C-C=1)-N-C-Cl",
        "N(-C-Cl)-C1-C(-C-N(-C2-C-O-C-2)-C-O)=C-C=C-C=1",
        "N(-C-Cl)-C1=C-C=C-C=C-1-C-N(-C1-C-O-C-1)-C-O",
        "O-C-N(-C-C1=C-C=C-C=C-1-N-C-Cl)-C1-C-O-C-1",
        "C(-N(-C1-C-O-C-1)-C-C1-C(=C-C=C-C=1)-N-C-Cl)-O"};
    RWMol nm(*m);
    MolOps::Kekulize(nm, true);
    unsigned int randomSeed = 0xf00d;
    bool isomericSmiles = true;
    bool kekuleSmiles = true;
    bool allBondsExplicit = true;
    bool allHsExplicit = false;
    auto smiV =
        MolToRandomSmilesVect(nm, 5, randomSeed, isomericSmiles, kekuleSmiles,
                              allBondsExplicit, allHsExplicit);
    CHECK(smiV == tgt);
  }
  SECTION("options2") {
    std::vector<std::string> tgt = {
        "[cH]1[cH][c]([CH2][N]([CH]2[CH2][O][CH2]2)[CH2][OH])[c]([cH][cH]1)[NH]"
        "[CH2][Cl]",
        "[NH]([CH2][Cl])[c]1[c]([CH2][N]([CH]2[CH2][O][CH2]2)[CH2][OH])[cH][cH]"
        "[cH][cH]1",
        "[NH]([CH2][Cl])[c]1[cH][cH][cH][cH][c]1[CH2][N]([CH]1[CH2][O][CH2]1)["
        "CH2][OH]",
        "[OH][CH2][N]([CH2][c]1[cH][cH][cH][cH][c]1[NH][CH2][Cl])[CH]1[CH2][O]["
        "CH2]1",
        "[CH2]([N]([CH]1[CH2][O][CH2]1)[CH2][c]1[c]([cH][cH][cH][cH]1)[NH][CH2]"
        "[Cl])[OH]"};
    RWMol nm(*m);
    MolOps::Kekulize(nm, false);
    unsigned int randomSeed = 0xf00d;
    bool isomericSmiles = true;
    bool kekuleSmiles = false;
    bool allBondsExplicit = false;
    bool allHsExplicit = true;
    auto smiV =
        MolToRandomSmilesVect(nm, 5, randomSeed, isomericSmiles, kekuleSmiles,
                              allBondsExplicit, allHsExplicit);
    CHECK(smiV == tgt);
  }
}

TEST_CASE(
    "github #3197: Molecule constructed from CXSMILES cannot be translated to "
    "SMARTS",
    "[smarts][bug]") {
  auto m = "C* |$;M_p$|"_smiles;
  REQUIRE(m);
  SECTION("smarts writing") {
    auto smarts = MolToSmarts(*m);
    // this will change if/when the definition of the query changes, just have
    // to update then
    CHECK(
        smarts ==
        "[#6]-[!#0&!#2&!#5&!#6&!#7&!#8&!#9&!#10&!#14&!#15&!#16&!#17&!#18&!#33&!#"
        "34&!#35&!#36&!#52&!#53&!#54&!#85&!#86&!#1]");
  }
  SECTION("serialization") {
    std::string pkl;
    MolPickler::pickleMol(*m, pkl, PicklerOps::PropertyPickleOptions::AllProps);
    ROMol cpy(pkl);
    auto osmi = MolToCXSmiles(*m);
    CHECK(osmi == "*C |$M_p;$|");
    auto smi = MolToCXSmiles(cpy);
    CHECK(smi == osmi);
    QueryAtom *oa1 = static_cast<QueryAtom *>(m->getAtomWithIdx(1));
    QueryAtom *a1 = static_cast<QueryAtom *>(m->getAtomWithIdx(1));
    REQUIRE(oa1->hasQuery());
    REQUIRE(a1->hasQuery());
    size_t osz =
        oa1->getQuery()->endChildren() - oa1->getQuery()->beginChildren();
    size_t sz = a1->getQuery()->endChildren() - a1->getQuery()->beginChildren();
    // we don't need to test the exact size (since that may change), but let's
    // at least be sure it's not unreasonable:
    CHECK(osz > 0);
    CHECK(osz < 200);
    CHECK(osz == sz);
  }
}

TEST_CASE("d primitive in SMARTS", "[smarts][extension]") {
  SmilesParserParams ps;
  ps.removeHs = false;
  std::unique_ptr<ROMol> m(SmilesToMol("[H]OCO[2H]", ps));
  REQUIRE(m);
  CHECK(m->getNumAtoms() == 5);
  SECTION("basics") {
    auto q = "[d2]"_smarts;
    REQUIRE(q);
    CHECK(SubstructMatch(*m, *q).size() == 2);
  }
  SECTION("comparison to D") {
    auto q = "[D2]"_smarts;
    REQUIRE(q);
    CHECK(SubstructMatch(*m, *q).size() == 3);
  }
}

TEST_CASE(
    "github #3342: unspecified branch bonds in SMARTS don't have aromaticity "
    "set",
    "[smarts][bug]") {
  SECTION("as reported") {
    auto m = "c1(ccccc1)"_smarts;
    REQUIRE(m);
    REQUIRE(m->getBondBetweenAtoms(0, 1));
    CHECK(m->getBondBetweenAtoms(0, 1)->getBondType() ==
          Bond::BondType::AROMATIC);
    CHECK(m->getBondBetweenAtoms(0, 1)->getIsAromatic());
  }
}

TEST_CASE("github #3320: incorrect bond properties from CXSMILES",
          "[cxsmiles][bug]") {
  SECTION("as reported") {
    auto m = "[Cl-][Pt++]1([Cl-])NCCN1C1CCCCC1 |C:6.6,3.2,0.0,2.1|"_smiles;
    REQUIRE(m);
    std::vector<std::pair<unsigned, unsigned>> bonds = {
        {0, 1}, {3, 1}, {2, 1}, {6, 1}};
    for (const auto &pr : bonds) {
      auto bnd = m->getBondBetweenAtoms(pr.first, pr.second);
      REQUIRE(bnd);
      CHECK(bnd->getBondType() == Bond::BondType::DATIVE);
      CHECK(bnd->getBeginAtomIdx() == pr.first);
    }
  }
  SECTION("as reported") {
    auto m = "[Cl-][Pt++]1([Cl-])NCC3C2CCCCC2.N13 |C:12.12,3.2,0.0,2.1|"_smiles;
    REQUIRE(m);
    std::vector<std::pair<unsigned, unsigned>> bonds = {
        {0, 1}, {3, 1}, {2, 1}, {12, 1}};
    for (const auto &pr : bonds) {
      auto bnd = m->getBondBetweenAtoms(pr.first, pr.second);
      REQUIRE(bnd);
      CHECK(bnd->getBondType() == Bond::BondType::DATIVE);
      CHECK(bnd->getBeginAtomIdx() == pr.first);
    }
  }
}

TEST_CASE("github #3774: MolToSmarts inverts direction of dative bond",
          "[smarts][bug]") {
  SECTION("as reported") {
    {
      auto m = "N->[Cu+]"_smiles;
      REQUIRE(m);
      CHECK(MolToSmarts(*m) == "[#7]->[Cu+]");
      CHECK(MolToSmiles(*m) == "N->[Cu+]");
    }
    {
      auto m = "N<-[Cu+]"_smiles;
      REQUIRE(m);
      CHECK(MolToSmarts(*m) == "[#7]<-[Cu+]");
      CHECK(MolToSmiles(*m) == "N<-[Cu+]");
    }
  }
  SECTION("from smarts") {
    {
      auto m = "N->[Cu+]"_smarts;
      REQUIRE(m);
      CHECK(MolToSmarts(*m) == "N->[#29&+]");
    }
    {
      auto m = "N<-[Cu+]"_smarts;
      REQUIRE(m);
      CHECK(MolToSmarts(*m) == "N<-[#29&+]");
    }
  }
}

TEST_CASE("Hydrogen bonds", "[smiles]") {
  SECTION("basics") {
    auto m = "CC1O[H]O=C(C)C1 |H:4.3|"_smiles;
    REQUIRE(m);
    REQUIRE(m->getBondBetweenAtoms(3, 4));
    CHECK(m->getBondBetweenAtoms(3, 4)->getBondType() ==
          Bond::BondType::HYDROGEN);
  }
}

TEST_CASE("Github #2788: doKekule=true should kekulize the molecule",
          "[smiles]") {
  SECTION("basics1") {
    auto m = "c1ccccc1"_smiles;
    REQUIRE(m);
    bool doIsomeric = true;
    bool doKekule = true;
    CHECK(MolToSmiles(*m, doIsomeric, doKekule) == "C1=CC=CC=C1");
  }
  SECTION("basics2") {
    auto m = "c1cc[nH]c1"_smiles;
    REQUIRE(m);
    bool doIsomeric = true;
    bool doKekule = true;
    CHECK(MolToSmiles(*m, doIsomeric, doKekule) == "C1=CNC=C1");
  }

  SECTION("can thrown exceptions") {
    int debugParse = 0;
    bool sanitize = false;
    std::unique_ptr<RWMol> m{SmilesToMol("c1ccnc1", debugParse, sanitize)};
    REQUIRE(m);
    bool doIsomeric = true;
    bool doKekule = false;
    {
      RWMol tm(*m);
      CHECK(MolToSmiles(tm, doIsomeric, doKekule) == "c1ccnc1");
    }
    doKekule = true;
    {
      RWMol tm(*m);
      CHECK_THROWS_AS(MolToSmiles(tm, doIsomeric, doKekule), KekulizeException);
    }
  }
}

TEST_CASE("bogus recursive SMARTS", "[smarts]") {
  std::string sma = "C)foo";
  CHECK(SmartsToMol(sma) == nullptr);
}

TEST_CASE(
    "Github #3998 MolFragmentToSmiles failing in Kekulization with "
    "kekuleSmiles=true") {
  auto mol = "Cc1ccccc1"_smiles;
  REQUIRE(mol);
  SECTION("normal") {
    std::vector<int> ats{0};
    std::string smi = MolFragmentToSmiles(*mol, ats);
    CHECK(smi == "C");
  }
  SECTION("kekulized") {
    std::vector<int> ats{0};
    bool doIsomericSmiles = true;
    bool doKekule = true;
    std::string smi = MolFragmentToSmiles(*mol, ats, nullptr, nullptr, nullptr,
                                          doIsomericSmiles, doKekule);
    CHECK(smi == "C");
  }
  SECTION("including ring parts") {
    std::vector<int> ats{0, 1, 2};
    bool doIsomericSmiles = true;
    bool doKekule = true;
    std::string smi = MolFragmentToSmiles(*mol, ats, nullptr, nullptr, nullptr,
                                          doIsomericSmiles, doKekule);
    CHECK(smi == "C:CC");
  }
}

TEST_CASE("Github #4319 add CXSMARTS support") {
  // note: the CXSMARTS support uses exactly the same code as the CXSMILES
  // parser/writer. We aren't testing that here since it's tested already with
  // the CXSMILES tests. The goal here is just to make sure that it's being
  // called by default and that we can control its behavior with the
  // SmartsParseParams structure
  SECTION("defaults") {
    auto mol = "CCC |$foo;;bar$|"_smarts;
    REQUIRE(mol);
    REQUIRE(mol->getNumAtoms() == 3);
    CHECK(mol->getAtomWithIdx(0)->getProp<std::string>(
              common_properties::atomLabel) == "foo");
    CHECK(mol->getAtomWithIdx(2)->getProp<std::string>(
              common_properties::atomLabel) == "bar");
    CHECK(!mol->getAtomWithIdx(1)->hasProp(common_properties::atomLabel));
  }
  SECTION("params") {
    std::string sma = "CCC |$foo;;bar$|";
    SmartsParserParams ps;
    const std::unique_ptr<RWMol> mol(SmartsToMol(sma, ps));
    REQUIRE(mol);
    REQUIRE(mol->getNumAtoms() == 3);
    CHECK(mol->getAtomWithIdx(0)->getProp<std::string>(
              common_properties::atomLabel) == "foo");
    CHECK(mol->getAtomWithIdx(2)->getProp<std::string>(
              common_properties::atomLabel) == "bar");
    CHECK(!mol->getAtomWithIdx(1)->hasProp(common_properties::atomLabel));
  }
  SECTION("no cxsmarts") {
    std::string sma = "CCC |$foo;;bar$|";
    SmartsParserParams ps;
    ps.allowCXSMILES = false;
    ps.parseName = false;
    const std::unique_ptr<RWMol> mol(SmartsToMol(sma, ps));
    REQUIRE(!mol);
  }
  SECTION("name") {
    std::string sma = "CCC foobar";
    SmartsParserParams ps;
    ps.parseName = true;
    const std::unique_ptr<RWMol> mol(SmartsToMol(sma, ps));
    REQUIRE(mol);
    REQUIRE(mol->getProp<std::string>(common_properties::_Name) == "foobar");
  }
  SECTION("writer") {
    auto mol = "CCC |$foo;;bar$|"_smarts;
    REQUIRE(mol);
    REQUIRE(mol->getNumAtoms() == 3);
    CHECK(MolToSmarts(*mol) == "CCC");
    CHECK(MolToCXSmarts(*mol) == "CCC |$foo;;bar$|");
  }
  SECTION("writer, check reordering") {
    auto mol = "CC1.OC1 |$foo;;;bar$|"_smarts;
    REQUIRE(mol);
    REQUIRE(mol->getNumAtoms() == 4);
    CHECK(MolToSmarts(*mol) == "CCCO");
    CHECK(MolToCXSmarts(*mol) == "CCCO |$foo;;bar;$|");
  }

  SECTION("parser, confirm enhanced stereo working") {
    auto mol = "[#6][C@]([#8])(F)Cl |&1:1|"_smarts;
    REQUIRE(mol);
    REQUIRE(mol->getNumAtoms() == 5);
    CHECK(MolToSmarts(*mol) == "[#6][C@]([#8])(F)Cl");
    CHECK(MolToCXSmarts(*mol) == "[#6][C@]([#8])(F)Cl |&1:1|");

    {
      auto smol = "C[C@](O)(F)Cl |&1:1|"_smiles;
      REQUIRE(smol);
      SubstructMatchParameters sssparams;
      sssparams.useEnhancedStereo = true;
      sssparams.useChirality = true;
      CHECK(SubstructMatch(*smol, *mol, sssparams).size() == 1);
    }
    {
      auto smol = "C[C@](O)(F)Cl |o1:1|"_smiles;
      REQUIRE(smol);
      SubstructMatchParameters sssparams;
      sssparams.useEnhancedStereo = true;
      sssparams.useChirality = true;
      CHECK(SubstructMatch(*smol, *mol, sssparams).empty());
    }
  }

  SECTION("CXSMARTS parsing bug") {
    {  // no cxsmarts
      auto mol = "C[C@H]([F,Cl,Br])[C@H](C)[C@@H](C)Br"_smarts;
      REQUIRE(mol);
      CHECK(mol->getAtomWithIdx(2)->getQuery()->getDescription() == "AtomOr");
      CHECK(MolToSmarts(*mol) == "C[C@&H1]([F,Cl,Br])[C@&H1](C)[C@@&H1](C)Br");
      CHECK(MolToCXSmarts(*mol) ==
            "C[C@&H1]([F,Cl,Br])[C@&H1](C)[C@@&H1](C)Br");
    }
    {  // make sure that doesn't break anything
      auto mol = "C[C@H]([F,Cl,Br])[C@H](C)[C@@H](C)Br |a:1,o1:4,5|"_smarts;
      REQUIRE(mol);
      CHECK(mol->getAtomWithIdx(2)->getQuery()->getDescription() == "AtomOr");
      CHECK(MolToSmarts(*mol) == "C[C@&H1]([F,Cl,Br])[C@&H1](C)[C@@&H1](C)Br");
      CHECK(MolToCXSmarts(*mol) ==
            "C[C@&H1]([F,Cl,Br])[C@&H1](C)[C@@&H1](C)Br |a:1,o1:4,5|");
    }
  }
}

TEST_CASE("Github #4233: data groups in CXSMILES neither parsed nor written") {
  SECTION("basics") {
    {
      auto mol = "C/C=C/C |SgD:2,1:FIELD:info::::|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 1);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{2, 1});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "DAT");
      CHECK(sgs[0].getProp<std::string>("FIELDNAME") == "FIELD");
      CHECK(sgs[0].getProp<std::vector<std::string>>("DATAFIELDS") ==
            std::vector<std::string>{"info"});
      CHECK(MolToCXSmiles(*mol) == "C/C=C/C |SgD:2,1:FIELD:info::::|");
    }
    {
      auto mol = "C/C=C/C |SgD:2,1:FIELD:foo:like:info:tag:|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 1);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{2, 1});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "DAT");
      CHECK(sgs[0].getProp<std::string>("FIELDNAME") == "FIELD");
      CHECK(sgs[0].getProp<std::vector<std::string>>("DATAFIELDS") ==
            std::vector<std::string>{"foo"});
      CHECK(sgs[0].getProp<std::string>("QUERYOP") == "like");
      CHECK(sgs[0].getProp<std::string>("FIELDINFO") == "info");
      CHECK(sgs[0].getProp<std::string>("FIELDTAG") == "tag");
      CHECK(MolToCXSmiles(*mol) ==
            "C/C=C/C |SgD:2,1:FIELD:foo:like:info:tag:|");
    }
    {  // data on a dummy atom
      auto mol = "CC-* |$;;star_e$,SgD:2:querydata:val::::|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 1);
      CHECK(sgs[0].getAtoms().size() == 1);
      CHECK(sgs[0].getAtoms()[0] == 2);
      CHECK(sgs[0].getProp<std::string>("TYPE") == "DAT");
      CHECK(sgs[0].getProp<std::vector<std::string>>("DATAFIELDS") ==
            std::vector<std::string>{"val"});
      CHECK(MolToCXSmiles(*mol) == "*CC |$star_e;;$,SgD:0:querydata:val::::|");
    }
    {
      auto mol =
          "C1=CC=C(C=C1)C1=CC=CC=C1 |c:0,2,4,9,11,t:7,SgD:8,9,11,10,7,6:PieceName:Ring1::::,SgD:1,2,3,4,5,0:PieceName:Ring2::::|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 2);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{8, 9, 11, 10, 7, 6});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "DAT");
      CHECK(sgs[0].getProp<std::string>("FIELDNAME") == "PieceName");
      CHECK(sgs[0].getProp<std::vector<std::string>>("DATAFIELDS") ==
            std::vector<std::string>{"Ring1"});
      CHECK(sgs[1].getAtoms() == std::vector<unsigned int>{1, 2, 3, 4, 5, 0});
      CHECK(sgs[1].getProp<std::string>("TYPE") == "DAT");
      CHECK(sgs[1].getProp<std::string>("FIELDNAME") == "PieceName");
      CHECK(sgs[1].getProp<std::vector<std::string>>("DATAFIELDS") ==
            std::vector<std::string>{"Ring2"});

      CHECK(MolToCXSmiles(*mol) ==
            "c1ccc(-c2ccccc2)cc1 "
            "|SgD:6,7,9,8,5,4:PieceName:Ring1::::,SgD:1,2,3,10,11,0:PieceName:"
            "Ring2::::|");

      // make sure we can round-trip that result through mol blocks
      auto mb = MolToV3KMolBlock(*mol);
      std::unique_ptr<RWMol> mol2(MolBlockToMol(mb));
      REQUIRE(mol2);
      const auto &sgs2 = getSubstanceGroups(*mol2);
      REQUIRE(sgs2.size() == 2);
      CHECK(sgs2[0].getAtoms() ==
            std::vector<unsigned int>{8, 9, 11, 10, 7, 6});
      CHECK(sgs2[0].getProp<std::string>("TYPE") == "DAT");
      CHECK(sgs2[0].getProp<std::string>("FIELDNAME") == "PieceName");
      CHECK(sgs2[0].getProp<std::vector<std::string>>("DATAFIELDS") ==
            std::vector<std::string>{"Ring1"});
      CHECK(sgs2[1].getAtoms() == std::vector<unsigned int>{1, 2, 3, 4, 5, 0});
      CHECK(sgs2[1].getProp<std::string>("TYPE") == "DAT");
      CHECK(sgs2[1].getProp<std::string>("FIELDNAME") == "PieceName");
      CHECK(sgs2[1].getProp<std::vector<std::string>>("DATAFIELDS") ==
            std::vector<std::string>{"Ring2"});
    }
  }
}

TEST_CASE("polymer SGroups") {
  SECTION("basics") {
    {
      auto mol =
          "*CC(*)C(*)N* |$star_e;;;star_e;;star_e;;star_e$,Sg:n:6,1,2,4::ht:6,0,:4,2,|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 1);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{6, 1, 2, 4});
      CHECK(sgs[0].getBonds() == std::vector<unsigned int>{6, 0, 4, 2});
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBHEAD") ==
            std::vector<unsigned int>{6, 0});
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBCORR") ==
            std::vector<unsigned int>{6, 4, 0, 2});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "SRU");
      CHECK(sgs[0].getProp<std::string>("CONNECT") == "HT");
      CHECK(sgs[0].getProp<unsigned int>("index") == 1);

      auto smi = MolToCXSmiles(*mol);
      CHECK(smi ==
            "*CC(*)C(*)N* "
            "|$star_e;;;star_e;;star_e;;star_e$,Sg:n:6,1,2,4::ht:6,0:4,2:|");
      // auto mb = MolToV3KMolBlock(*mol);
      // std::cerr << mb << std::endl;
    }
    {
      auto mol =
          "*CC(*)C(*)N* |$star_e;;;star_e;;star_e;;star_e$,Sg:n:6,1,2,4::hh&#44;f:6,0,:4,2,|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 1);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{6, 1, 2, 4});
      CHECK(sgs[0].getBonds() == std::vector<unsigned int>{6, 0, 4, 2});
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBHEAD") ==
            std::vector<unsigned int>{6, 0});
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBCORR") ==
            std::vector<unsigned int>{6, 2, 0, 4});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "SRU");
      CHECK(sgs[0].getProp<std::string>("CONNECT") == "HH");
      CHECK(sgs[0].getProp<unsigned int>("index") == 1);

      auto smi = MolToCXSmiles(*mol);
      CHECK(smi ==
            "*CC(*)C(*)N* "
            "|$star_e;;;star_e;;star_e;;star_e$,Sg:n:6,1,2,4::hh:6,0:2,4:|");
    }
    {  // minimal
      auto mol = "*-CCO-* |$star_e;;;;star_e$,Sg:n:1,2,3|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 1);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{1, 2, 3});
      CHECK(sgs[0].getBonds() == std::vector<unsigned int>{0, 3});
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBCORR") ==
            sgs[0].getBonds());
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBHEAD") ==
            std::vector<unsigned int>{0});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "SRU");
      CHECK(sgs[0].getProp<std::string>("CONNECT") == "EU");
      CHECK(sgs[0].getProp<unsigned int>("index") == 1);

      auto smi = MolToCXSmiles(*mol);
      CHECK(smi == "*CCO* |$star_e;;;;star_e$,Sg:n:1,2,3::eu:::|");
    }
    {  // single atom
      auto mol = "*-C-* |$star_e;;star_e$,Sg:n:1::ht|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 1);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{1});
      CHECK(sgs[0].getBonds() == std::vector<unsigned int>{0, 1});
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBCORR") ==
            sgs[0].getBonds());
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBHEAD") ==
            std::vector<unsigned int>{0});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "SRU");
      CHECK(sgs[0].getProp<std::string>("CONNECT") == "HT");
      CHECK(sgs[0].getProp<unsigned int>("index") == 1);

      auto smi = MolToCXSmiles(*mol);
      CHECK(smi == "*C* |$star_e;;star_e$,Sg:n:1::ht:::|");
    }
  }
  SECTION("multiple s groups") {
    {
      auto mol =
          "*-NCCO-* |$star_e;;;;;star_e$,Sg:n:1,2::ht,Sg:any:3,4::hh|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 2);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{1, 2});
      CHECK(sgs[0].getBonds() == std::vector<unsigned int>{0, 2});
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBCORR") ==
            sgs[0].getBonds());
      CHECK(sgs[0].getProp<std::vector<unsigned int>>("XBHEAD") ==
            std::vector<unsigned int>{0});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "SRU");
      CHECK(sgs[0].getProp<std::string>("CONNECT") == "HT");
      CHECK(sgs[0].getProp<unsigned int>("index") == 1);

      CHECK(sgs[1].getAtoms() == std::vector<unsigned int>{3, 4});
      CHECK(sgs[1].getBonds() == std::vector<unsigned int>{2, 4});
      CHECK(sgs[1].getProp<std::vector<unsigned int>>("XBCORR") ==
            sgs[1].getBonds());
      CHECK(sgs[1].getProp<std::vector<unsigned int>>("XBHEAD") ==
            std::vector<unsigned int>{2});
      CHECK(sgs[1].getProp<std::string>("TYPE") == "ANY");
      CHECK(sgs[1].getProp<std::string>("CONNECT") == "HH");
      CHECK(sgs[1].getProp<unsigned int>("index") == 2);

      auto smi = MolToCXSmiles(*mol);
      CHECK(smi ==
            "*NCCO* |$star_e;;;;;star_e$,,,Sg:n:1,2::ht:::,Sg:any:3,4::hh:::|");
    }

    {  // multiple s groups + data
      auto mol =
          "CCNCCO-* "
          "|$;;;;;;star_e$,SgD:1:atomdata:val::::,Sg:n:2,3::ht,Sg:any:4,5::hh|"_smiles;
      REQUIRE(mol);
      const auto &sgs = getSubstanceGroups(*mol);
      REQUIRE(sgs.size() == 3);
      CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{1});
      CHECK(sgs[0].getProp<std::string>("TYPE") == "DAT");
      CHECK(sgs[0].getProp<std::string>("FIELDNAME") == "atomdata");
      CHECK(sgs[0].getProp<std::vector<std::string>>("DATAFIELDS") ==
            std::vector<std::string>{"val"});
      CHECK(sgs[0].getProp<unsigned int>("index") == 1);

      CHECK(sgs[1].getAtoms() == std::vector<unsigned int>{2, 3});
      CHECK(sgs[1].getBonds() == std::vector<unsigned int>{1, 3});
      CHECK(sgs[1].getProp<std::vector<unsigned int>>("XBCORR") ==
            sgs[1].getBonds());
      CHECK(sgs[1].getProp<std::vector<unsigned int>>("XBHEAD") ==
            std::vector<unsigned int>{1});
      CHECK(sgs[1].getProp<std::string>("TYPE") == "SRU");
      CHECK(sgs[1].getProp<std::string>("CONNECT") == "HT");
      CHECK(sgs[1].getProp<unsigned int>("index") == 2);

      CHECK(sgs[2].getAtoms() == std::vector<unsigned int>{4, 5});
      CHECK(sgs[2].getBonds() == std::vector<unsigned int>{3, 5});
      CHECK(sgs[2].getProp<std::vector<unsigned int>>("XBCORR") ==
            sgs[2].getBonds());
      CHECK(sgs[2].getProp<std::vector<unsigned int>>("XBHEAD") ==
            std::vector<unsigned int>{3});
      CHECK(sgs[2].getProp<std::string>("TYPE") == "ANY");
      CHECK(sgs[2].getProp<std::string>("CONNECT") == "HH");
      CHECK(sgs[2].getProp<unsigned int>("index") == 3);

      auto smi = MolToCXSmiles(*mol);
      CHECK(smi ==
            "*OCCNCC "
            "|$star_e;;;;;;$,SgD:5:atomdata:val::::,,,,Sg:n:4,3::ht:::,Sg:any:"
            "2,1::hh:::|");
    }
  }
}

TEST_CASE("SGroup hierarchy") {
  SECTION("basics") {
    auto mol =
        "*-CNC(C-*)O-* "
        "|$star_e;;;;;star_e;;star_e$,Sg:any:2,1::ht,Sg:any:4,3,2,1,0,6::ht,"
        "SgH:1:0|"_smiles;
    REQUIRE(mol);
    const auto &sgs = getSubstanceGroups(*mol);
    REQUIRE(sgs.size() == 2);
    CHECK(sgs[0].getAtoms() == std::vector<unsigned int>{2, 1});
    CHECK(sgs[0].getProp<std::string>("TYPE") == "ANY");
    CHECK(sgs[0].getProp<unsigned int>("PARENT") == 2);
    CHECK(sgs[0].getProp<unsigned int>("index") == 1);
    CHECK(sgs[1].getAtoms() == std::vector<unsigned int>{4, 3, 2, 1, 0, 6});
    CHECK(sgs[1].getProp<std::string>("TYPE") == "ANY");
    CHECK(sgs[1].getProp<unsigned int>("index") == 2);
    CHECK(!sgs[1].hasProp("PARENT"));
    CHECK(MolToCXSmiles(*mol) ==
          "*CNC(C*)O* "
          "|$star_e;;;;;star_e;;star_e$,,,Sg:any:2,1::ht:::,Sg:any:4,3,2,1,0,6:"
          ":ht:::,SgH:1:0|");
  }
  SECTION("nested") {
    auto mol =
        "*-CNC(CC(-*)C-*)O-* "
        "|$star_e;;;;;;star_e;;star_e;;star_e$,"
        "SgD:4:internal data:val::::,SgD:7:atom value:value2::::,"
        "Sg:n:7::ht,Sg:n:2::ht,Sg:any:5,7,8,4,3,2,1,0,9::ht,"
        "SgH:4:2.3.0,2:1|"_smiles;
    REQUIRE(mol);
    const auto &sgs = getSubstanceGroups(*mol);
    REQUIRE(sgs.size() == 5);
    CHECK(sgs[0].getProp<unsigned int>("PARENT") == 5);
    CHECK(sgs[2].getProp<unsigned int>("PARENT") == 5);
    CHECK(sgs[3].getProp<unsigned int>("PARENT") == 5);
    CHECK(sgs[1].getProp<unsigned int>("PARENT") == 3);
    CHECK(
        MolToCXSmiles(*mol) ==
        "*CNC(CC(*)C*)O* |$star_e;;;;;;star_e;;star_e;;star_e$,SgD:4:internal "
        "data:val::::,SgD:7:atom "
        "value:value2::::,,,,,,Sg:n:7::ht:::,Sg:n:2::ht:::,Sg:any:5,7,8,4,3,2,"
        "1,0,9::ht:::,SgH:2:1,4:0.2.3|");
  }
}

TEST_CASE("Linknode writing") {
  SECTION("single") {
    auto mol = "OC1CCC(F)C1 |LN:1:1.3.2.6|"_smiles;
    REQUIRE(mol);
    std::string lns;
    CHECK(mol->getPropIfPresent(common_properties::molFileLinkNodes, lns));
    CHECK(lns == "1 3 2 2 3 2 7");
    CHECK(MolToCXSmiles(*mol) == "OC1CCC(F)C1 |LN:1:1.3.2.6|");
  }
  SECTION("multiple") {
    auto mol = "FC1CCC(O)C1 |LN:1:1.3.2.6,4:1.4.3.6|"_smiles;
    REQUIRE(mol);
    std::string lns;
    CHECK(mol->getPropIfPresent(common_properties::molFileLinkNodes, lns));
    CHECK(lns == "1 3 2 2 3 2 7|1 4 2 5 4 5 7");
    CHECK(MolToCXSmiles(*mol) == "OC1CCC(F)C1 |LN:4:1.3.3.6,1:1.4.2.6|");
  }
  SECTION("two-coordinate") {
    auto mol = "C1OCCC1C |LN:0:1.5,1:1.3|"_smiles;
    REQUIRE(mol);
    CHECK(MolToCXSmiles(*mol) == "CC1CCOC1 |LN:5:1.5,4:1.3|");
  }
}

TEST_CASE("smilesBondOutputOrder") {
  SECTION("basics") {
    auto m = "OCCN.CCO"_smiles;
    REQUIRE(m);
    CHECK(MolToSmiles(*m) == "CCO.NCCO");
    std::vector<unsigned int> order;
    m->getProp(common_properties::_smilesAtomOutputOrder, order);
    CHECK(order == std::vector<unsigned int>{4, 5, 6, 3, 2, 1, 0});
    m->getProp(common_properties::_smilesBondOutputOrder, order);
    CHECK(order == std::vector<unsigned int>{3, 4, 2, 1, 0});
  }
  SECTION("Github #5585: incorrect ordering for ring closures") {
    auto m = "OC1CCCN1"_smiles;
    REQUIRE(m);
    CHECK(MolToSmiles(*m) == "OC1CCCN1");
    std::vector<unsigned int> order;
    m->getProp(common_properties::_smilesAtomOutputOrder, order);
    CHECK(order == std::vector<unsigned int>{0, 1, 2, 3, 4, 5});
    m->getProp(common_properties::_smilesBondOutputOrder, order);
    CHECK(order == std::vector<unsigned int>{0, 1, 2, 3, 4, 5});
  }
}

TEST_CASE("Github #4320: Support toggling components of CXSMILES output") {
  SECTION("sgroups") {
    auto mol =
        "*-CNC(CC(-*)C-*)O-* "
        "|$star_e;;;;;;star_e;;star_e;;star_e$,"
        "SgD:4:internal data:val::::,SgD:7:atom value:value2::::,"
        "Sg:n:7::ht,Sg:n:2::ht,Sg:any:5,7,8,4,3,2,1,0,9::ht,"
        "SgH:4:2.3.0,2:1|"_smiles;
    SmilesWriteParams ps;
    {
      auto cxsmi = MolToCXSmiles(*mol, ps, SmilesWrite::CXSmilesFields::CX_ALL);
      CHECK(cxsmi ==
            "*CNC(CC(*)C*)O* "
            "|$star_e;;;;;;star_e;;star_e;;star_e$,SgD:4:internal "
            "data:val::::,SgD:7:atom "
            "value:value2::::,,,,,,Sg:n:7::ht:::,Sg:n:2::ht:::,Sg:any:5,7,8,4,"
            "3,2,"
            "1,0,9::ht:::,SgH:2:1,4:0.2.3|");
      CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
    }
    {
      auto cxsmi =
          MolToCXSmiles(*mol, ps, SmilesWrite::CXSmilesFields::CX_NONE);
      CHECK(cxsmi == "*CNC(CC(*)C*)O*");
      CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
    }
    {
      auto cxsmi =
          MolToCXSmiles(*mol, ps,
                        SmilesWrite::CXSmilesFields::CX_ALL ^
                            SmilesWrite::CXSmilesFields::CX_ATOM_LABELS);
      CHECK(
          cxsmi ==
          "*CNC(CC(*)C*)O* |SgD:4:internal data:val::::,SgD:7:atom "
          "value:value2::::,,,,,,Sg:n:7::ht:::,Sg:n:2::ht:::,Sg:any:5,7,8,4,3,"
          "2,1,0,9::ht:::,SgH:2:1,4:0.2.3|");
      CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
    }
    {
      auto cxsmi = MolToCXSmiles(*mol, ps,
                                 SmilesWrite::CXSmilesFields::CX_ALL ^
                                     SmilesWrite::CXSmilesFields::CX_SGROUPS);
      CHECK(cxsmi ==
            "*CNC(CC(*)C*)O* "
            "|$star_e;;;;;;star_e;;star_e;;star_e$,,,Sg:n:7::ht:::,Sg:n:2::ht::"
            ":,Sg:any:5,7,8,4,3,2,1,0,9::ht:::,SgH:2:0.1|");
      CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
    }
    {
      auto cxsmi = MolToCXSmiles(*mol, ps,
                                 SmilesWrite::CXSmilesFields::CX_ALL ^
                                     SmilesWrite::CXSmilesFields::CX_POLYMER);
      CHECK(cxsmi ==
            "*CNC(CC(*)C*)O* "
            "|$star_e;;;;;;star_e;;star_e;;star_e$,SgD:4:internal "
            "data:val::::,SgD:7:atom value:value2::::,,,|");
      CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
    }
  }
  SECTION("coordinates") {
    auto m = "CC |(0,.75,;0,-.75,)|"_smiles;
    REQUIRE(m);
    SmilesWriteParams ps;
    auto cxsmi = MolToCXSmiles(*m, ps,
                               SmilesWrite::CXSmilesFields::CX_ALL ^
                                   SmilesWrite::CXSmilesFields::CX_COORDS);
    CHECK(cxsmi == "CC");
    CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
  }
  SECTION("enhanced stereo") {
    auto m = "C[C@H](F)Cl |o1:1|"_smiles;
    REQUIRE(m);
    SmilesWriteParams ps;
    auto cxsmi =
        MolToCXSmiles(*m, ps,
                      SmilesWrite::CXSmilesFields::CX_ALL ^
                          SmilesWrite::CXSmilesFields::CX_ENHANCEDSTEREO);
    CHECK(cxsmi == "C[C@H](F)Cl");
    CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
  }
  SECTION("link nodes") {
    auto m = "OC1CCC(F)C1 |LN:1:1.3.2.6|"_smiles;
    REQUIRE(m);
    SmilesWriteParams ps;
    auto cxsmi = MolToCXSmiles(*m, ps,
                               SmilesWrite::CXSmilesFields::CX_ALL ^
                                   SmilesWrite::CXSmilesFields::CX_LINKNODES);
    CHECK(cxsmi == "OC1CCC(F)C1");
    CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
  }
  SECTION("radicals") {
    auto m = "[O][C][O] |^1:0,2|"_smiles;
    REQUIRE(m);
    SmilesWriteParams ps;
    auto cxsmi = MolToCXSmiles(*m, ps,
                               SmilesWrite::CXSmilesFields::CX_ALL ^
                                   SmilesWrite::CXSmilesFields::CX_RADICALS);
    CHECK(cxsmi == "[O][C][O]");
    CHECK(std::unique_ptr<ROMol>(SmilesToMol(cxsmi)));
  }
  SECTION("values") {
    auto m = "COCC |$_AV:;bar;;foo$|"_smiles;
    REQUIRE(m);
    SmilesWriteParams ps;
    auto cxsmi =
        MolToCXSmiles(*m, ps,
                      SmilesWrite::CXSmilesFields::CX_ALL ^
                          SmilesWrite::CXSmilesFields::CX_MOLFILE_VALUES);
    CHECK(cxsmi == "CCOC");
  }
}

TEST_CASE("non-tetrahedral chirality") {
  SECTION("allowed values") {
    {
      auto m = "C[Fe@SP3](Cl)(F)N"_smiles;
      REQUIRE(m);
      CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
            Atom::ChiralType::CHI_SQUAREPLANAR);
      CHECK(m->getAtomWithIdx(1)->getProp<unsigned int>(
                common_properties::_chiralPermutation) == 3);
    }
    {
      auto m = "C[Fe@SP3](Cl)(F)N"_smiles;
      REQUIRE(m);
      CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
            Atom::ChiralType::CHI_SQUAREPLANAR);
      CHECK(m->getAtomWithIdx(1)->getProp<unsigned int>(
                common_properties::_chiralPermutation) == 3);
    }
  }
}
TEST_CASE(
    "Github #4503: MolFromSmiles and MolFromSmarts incorrectly accepting input "
    "with spaces") {
  SECTION("SMILES defaults") {
    std::unique_ptr<RWMol> m{SmilesToMol("NON sense extra")};
    CHECK(m);
    CHECK(m->hasProp("_Name"));
    CHECK(m->getProp<std::string>("_Name") == "sense extra");

    CHECK_THROWS_AS(SmilesToMol("NON |sense|"), SmilesParseException);
  }
  SECTION("SMARTS defaults") {
    std::unique_ptr<RWMol> m{SmilesToMol("NON sense extra")};
    CHECK(m);
    CHECK(m->hasProp("_Name"));
    CHECK(m->getProp<std::string>("_Name") == "sense extra");

    CHECK_THROWS_AS(SmartsToMol("NON |sense|"), SmilesParseException);
  }
  SECTION("SMILES no names") {
    SmilesParserParams ps;
    ps.parseName = false;
    CHECK_THROWS_AS(SmilesToMol("NON sense", ps), SmilesParseException);
  }
  SECTION("SMARTS no names") {
    SmartsParserParams ps;
    ps.parseName = false;
    CHECK_THROWS_AS(SmartsToMol("NON sense", ps), SmilesParseException);
  }
  SECTION("SMILES names without parsing CXSMILES") {
    SmilesParserParams ps;
    ps.allowCXSMILES = false;
    {
      std::unique_ptr<RWMol> m{SmilesToMol("NON sense extra", ps)};
      CHECK(m);
      CHECK(m->hasProp("_Name"));
      CHECK(m->getProp<std::string>("_Name") == "sense extra");
    }
    {
      std::unique_ptr<RWMol> m{SmilesToMol("NON |$N1;O2;N3$| sense extra", ps)};
      CHECK(m);
      CHECK(m->hasProp("_Name"));
      CHECK(m->getProp<std::string>("_Name") == "|$N1;O2;N3$| sense extra");
    }
  }

  SECTION("SMILES not strict") {
    SmilesParserParams ps;
    ps.strictCXSMILES = false;
    {
      std::unique_ptr<RWMol> m{SmilesToMol("NON sense", ps)};
      CHECK(m);
      CHECK(m->hasProp("_Name"));
    }
    {
      std::unique_ptr<RWMol> m{SmilesToMol("NON |sense|", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
    ps.parseName = false;
    {
      std::unique_ptr<RWMol> m{SmilesToMol("NON sense", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
    {
      std::unique_ptr<RWMol> m{SmilesToMol("NON |sense|", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
  }
  SECTION("SMARTS not strict") {
    SmartsParserParams ps;
    ps.strictCXSMILES = false;
    {
      std::unique_ptr<RWMol> m{SmartsToMol("NON sense", ps)};
      CHECK(m);
      CHECK(m->hasProp("_Name"));
    }
    {
      std::unique_ptr<RWMol> m{SmartsToMol("NON |sense|", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
    ps.parseName = false;
    {
      std::unique_ptr<RWMol> m{SmartsToMol("NON sense", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
    {
      std::unique_ptr<RWMol> m{SmartsToMol("NON |sense|", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
  }
  SECTION("SMARTS CXExtensions + names") {
    SmartsParserParams ps;
    ps.strictCXSMILES = false;
    {  // CXSMILES + name
      std::unique_ptr<RWMol> m{SmartsToMol("NON |$_AV:bar;;foo$| name", ps)};
      CHECK(m);
      CHECK(m->hasProp("_Name"));
      CHECK(m->getProp<std::string>("_Name") == "name");
    }
    {  // CXSMILES fails, so we don't read the name
      std::unique_ptr<RWMol> m{SmartsToMol("NON |sense| name", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
    ps.parseName = false;
    {  // CXSMILES, skip the name
      std::unique_ptr<RWMol> m{SmartsToMol("NON |$_AV:bar;;foo$| name", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
    ps.parseName = true;
    ps.allowCXSMILES = false;
    ps.strictCXSMILES = true;
    {  // CXSMILES + name, but not parsing the CXSMILES, so we read it as the
       // name
      std::unique_ptr<RWMol> m{SmartsToMol("NON |$_AV:bar;;foo$| name", ps)};
      CHECK(m);
      CHECK(m->hasProp("_Name"));
      CHECK(m->getProp<std::string>("_Name") == "|$_AV:bar;;foo$| name");
    }
  }
  SECTION("SMILES CXExtensions + names") {
    SmilesParserParams ps;
    ps.strictCXSMILES = false;
    {  // CXSMILES + name
      std::unique_ptr<RWMol> m{SmilesToMol("NON |$_AV:bar;;foo$| name", ps)};
      CHECK(m->hasProp("_Name"));
      CHECK(m->getProp<std::string>("_Name") == "name");
    }
    {  // CXSMILES fails, so we don't read the name
      std::unique_ptr<RWMol> m{SmilesToMol("NON |sense| name", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
    ps.parseName = false;
    {  // CXSMILES, skip the name
      std::unique_ptr<RWMol> m{SmilesToMol("NON |$_AV:bar;;foo$| name", ps)};
      CHECK(m);
      CHECK(!m->hasProp("_Name"));
    }
    ps.parseName = true;
    ps.allowCXSMILES = false;
    ps.strictCXSMILES = true;
    {  // CXSMILES + name, but not parsing the CXSMILES, so we read it as the
       // name
      std::unique_ptr<RWMol> m{SmilesToMol("NON |$_AV:bar;;foo$| name", ps)};
      CHECK(m);
      CHECK(m->hasProp("_Name"));
      CHECK(m->getProp<std::string>("_Name") == "|$_AV:bar;;foo$| name");
    }
  }
}

TEST_CASE("Github #4582: double bonds and ring closures") {
  auto mol = R"CTAB(CHEMBL409450
     RDKit          2D

 22 25  0  0  0  0  0  0  0  0999 V2000
   -1.1669    1.3591    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.6820    0.6916    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.9516    1.1041    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.9516    0.2791    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -2.5647    1.6562    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.3931    2.4631    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.6085    2.7181    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.9954    2.1661    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.1669    0.0242    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.9120   -0.7604    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.3969   -1.4279    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.9120   -2.0953    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
   -0.1274   -1.8404    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.1274   -1.0154    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.5871   -2.2529    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.3016   -1.8404    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.3016   -1.0154    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.5871   -0.6029    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.2219   -1.4279    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    0.1430    0.6916    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    0.5555    1.4061    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    2.0160   -2.2529    0.0000 Br  0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  0
  1  3  2  0
  1  8  1  0
  9  2  1  0
  2 20  2  0
  3  4  1  0
  3  5  1  0
  9  4  1  0
  5  6  2  0
  6  7  1  0
  8  7  2  0
  9 10  2  0
 10 11  1  0
 10 14  1  0
 11 12  1  0
 11 19  2  0
 13 12  1  0
 13 14  2  0
 13 15  1  0
 14 18  1  0
 15 16  2  0
 16 17  1  0
 22 16  1  0
 17 18  2  0
 20 21  1  0
M  END)CTAB"_ctab;
  REQUIRE(mol);
  auto dbond = mol->getBondBetweenAtoms(1, 19);
  REQUIRE(dbond);
  CHECK(dbond->getBondType() == Bond::BondType::DOUBLE);
  CHECK((dbond->getStereo() == Bond::BondStereo::STEREOE ||
         dbond->getStereo() == Bond::BondStereo::STEREOTRANS));
  CHECK(dbond->getStereoAtoms() == std::vector<int>{8, 20});
  auto csmiles = MolToSmiles(*mol);
  CHECK(csmiles == "O=C1Nc2cc(Br)ccc2/C1=C1/Nc2ccccc2/C1=N\\O");

#if 0
  // FIX: this test fails. The SMILES generated for this random permutation
  // has the stereo for one of the double bonds wrong.
  SECTION("specific random output") {
    SmilesWriteParams ps;
    ps.doRandom = true;
    getRandomGenerator(51)();
    auto rsmiles = MolToSmiles(*mol, ps);
    std::unique_ptr<RWMol> mol2(SmilesToMol(rsmiles));
    REQUIRE(mol2);
    auto smiles = MolToSmiles(*mol2);
    if (smiles != csmiles) {
      std::cerr << smiles << std::endl;
    }
    CHECK(smiles == csmiles);
  }
#endif

  SECTION("bulk random output order") {
    auto csmiles = MolToSmiles(*mol);
    CHECK(csmiles == "O=C1Nc2cc(Br)ccc2/C1=C1/Nc2ccccc2/C1=N\\O");
    SmilesWriteParams ps;
    ps.doRandom = true;
    for (auto i = 0u; i < 100; ++i) {
      if (i == 50) {
        // we know this one fails (it's explicitly tested above)
        continue;
      }
      getRandomGenerator(i + 1)();
      auto rsmiles = MolToSmiles(*mol, ps);
      std::unique_ptr<RWMol> mol2(SmilesToMol(rsmiles));
      REQUIRE(mol2);
      auto smiles = MolToSmiles(*mol2);
      if (smiles != csmiles) {
        std::cerr << ">>> " << i << " " << rsmiles << std::endl;
      }
      CHECK(smiles == csmiles);
    }
  }
}

TEST_CASE("Github #4582 continued: double bonds and ring closures") {
  SECTION("basics") {
    auto mol = R"CTAB(
  Mrv2108 10072106112D

 11 11  0  0  0  0            999 V2000
    1.2097   -1.0517    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.3651   -1.1116    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.0571    0.9536    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.2670   -0.5667    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.7001    0.4032    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.3269    0.2561    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.2180    0.8882    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.7601   -0.4305    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.5763   -1.7848    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.1262   -2.4676    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    1.4920   -3.1990    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
  8  1  1  0  0  0  0
  1  2  2  0  0  0  0
  5  3  1  0  0  0  0
  7  3  1  0  0  0  0
  4  2  1  0  0  0  0
  5  8  1  0  0  0  0
  6  7  1  0  0  0  0
  1  9  1  0  0  0  0
  9 10  2  0  0  0  0
 10 11  1  0  0  0  0
  6  4  1  0  0  0  0
M  END)CTAB"_ctab;
    REQUIRE(mol);
    auto csmiles = MolToSmiles(*mol);
    CHECK(csmiles == "C/N=C/C1=C/CCCCCC1");
  }
  SECTION("github #3967 part 1") {
    // duplicating a test from elsewhere
    auto mol = "C=c1s/c2n(c1=O)CCCCCCC\\N=2"_smiles;
    REQUIRE(mol);
    auto smi = MolToSmiles(*mol);
    CHECK(smi == "C=c1s/c2n(c1=O)CCCCCCC\\N=2");
  }
  SECTION("github #3967 part 2") {
    // duplicating a test from elsewhere
    auto mol = R"SMI(C1=CC/C=C2C3=C/CC=CC=CC\3C\2C=C1)SMI"_smiles;
    REQUIRE(mol);
    auto smi = MolToSmiles(*mol);
    CHECK(smi == R"SMI(C1=CC/C=C2C3=C\CC=CC=CC/3C\2C=C1)SMI");
  }
  SECTION("CHEMBL3623347") {
    auto mol = R"CTAB(CHEMBL3623347
     RDKit          2D

 44 47  0  0  0  0  0  0  0  0999 V2000
   -2.0000    1.0700    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.1400   -0.4700    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
   -3.6400   -0.8300    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -4.4400    0.4800    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -3.4200    1.6600    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    2.2200   -1.9300    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.6700   -2.0400    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.9400    1.7500    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.4900   -1.0400    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.1200    0.7400    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.5400    0.1400    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.5400    1.3300    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.6000    0.4700    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.4000    1.6300    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.4000    2.9600    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.8800    2.6000    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.6604    2.1462    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.6965    1.6501    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    3.2764    1.7277    0.0000 H   0  0  0  0  0  0  0  0  0  0  0  0
    0.9496    2.6967    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.4143   -0.1105    0.0000 H   0  0  0  0  0  0  0  0  0  0  0  0
   -3.6960    2.8278    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -4.2128   -2.2139    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -3.2997   -3.4050    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -3.7586   -4.5137    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -2.1099   -3.2485    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.7200   -1.1200    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.2300   -0.7900    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    5.1740   -2.2308    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    2.8927   -3.2754    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    2.0667   -4.5284    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    2.7380   -5.8707    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.9120   -7.1238    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    2.5833   -8.4661    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.7573   -9.7192    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    2.4287  -11.0615    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.9267  -11.1538    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    4.4623  -12.2277    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    4.5892  -10.1533    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    1.6027  -12.3146    0.0000 N   0  0  0  0  0  0  0  0  0  0  0  0
    2.2740  -13.6569    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    3.4719  -13.7282    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    1.6136  -14.6588    0.0000 O   0  0  0  0  0  0  0  0  0  0  0  0
    3.5388   -1.7411    0.0000 H   0  0  0  0  0  0  0  0  0  0  0  0
  1  2  1  6
  3  2  1  0
  3  4  1  0
  4  5  1  0
  5  1  1  0
 28  6  1  0
  6  7  2  0
 10  8  1  0
  8 14  1  0
 13  9  1  0
  9  7  1  0
 10 28  1  0
 27 11  1  0
 11 12  1  0
 12 10  1  0
 13 14  1  0
 14 15  1  0
 15 16  1  0
 16  1  1  0
  1 13  1  0
 12 17  1  1
 12 18  1  0
 10 19  1  1
 14 20  1  1
 13 21  1  6
  5 22  1  6
  3 23  1  1
 23 24  2  0
 24 25  1  0
 24 26  1  0
 27 28  1  0
 27 29  2  0
  6 30  1  0
 30 31  2  0
 31 32  1  0
 32 33  1  0
 33 34  1  0
 34 35  1  0
 35 36  1  0
 36 37  1  0
 37 38  2  0
 37 39  1  0
 36 40  1  1
 40 41  1  0
 41 42  1  0
 41 43  2  0
 28 44  1  1
M  END)CTAB"_ctab;
    REQUIRE(mol);
    auto csmiles = MolToSmiles(*mol);
    // clang-format off
    CHECK(
        csmiles ==
        "CC(=O)N[C@@H](CCCC/N=C/C1=C/C[C@@H]2[C@](C)(CC[C@@]23O[C@@H](C=C(C)C)C[C@@H]3C)C[C@H]2[C@@H]1C(=O)C[C@@]2(C)O)C(=O)O");
  }
  // clang-format on
}

TEST_CASE(
    "unspecified bonds at beginning of branch in SMARTS not properly tagged") {
  auto m = "C(C)C"_smarts;
  REQUIRE(m);
  CHECK(m->getBondWithIdx(0)->getQuery()->getDescription() ==
        "SingleOrAromaticBond");
  CHECK(m->getBondWithIdx(1)->getQuery()->getDescription() ==
        "SingleOrAromaticBond");
}

TEST_CASE("Github #4878: cannot parse coordinate bonds from CXSMARTS",
          "[cxsmiles]") {
  SECTION("basics") {
    auto m = "[Fe]OC |C:1.0|"_smarts;
    REQUIRE(m);
    CHECK(m->getBondWithIdx(0)->getBondType() == Bond::BondType::DATIVE);
  }
  SECTION("branches") {
    {
      auto m = "[Fe](O)OC |C:2.1|"_smarts;
      REQUIRE(m);
      CHECK(m->getBondWithIdx(1)->getBondType() == Bond::BondType::DATIVE);
    }
    {
      auto m = "[Fe](OC)O |C:1.0|"_smarts;
      REQUIRE(m);
      CHECK(m->getBondWithIdx(0)->getBondType() == Bond::BondType::DATIVE);
    }
  }
  SECTION("rings") {
    {
      auto m = "O1CC[Fe]1 |C:0.3|"_smarts;
      REQUIRE(m);
      CHECK(m->getBondBetweenAtoms(0, 3)->getBondType() ==
            Bond::BondType::DATIVE);
    }
  }
}

TEST_CASE("Github #4981: Invalid SMARTS for negated single-atoms", "[smarts]") {
  SECTION("basics") {
    auto mol = "[!C]"_smarts;
    REQUIRE(mol);
    CHECK(SmartsWrite::GetAtomSmarts(mol->getAtomWithIdx(0)) == "[!C]");
  }
  SECTION("as reported") {
    auto mol = "[NX3H2+0,NX4H3+;!$([N][!C]);!$([N]*~[#7,#8,#15,#16])]"_smarts;
    REQUIRE(mol);
    CHECK(MolToSmarts(*mol).find("N!C") == std::string::npos);
  }
}

TEST_CASE("Pol and Mod atoms in CXSMILES", "[cxsmiles]") {
  SECTION("Pol basics") {
    auto mol = "CC* |$;;Pol_p$|"_smiles;
    REQUIRE(mol);
    std::string val;
    CHECK(mol->getAtomWithIdx(2)->getPropIfPresent(
        common_properties::dummyLabel, val));
    CHECK(val == "Pol");
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "*CC |$Pol_p;;$|");
  }
  SECTION("Mod basics") {
    auto mol = "CC* |$;;Mod_p$|"_smiles;
    REQUIRE(mol);
    std::string val;
    CHECK(mol->getAtomWithIdx(2)->getPropIfPresent(
        common_properties::dummyLabel, val));
    CHECK(val == "Mod");
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "*CC |$Mod_p;;$|");
  }
}

TEST_CASE("empty atom label block", "[cxsmiles]") {
  SECTION("basics") {
    auto m = R"CTAB(
  MJ201100

  8  8  0  0  0  0  0  0  0  0999 V2000
   -1.0491    1.5839    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.7635    1.1714    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.7635    0.3463    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -1.0491   -0.0661    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.3346    0.3463    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
   -0.3346    1.1714    0.0000 C   0  0  0  0  0  0  0  0  0  0  0  0
    0.3798    1.5839    0.0000 R#  0  0  0  0  0  0  0  0  0  0  0  0
    0.3798   -0.0661    0.0000 R#  0  0  0  0  0  0  0  0  0  0  0  0
  1  2  2  0  0  0  0
  2  3  1  0  0  0  0
  3  4  2  0  0  0  0
  4  5  1  0  0  0  0
  5  6  2  0  0  0  0
  6  1  1  0  0  0  0
  6  7  1  0  0  2  0
  5  8  1  0  0  2  0
M  RGP  2   7   1   8   2
M  END)CTAB"_ctab;
    REQUIRE(m);
    m->clearConformers();
    auto smi = MolToCXSmiles(*m);
    CHECK(smi.find("$") == std::string::npos);
  }
}

TEST_CASE("Github #5372: errors with fragments and doRandom=True") {
  SECTION("basics") {
    auto m = "C.C"_smiles;
    REQUIRE(m);
    SmilesWriteParams ps;
    ps.doRandom = true;
    auto smi = MolToSmiles(*m, ps);
    CHECK(smi == "C.C");
  }
}

TEST_CASE("github #5466 writing floating point atom props cxsmiles",
          "[smiles][cxsmiles]") {
  SECTION("simple") {
    auto mol = "C"_smiles;
    REQUIRE(mol);

    mol->getAtomWithIdx(0)->setProp<std::string>("foo", "7.6");
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "C |atomProp:0.foo.7&#46;6|");

    // 7.5 is exactly representable in IEEE so this helps :)
    mol->getAtomWithIdx(0)->setProp<double>("foo", 7.5);
    smi = MolToCXSmiles(*mol);
    CHECK(smi == "C |atomProp:0.foo.7&#46;5|");
  }
  SECTION("label with .") {
    auto mol = "C"_smiles;
    REQUIRE(mol);

    mol->getAtomWithIdx(0)->setProp<int>("foo.foo", 7);
    auto smi = MolToCXSmiles(*mol);
    CHECK(smi == "C |atomProp:0.foo&#46;foo.7|");
  }
}

TEST_CASE("CXSMILES and 3d conformers") {
  SECTION("detect 3d and perceive stereo") {
    auto m =
        "CC(O)(F)Cl |(0.856693,-0.0843716,-0.11692;-0.539611,0.360041,0.18126;-1.47941,-0.628083,-0.170155;-0.665621,0.560167,1.55524;-0.999017,1.83232,-0.693661)|"_smiles;
    REQUIRE(m);
    REQUIRE(m->getNumConformers() == 1);
    CHECK(m->getConformer().is3D());
    CHECK(m->getAtomWithIdx(1)->getChiralTag() !=
          Atom::ChiralType::CHI_UNSPECIFIED);
  }
  SECTION("detect 2d") {
    auto m =
        "CC(O)Cl |(-3.9163,5.4767,;-3.9163,3.9367,;-2.5826,3.1667,;-5.25,3.1667,)|"_smiles;
    REQUIRE(m);
    REQUIRE(m->getNumConformers() == 1);
    CHECK(!m->getConformer().is3D());
    CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
          Atom::ChiralType::CHI_UNSPECIFIED);
  }
  SECTION("detect 2d when z coords are all zero") {
    auto m =
        "CC(O)Cl |(-3.9163,5.4767,0;-3.9163,3.9367,0;-2.5826,3.1667,0;-5.25,3.1667,0)|"_smiles;
    REQUIRE(m);
    REQUIRE(m->getNumConformers() == 1);
    CHECK(!m->getConformer().is3D());
    CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
          Atom::ChiralType::CHI_UNSPECIFIED);
  }
}

TEST_CASE("wiggly and wedged bonds in CXSMILES") {
  SECTION("basic reading/writing") {
    auto m = "CC(O)F |w:1.2|"_smiles;
    REQUIRE(m);
    unsigned int bondcfg = 0;
    CHECK(m->getBondWithIdx(2)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    CHECK(bondcfg == 2);
    // make sure we end up with a wiggly bond in output mol blocks:
    Chirality::reapplyMolBlockWedging(*m);
    auto mb = MolToV3KMolBlock(*m);
    CHECK(mb.find("CFG=2") != std::string::npos);
    // make sure we end up with the wiggly bond in the output CXSMILES:
    auto cxsmi = MolToCXSmiles(*m, SmilesWriteParams(),
                               SmilesWrite::CXSmilesFields::CX_ALL,
                               RestoreBondDirOptionTrue);
    CHECK(cxsmi == "CC(O)F |w:1.2|");
    // but we can turn that off
    SmilesWriteParams ps;
    cxsmi =
        MolToCXSmiles(*m, ps, SmilesWrite::CX_ALL ^ SmilesWrite::CX_BOND_CFG);
    CHECK(cxsmi == "CC(O)F");
  }

  SECTION("CXSMILES wiggly bond over-rides atomic stereo") {
    auto m = "C[C@H](O)F |w:1.2|"_smiles;
    REQUIRE(m);
    unsigned int bondcfg = 0;
    CHECK(m->getBondWithIdx(2)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    CHECK(bondcfg == 2);
    CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
          Atom::ChiralType::CHI_UNSPECIFIED);

    // make sure we end up with a wiggly bond in output mol blocks:
    Chirality::reapplyMolBlockWedging(*m);
    auto mb = MolToV3KMolBlock(*m);
    CHECK(mb.find("CFG=2") != std::string::npos);
    // make sure we end up with the wiggly bond in the output CXSMILES:
    auto cxsmi = MolToCXSmiles(*m, SmilesWriteParams(),
                               SmilesWrite::CXSmilesFields::CX_ALL,
                               RestoreBondDirOptionTrue);
    CHECK(cxsmi == "CC(O)F |w:1.2|");
  }
  SECTION("make sure order gets reversed when needed") {
    auto m = "CC(O)Cl |w:1.0|"_smiles;
    REQUIRE(m);
    CHECK(m->getBondWithIdx(0)->getBeginAtomIdx() == 1);
    unsigned int bondcfg = 0;
    CHECK(m->getBondWithIdx(0)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    CHECK(bondcfg == 2);
  }

  SECTION("make sure wedging gets applied when coordinates are there") {
    auto m =
        "CC(O)Cl |(-3.9163,5.4767,;-3.9163,3.9367,;-2.5826,3.1667,;-5.25,3.1667,),wU:1.0|"_smiles;
    REQUIRE(m);
    unsigned int bondcfg = 0;
    CHECK(m->getBondWithIdx(0)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    CHECK(bondcfg == 1);
    CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
          Atom::ChiralType::CHI_TETRAHEDRAL_CW);
    m = "CC(O)Cl |(-3.9163,5.4767,;-3.9163,3.9367,;-2.5826,3.1667,;-5.25,3.1667,),wD:1.0|"_smiles;
    REQUIRE(m);
    CHECK(m->getBondWithIdx(0)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    CHECK(bondcfg == 3);
    CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
          Atom::ChiralType::CHI_TETRAHEDRAL_CCW);
    invertMolBlockWedgingInfo(*m);
    CHECK(m->getBondWithIdx(0)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    CHECK(bondcfg == 1);
    Chirality::reapplyMolBlockWedging(*m);
    MolOps::assignChiralTypesFromBondDirs(*m);
    CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
          Atom::ChiralType::CHI_TETRAHEDRAL_CW);
    invertMolBlockWedgingInfo(*m);
    CHECK(m->getBondWithIdx(0)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    CHECK(bondcfg == 3);
    Chirality::reapplyMolBlockWedging(*m);
    MolOps::assignChiralTypesFromBondDirs(*m);
    CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
          Atom::ChiralType::CHI_TETRAHEDRAL_CCW);
    Chirality::clearMolBlockWedgingInfo(*m);
    m->getAtomWithIdx(1)->setChiralTag(Atom::ChiralType::CHI_UNSPECIFIED);
    CHECK(!m->getBondWithIdx(0)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    Chirality::reapplyMolBlockWedging(*m);
    MolOps::assignChiralTypesFromBondDirs(*m);
    CHECK(m->getAtomWithIdx(1)->getChiralTag() ==
          Atom::ChiralType::CHI_UNSPECIFIED);
  }

  SECTION("writing examples") {
    auto m = "OC(C)Cl"_smiles;
    REQUIRE(m);
    {
      ROMol nm(*m);
      nm.getBondWithIdx(1)->setBondDir(Bond::BondDir::UNKNOWN);
      auto cxsmi = MolToCXSmiles(nm, SmilesWriteParams(),
                                 SmilesWrite::CXSmilesFields::CX_ALL,
                                 RestoreBondDirOptionTrue);
      CHECK(cxsmi == "CC(O)Cl |w:1.0|");
    }
    {
      ROMol nm(*m);
      nm.getBondWithIdx(1)->setProp(common_properties::_MolFileBondCfg, 2);
      auto cxsmi = MolToCXSmiles(nm, SmilesWriteParams(),
                                 SmilesWrite::CXSmilesFields::CX_ALL,
                                 RestoreBondDirOptionClear);
      CHECK(cxsmi == "CC(O)Cl");
    }
  }

  SECTION("writing wedges and dashes") {
    auto m = R"CTAB(
  RDKit             2D

  0  0  0     0  0            999 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 4 3 0 0 0
M  V30 BEGIN ATOM
M  V30 1 Cl -5.25 3.1667 0 0
M  V30 2 C -3.9163 3.9367 0 0
M  V30 3 O -2.5826 3.1667 0 0
M  V30 4 C -3.9163 5.4767 0 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 2
M  V30 2 1 2 3
M  V30 3 1 2 4 CFG=1
M  V30 END BOND
M  V30 END CTAB
M  END
)CTAB"_ctab;
    REQUIRE(m);
    auto cxsmi = MolToCXSmiles(*m);
    CHECK(cxsmi.find("wU:1.0") != std::string::npos);
    SmilesWriteParams ps;
    cxsmi =
        MolToCXSmiles(*m, ps, SmilesWrite::CXSmilesFields::CX_ALL_BUT_COORDS);
    CHECK(cxsmi.find("wU:1.0") == std::string::npos);
    // change the bond dir. This also tests that the wedging overrides the
    // CFG property
    m->getBondWithIdx(2)->setBondDir(Bond::BondDir::BEGINDASH);
    cxsmi = MolToCXSmiles(*m, SmilesWriteParams(),
                          SmilesWrite::CXSmilesFields::CX_ALL,
                          RestoreBondDirOptionTrue);
    CHECK(cxsmi.find("wU:1.0") != std::string::npos);
    cxsmi =
        MolToCXSmiles(*m, ps, SmilesWrite::CXSmilesFields::CX_ALL_BUT_COORDS);
    CHECK(cxsmi.find("wU:1.0") == std::string::npos);
    m->getBondWithIdx(2)->setBondDir(Bond::BondDir::UNKNOWN);
    cxsmi = MolToCXSmiles(*m, SmilesWriteParams(),
                          SmilesWrite::CXSmilesFields::CX_ALL,
                          RestoreBondDirOptionTrue);
    CHECK(cxsmi.find("wU:1.0") != std::string::npos);
    // wiggly bonds get written even if we don't output coords:
    cxsmi =
        MolToCXSmiles(*m, ps, SmilesWrite::CXSmilesFields::CX_ALL_BUT_COORDS,
                      RestoreBondDirOptionClear);
    CHECK(cxsmi.find("w:1.0") == std::string::npos);
  }

  SECTION("double bond stereo") {
    auto m_from_ctab = R"CTAB(
  Mrv2211 09152216122D

  0  0  0     0  0            999 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 4 3 0 0 0
M  V30 BEGIN ATOM
M  V30 1 C -7.125 -12.0417 0 0
M  V30 2 C -5.7913 -11.2717 0 0
M  V30 3 C -4.4576 -12.0417 0 0
M  V30 4 C -8.4587 -11.2717 0 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 2 1 2
M  V30 2 1 2 3 CFG=2
M  V30 3 1 1 4
M  V30 END BOND
M  V30 END CTAB
M  END
)CTAB"_ctab;
    REQUIRE(m_from_ctab);
    auto m = "CC=CC |w:2.2|"_smiles;
    REQUIRE(m);
    unsigned int bondcfg = 0;
    CHECK(m->getBondWithIdx(2)->getPropIfPresent("_MolFileBondCfg", bondcfg));
    CHECK(bondcfg == 2);
  }

  SECTION("some invalid cxsmiles") {
    std::vector<std::string> smileses = {"CC(O)Cl |wU:0.0,wU:1.0|",
                                         "CC(O)Cl |wUP:1.0|", "CC(O)Cl |wU:1|"};
    for (const auto &smi : smileses) {
      INFO(smi);
      CHECK_THROWS_AS(SmilesToMol(smi), SmilesParseException);
    }
  }
}

TEST_CASE("ring bond stereochemistry in CXSMILES") {
  UseLegacyStereoPerceptionFixture useLegacy(false);
  SECTION("basic reading") {
    std::vector<std::pair<std::string, Bond::BondStereo>> tests = {
        {"C1CCCC/C=C/CCC1 |t:5|", Bond::BondStereo::STEREOTRANS},
        {"C1CCCCC=CCCC1 |t:5|", Bond::BondStereo::STEREOTRANS},
        {"C1CCCCC=CCCC1 |c:5|", Bond::BondStereo::STEREOCIS},
        {"C1CCCC/C=C/CCC1 |c:5|", Bond::BondStereo::STEREOCIS},
        {"C1CCCCC=CCCC1 |ctu:5|", Bond::BondStereo::STEREOANY},
        {"C1CCCC/C=C/CCC1 |ctu:5|", Bond::BondStereo::STEREOANY},
    };
    for (const auto &[smi, val] : tests) {
      std::unique_ptr<RWMol> m{SmilesToMol(smi)};
      INFO(smi);
      REQUIRE(m);
      CHECK(m->getBondWithIdx(5)->getBondType() == Bond::BondType::DOUBLE);
      CHECK(m->getBondWithIdx(5)->getStereo() == val);
    }
  }
  SECTION("read multiple values") {
    std::unique_ptr<RWMol> m{SmilesToMol("C1CCCCC=CCCC=1 |t:5,9|")};
    REQUIRE(m);
    CHECK(m->getBondWithIdx(5)->getBondType() == Bond::BondType::DOUBLE);
    CHECK(m->getBondWithIdx(5)->getStereo() == Bond::BondStereo::STEREOTRANS);
    CHECK(m->getBondWithIdx(9)->getBondType() == Bond::BondType::DOUBLE);
    CHECK(m->getBondWithIdx(9)->getStereo() == Bond::BondStereo::STEREOTRANS);
  }
  SECTION("skip small rings") {
    std::vector<std::pair<std::string, Bond::BondStereo>> tests = {
        {"C1=CCCCC1 |ctu:0|", Bond::BondStereo::STEREONONE},
        {"C1=CCCCC1 |c:0|", Bond::BondStereo::STEREONONE}};
    for (const auto &[smi, val] : tests) {
      std::unique_ptr<RWMol> m{SmilesToMol(smi)};
      INFO(smi);
      REQUIRE(m);
      CHECK(m->getBondWithIdx(0)->getBondType() == Bond::BondType::DOUBLE);
      CHECK(m->getBondWithIdx(0)->getStereo() == val);
    }
  }
  SECTION("basic writing") {
    std::vector<std::pair<std::string, std::string>> tests = {
        {"C1CCCC/C=C/CCC1 |t:5|", "C1=C/CCCCCCCC/1 |t:0|"},
        {"C1CCCCC=CCCC1 |t:5|", "C1=C/CCCCCCCC/1 |t:0|"},
        {"C1CCCCC=CCCC1 |c:5|", "C1=C\\CCCCCCCC/1 |c:0|"},
        {"C1CCCC/C=C/CCC1 |c:5|", "C1=C\\CCCCCCCC/1 |c:0|"},
        {"C1=CCCCCCCCC1 |ctu:0|", "C1=CCCCCCCCC1 |ctu:0|"},
        {"C=CCCCCCCCC |ctu:0|", "C=CCCCCCCCC"}
        // we don't write the markers for non-ring bonds
    };
    for (const auto &[smi, val] : tests) {
      std::unique_ptr<RWMol> m{SmilesToMol(smi)};
      INFO(smi);
      REQUIRE(m);
      SmilesWriteParams ops;
      auto cxsmi = MolToCXSmiles(
          *m, ops, SmilesWrite::CXSmilesFields::CX_ALL_BUT_COORDS);
      CHECK(cxsmi == val);
    }
  }
}

TEST_CASE(
    "Github #5722: check w/c/t/ctu CX labels use bond positions from SMILES",
    "[SMILES][bug]") {
  SECTION("'w:' label") {
    auto m = "CC1CN1C=CC1CC1 |w:4.5|"_smiles;
    REQUIRE(m);
    auto b = m->getBondWithIdx(4);
    REQUIRE(b->getBondType() == Bond::BondType::DOUBLE);
    CHECK(b->getBondDir() == Bond::BondDir::UNKNOWN);
  }
  SECTION("'ctu:' label") {
    auto m = "CC1CN1C=CC1CC1 |ctu:5|"_smiles;
    REQUIRE(m);
    auto b = m->getBondWithIdx(4);
    REQUIRE(b->getBondType() == Bond::BondType::DOUBLE);
    CHECK(b->getStereo() == Bond::STEREOANY);
  }
  SECTION("'c:' label") {
    UseLegacyStereoPerceptionFixture useLegacy(false);

    auto m = "CC1CN1C=CC1CC1 |c:5|"_smiles;
    REQUIRE(m);
    auto b = m->getBondWithIdx(4);
    REQUIRE(b->getBondType() == Bond::BondType::DOUBLE);
    CHECK(b->getStereo() == Bond::STEREOCIS);
  }
  SECTION("'t:' label") {
    UseLegacyStereoPerceptionFixture useLegacy(false);

    auto m = "CC1CN1C=CC1CC1 |t:5|"_smiles;
    REQUIRE(m);
    auto b = m->getBondWithIdx(4);
    REQUIRE(b->getBondType() == Bond::BondType::DOUBLE);
    CHECK(b->getStereo() == Bond::STEREOTRANS);
  }
}

TEST_CASE("Github #5683: SMARTS bond ordering should be the same as SMILES") {
  SECTION("basics") {
    auto m = "C(OC)C"_smarts;
    REQUIRE(m);
    CHECK(m->getBondBetweenAtoms(0, 1)->getIdx() == 0);
    CHECK(m->getBondBetweenAtoms(1, 2)->getIdx() == 1);
    CHECK(m->getBondBetweenAtoms(0, 3)->getIdx() == 2);
  }
  SECTION("as reported: SMARTS which should generate an exception") {
    auto sma = "O=C(C=CCC1CCCCC1)N1N=Cc2ccccc2C1c1ccccc1 |w:3.1|";
    CHECK_THROWS_AS(SmartsToMol(sma), SmilesParseException);
  }
}

TEST_CASE("SMARTS for molecule with zero order bond should match itself") {
  auto m = "CN(<-[Li+])C"_smiles;
  m->getBondBetweenAtoms(1, 2)->setBondType(Bond::BondType::ZERO);
  const auto sma = MolToSmarts(*m);
  std::unique_ptr<ROMol> q{SmartsToMol(sma)};
  SubstructMatchParameters ps;
  ps.maxMatches = 1;
  CHECK(SubstructMatch(*m, *q, ps).size() == 1);
}

TEST_CASE("smilesSymbol in SMARTS", "[smarts][smilesSymbol]") {
  // Probably just the first case is going to be useful to people
  SECTION("smilesSymbol without queries") {
    auto m = "CC*"_smiles;
    REQUIRE(m);
    m->getAtomWithIdx(2)->setProp(common_properties::smilesSymbol, "Xa");
    CHECK(MolToSmarts(*m) == "[#6]-[#6]-[Xa]");
  }
  SECTION("smilesSymbol with query on other atoms") {
    auto m = "[#6]cc"_smarts;
    REQUIRE(m);
    m->getAtomWithIdx(0)->setProp(common_properties::smilesSymbol, "Xa");
    CHECK(MolToSmarts(*m) == "[Xa]cc");
  }
  SECTION("smilesSymbol with aromaticity query") {
    auto m = "ccc"_smarts;
    REQUIRE(m);
    m->getAtomWithIdx(0)->setProp(common_properties::smilesSymbol, "Xa");
    CHECK(MolToSmarts(*m) == "[Xa;a]cc");
  }
  SECTION("smilesSymbol with aliphatic query") {
    auto m = "CCC"_smarts;
    REQUIRE(m);
    m->getAtomWithIdx(0)->setProp(common_properties::smilesSymbol, "Xa");
    CHECK(MolToSmarts(*m) == "[Xa;A]CC");
  }
  SECTION("smilesSymbol with additional queries") {
    auto m = "[#6]C[#6]"_smarts;
    REQUIRE(m);
    auto atom = m->getAtomWithIdx(1);

    atom->expandQuery(makeAtomExplicitDegreeQuery(3), Queries::COMPOSITE_AND);
    atom->expandQuery(makeAtomTotalValenceQuery(4), Queries::COMPOSITE_AND);
    atom->expandQuery(makeAtomFormalChargeQuery(2), Queries::COMPOSITE_AND);

    atom->setProp(common_properties::smilesSymbol, "Xa");

    CHECK(MolToSmarts(*m) == "[#6][Xa;A&D3&v4&+2][#6]");
  }
  SECTION("degree query") {
    auto m = "[X3]-C"_smarts;
    REQUIRE(m);
    m->getAtomWithIdx(0)->setProp(common_properties::smilesSymbol, "Xa");
    CHECK(MolToSmarts(*m) == "[Xa;X3]-C");
  }
  SECTION("atom list") {
    auto m = "[C,N,O]C"_smarts;
    REQUIRE(m);
    m->getAtomWithIdx(0)->setProp(common_properties::smilesSymbol, "Xa");
    CHECK(MolToSmarts(*m) == "[Xa;C,N,O]C");
  }
}

TEST_CASE("Atropisomer output in CXSMILES", "[SMILES]") {
  SECTION("'WithChiralAtom' label") {
    auto mol =
        "CC1=C(N2C=CC=C2[C@H](C)Cl)C(C)CCC1 |(2.679,0.4142,;1.3509,1.181,;0.0229,0.4141,;0.0229,-1.1195,;1.2645,-2.0302,;0.7901,-3.4813,;-0.7446,-3.4813,;-1.219,-2.0302,;-2.679,-1.5609,;-3.0039,-0.0556,;-3.8202,-2.595,;-1.3054,1.1809,;-2.6335,0.4141,;-1.3054,2.7145,;0.0229,3.4813,;1.3509,2.7146,),wD:2.11,wU:8.10,&1:8|"_smiles;
    REQUIRE(mol);
    CHECK(mol->getNumConformers() == 1);

    RDKit::SmilesWriteParams ps;
    ps.canonical = false;
    unsigned int flags = SmilesWrite::CXSmilesFields::CX_COORDS |
                         SmilesWrite::CXSmilesFields::CX_MOLFILE_VALUES |
                         SmilesWrite::CXSmilesFields::CX_ATOM_PROPS |
                         SmilesWrite::CXSmilesFields::CX_BOND_CFG |
                         SmilesWrite::CXSmilesFields::CX_ENHANCEDSTEREO;

    auto smi = MolToCXSmiles(*mol, ps, flags,
                             RestoreBondDirOption::RestoreBondDirOptionTrue);

    CHECK(
        smi ==
        "CC1=C(n2cccc2[C@H](C)Cl)C(C)CCC1 |(2.679,0.4142,;1.3509,1.181,;0.0229,0.4141,;0.0229,-1.1195,;1.2645,-2.0302,;0.7901,-3.4813,;-0.7446,-3.4813,;-1.219,-2.0302,;-2.679,-1.5609,;-3.0039,-0.0556,;-3.8202,-2.595,;-1.3054,1.1809,;-2.6335,0.4141,;-1.3054,2.7145,;0.0229,3.4813,;1.3509,2.7146,),wD:2.11,wU:8.10,&1:8|");

    flags = SmilesWrite::CXSmilesFields::CX_COORDS |
            SmilesWrite::CXSmilesFields::CX_MOLFILE_VALUES |
            SmilesWrite::CXSmilesFields::CX_ATOM_PROPS |
            SmilesWrite::CXSmilesFields::CX_BOND_ATROPISOMER |
            SmilesWrite::CXSmilesFields::CX_ENHANCEDSTEREO;

    smi = MolToCXSmiles(*mol, ps, flags,
                        RestoreBondDirOption::RestoreBondDirOptionTrue);

    CHECK(
        smi ==
        "CC1=C(n2cccc2[C@H](C)Cl)C(C)CCC1 |(2.679,0.4142,;1.3509,1.181,;0.0229,0.4141,;0.0229,-1.1195,;1.2645,-2.0302,;0.7901,-3.4813,;-0.7446,-3.4813,;-1.219,-2.0302,;-2.679,-1.5609,;-3.0039,-0.0556,;-3.8202,-2.595,;-1.3054,1.1809,;-2.6335,0.4141,;-1.3054,2.7145,;0.0229,3.4813,;1.3509,2.7146,),wD:2.11,&1:8|");

    flags = SmilesWrite::CXSmilesFields::CX_COORDS |
            SmilesWrite::CXSmilesFields::CX_MOLFILE_VALUES |
            SmilesWrite::CXSmilesFields::CX_ATOM_PROPS |
            SmilesWrite::CXSmilesFields::CX_ENHANCEDSTEREO;

    smi = MolToCXSmiles(*mol, ps, flags,
                        RestoreBondDirOption::RestoreBondDirOptionTrue);

    CHECK(
        smi ==
        "CC1=C(n2cccc2[C@H](C)Cl)C(C)CCC1 |(2.679,0.4142,;1.3509,1.181,;0.0229,0.4141,;0.0229,-1.1195,;1.2645,-2.0302,;0.7901,-3.4813,;-0.7446,-3.4813,;-1.219,-2.0302,;-2.679,-1.5609,;-3.0039,-0.0556,;-3.8202,-2.595,;-1.3054,1.1809,;-2.6335,0.4141,;-1.3054,2.7145,;0.0229,3.4813,;1.3509,2.7146,),&1:8|");
  }
}

TEST_CASE("Dative  bond in cxsmiles double double def", "[bug][cxsmiles]") {
  SECTION("basics") {
    SmilesParserParams smilesParserParams;
    smilesParserParams.sanitize = true;
    smilesParserParams.allowCXSMILES = true;

    std::unique_ptr<RWMol> smilesMol(
        SmilesToMol("C1CCC2=[N]1[Fe](\\[O]=C(\\C)/C=C/C1CCCC1)[N]1=C(CCC1)C2",
                    smilesParserParams));
    RDKit::Chirality::reapplyMolBlockWedging(*smilesMol);
    {
      SmilesWriteParams ps;
      ps.canonical = true;

      std::string smilesOut = MolToSmiles(*smilesMol, ps);

      CHECK(smilesOut == "CC(/C=C/C1CCCC1)=O->[Fe]1<-N2=C(CC3=N->1CCC3)CCC2");
    }
  }
}

TEST_CASE("Fieldname not found in SuperatomSgroup in CXSmiles",
          "[bug][cxsmiles]") {
  SECTION("basics") {
    SmilesParserParams smilesParserParams;
    smilesParserParams.sanitize = true;
    smilesParserParams.allowCXSMILES = true;

    std::unique_ptr<RWMol> smilesMol(
        SmilesToMol("CC |SgD:0:::|", smilesParserParams));
    RDKit::Chirality::reapplyMolBlockWedging(*smilesMol);
    {
      SmilesWriteParams ps;
      ps.canonical = true;

      std::string smilesOut = MolToCXSmiles(*smilesMol, ps);

      CHECK(smilesOut == "CC |SgD:0::::::|");
    }
  }
}
TEST_CASE("ensure unused features are not used") {
  SECTION("isotopes") {
    auto mol1 = "FOCN[15F]"_smiles;
    REQUIRE(mol1);
    auto mol2 = "[15F]OCNF"_smiles;
    REQUIRE(mol2);
    std::vector<unsigned int> ranks;
    SmilesWriteParams ps;
    ps.doIsomericSmiles = true;
    auto smiles = MolToSmiles(*mol1, ps);
    CHECK(smiles == "FOCN[15F]");
    smiles = MolToSmiles(*mol2, ps);
    CHECK(smiles == "FNCO[15F]");

    ps.doIsomericSmiles = false;
    smiles = MolToSmiles(*mol1, ps);
    CHECK(smiles == "FNCOF");
    smiles = MolToSmiles(*mol2, ps);
    CHECK(smiles == "FNCOF");
  }

  SECTION("chirality") {
    auto mol1 = "FC(Cl)OCN[C@H](F)Cl"_smiles;
    REQUIRE(mol1);
    auto mol2 = "FC(Cl)NCO[C@H](F)Cl"_smiles;
    REQUIRE(mol2);
    std::vector<unsigned int> ranks;
    SmilesWriteParams ps;
    ps.doIsomericSmiles = true;
    auto smiles = MolToSmiles(*mol1, ps);
    CHECK(smiles == "FC(Cl)OCN[C@H](F)Cl");
    smiles = MolToSmiles(*mol2, ps);
    CHECK(smiles == "FC(Cl)NCO[C@H](F)Cl");

    ps.doIsomericSmiles = false;
    smiles = MolToSmiles(*mol1, ps);
    CHECK(smiles == "FC(Cl)NCOC(F)Cl");
    smiles = MolToSmiles(*mol2, ps);
    CHECK(smiles == "FC(Cl)NCOC(F)Cl");
  }

  SECTION("ring stereo") {
    auto mol1 = "CC1CCC(CC1)NO[C@@H]1CC[C@H](C)CC1"_smiles;
    REQUIRE(mol1);
    auto mol2 = "CC1CCC(CC1)ON[C@@H]1CC[C@H](C)CC1"_smiles;
    REQUIRE(mol2);
    std::vector<unsigned int> ranks;
    SmilesWriteParams ps;
    ps.doIsomericSmiles = true;
    auto smiles = MolToSmiles(*mol1, ps);
    CHECK(smiles == "CC1CCC(NO[C@H]2CC[C@@H](C)CC2)CC1");
    smiles = MolToSmiles(*mol2, ps);
    CHECK(smiles == "CC1CCC(ON[C@H]2CC[C@@H](C)CC2)CC1");

    ps.doIsomericSmiles = false;
    smiles = MolToSmiles(*mol1, ps);
    CHECK(smiles == "CC1CCC(NOC2CCC(C)CC2)CC1");
    smiles = MolToSmiles(*mol2, ps);
    CHECK(smiles == "CC1CCC(NOC2CCC(C)CC2)CC1");
  }

  SECTION("enhanced stereo") {
    // if we aren't doing CXSMILES then the enhanced stereo shouldn't enter into
    // consideration in canonicalization
    std::unique_ptr<ROMol> mol1 = "F[C@H](Cl)NCO[C@H](F)Cl |&1:6|"_smiles;
    REQUIRE(mol1);
    std::unique_ptr<ROMol> mol2 = "F[C@H](Cl)OCN[C@H](F)Cl |&1:6|"_smiles;
    REQUIRE(mol2);
    SmilesWriteParams ps;
    ps.doIsomericSmiles = true;
    auto smiles = MolToSmiles(*mol1, ps);
    CHECK(smiles == "F[C@H](Cl)NCO[C@H](F)Cl");
    smiles = MolToSmiles(*mol2, ps);
    CHECK(smiles == "F[C@H](Cl)NCO[C@H](F)Cl");

    smiles = MolToCXSmiles(*mol1, ps);
    CHECK(smiles == "F[C@H](Cl)NCO[C@H](F)Cl |&1:6|");
    smiles = MolToCXSmiles(*mol2, ps);
    CHECK(smiles == "F[C@H](Cl)OCN[C@H](F)Cl |&1:6|");

    ps.doIsomericSmiles = false;
    smiles = MolToSmiles(*mol1, ps);
    CHECK(smiles == "FC(Cl)NCOC(F)Cl");
    smiles = MolToSmiles(*mol2, ps);
    CHECK(smiles == "FC(Cl)NCOC(F)Cl");
  }

  SECTION("problematic cases") {
    auto mol1 =
        "[C@H]1CC(C[C@@H](C)[C@@H](C)O)C[C@@H](C)C1 |o1:1,11,o2:5,7|"_smiles;
    REQUIRE(mol1);
    Canon::canonicalizeEnhancedStereo(*mol1);
    auto smiles = MolToSmiles(*mol1);
    CHECK(smiles == "C[C@H]1C[CH]CC(C[C@@H](C)[C@@H](C)O)C1");
  }
}
TEST_CASE("enhanced stereo canonicalized") {
  SECTION("basic") {
    std::unique_ptr<ROMol> mol1 = "F[C@H](Cl)NCO[C@H](F)Cl |&1:6|"_smiles;
    REQUIRE(mol1);
    std::unique_ptr<ROMol> mol2 = "F[C@H](Cl)OCN[C@H](F)Cl |&1:6|"_smiles;
    REQUIRE(mol2);

    SmilesWriteParams ps;
    ps.doIsomericSmiles = true;

    RDKit::canonicalizeStereoGroups(mol1);
    RDKit::canonicalizeStereoGroups(mol2);

    auto smiles = MolToCXSmiles(*mol1, ps);
    CHECK(smiles == "F[C@H](Cl)NCO[C@H](F)Cl |a:1,&1:6|");
    smiles = MolToCXSmiles(*mol2, ps);
    CHECK(smiles == "F[C@H](Cl)OCN[C@H](F)Cl |a:1,&1:6|");
  }

  SECTION("trimethylcyclohexane") {
    std::unique_ptr<ROMol> mol1 =
        "C[C@H]1C[C@@H](C)C[C@@H](C)C1 |o1:1,o2:6,o3:3|"_smiles;
    std::unique_ptr<ROMol> mol2 = "C[C@H]1C[C@@H](C)C[C@@H](C)C1 |o1:1|"_smiles;
    std::unique_ptr<ROMol> mol3 =
        "C[C@@H]1C[C@H](C)C[C@H](C)C1 |o1:1,o2:6,o3:3|"_smiles;
    REQUIRE(mol1);
    REQUIRE(mol2);
    REQUIRE(mol3);

    SmilesWriteParams ps;
    RDKit::canonicalizeStereoGroups(mol1);
    auto smiles = MolToCXSmiles(*mol1, ps);
    CHECK(smiles == "C[C@H]1C[C@H](C)C[C@H](C)C1 |a:1,o1:3,6|");

    RDKit::canonicalizeStereoGroups(mol2);
    smiles = MolToCXSmiles(*mol2, ps);
    CHECK(smiles == "C[C@H]1C[C@H](C)C[C@H](C)C1 |a:1,o1:3,6|");

    RDKit::canonicalizeStereoGroups(mol3);
    smiles = MolToCXSmiles(*mol3, ps);
    CHECK(smiles == "C[C@H]1C[C@H](C)C[C@H](C)C1 |a:1,o1:3,6|");
  }

  SECTION("multiFrag test") {
    std::unique_ptr<ROMol> mol1 =
        "C[C@H]1C[C@@H](C)C[C@@H](C)C1.F[C@H](Cl)NCO[C@H](F)Cl |o1:6,o2:3,o3:1,o4:15|"_smiles;
    REQUIRE(mol1);

    SmilesWriteParams ps;
    RDKit::canonicalizeStereoGroups(mol1);
    auto smiles = MolToCXSmiles(*mol1, ps);
    CHECK(
        smiles ==
        "C[C@H]1C[C@H](C)C[C@H](C)C1.F[C@H](Cl)NCO[C@H](F)Cl |a:1,10,o1:3,6,o2:15|");
  }
}

std::unique_ptr<ROMol> getSmartsRootedAtAtom(const ROMol &mol, int root_idx) {
  bool doIsomericSmarts = true;
  auto smarts = MolToSmarts(mol, doIsomericSmarts, root_idx);
  return std::unique_ptr<ROMol>{SmartsToMol(smarts)};
}

TEST_CASE("Test rootedAtAtom argument", "[smarts]") {
  SubstructMatchParameters sssparams;
  sssparams.useChirality = GENERATE(false, true);
  CAPTURE(sssparams.useChirality);

  SECTION("fully substituted chiral center in linear mol") {
    auto mol1 = "C[C@](O)(F)CCCl"_smiles;
    auto mol2 = "C[C@](F)(O)CCCl"_smiles;
    REQUIRE(mol1);
    REQUIRE(mol2);
    REQUIRE(mol1->getNumAtoms() == 7);
    REQUIRE(mol2->getNumAtoms() == 7);

    auto root_idx = GENERATE(range(-1, 6));
    CAPTURE(root_idx);
    auto qmol1 = getSmartsRootedAtAtom(*mol1, root_idx);
    auto qmol2 = getSmartsRootedAtAtom(*mol2, root_idx);

    CHECK(SubstructMatch(*mol1, *qmol1, sssparams).size() == 1);
    CHECK(SubstructMatch(*mol2, *qmol2, sssparams).size() == 1);
    CHECK(SubstructMatch(*mol2, *qmol1, sssparams).size() ==
          static_cast<size_t>(static_cast<size_t>(!sssparams.useChirality)));
    CHECK(SubstructMatch(*mol1, *qmol2, sssparams).size() ==
          static_cast<size_t>(static_cast<size_t>(!sssparams.useChirality)));
  }

  SECTION("chiral center w/ implicit H in linear mol") {
    auto mol1 = "C[C@H](F)CCCl"_smiles;
    auto mol2 = "C[C@@H](F)CCCl"_smiles;
    REQUIRE(mol1);
    REQUIRE(mol2);
    REQUIRE(mol1->getNumAtoms() == 6);
    REQUIRE(mol2->getNumAtoms() == 6);

    auto root_idx = GENERATE(range(-1, 5));
    CAPTURE(root_idx);
    auto qmol1 = getSmartsRootedAtAtom(*mol1, root_idx);
    auto qmol2 = getSmartsRootedAtAtom(*mol2, root_idx);

    CHECK(SubstructMatch(*mol1, *qmol1, sssparams).size() == 1);
    CHECK(SubstructMatch(*mol2, *qmol2, sssparams).size() == 1);
    CHECK(SubstructMatch(*mol2, *qmol1, sssparams).size() ==
          static_cast<size_t>(static_cast<size_t>(!sssparams.useChirality)));
    CHECK(SubstructMatch(*mol1, *qmol2, sssparams).size() ==
          static_cast<size_t>(static_cast<size_t>(!sssparams.useChirality)));
  }

  SECTION("fully substituted, asymmetric chiral atoms (2) in ring") {
    auto mol1 = "C[C@](F)1CC[C@](N)(F)CC1"_smiles;
    auto mol2 = "F[C@](C)1CC[C@](N)(F)CC1"_smiles;
    REQUIRE(mol1);
    REQUIRE(mol2);
    REQUIRE(mol1->getNumAtoms() == 10);
    REQUIRE(mol2->getNumAtoms() == 10);

    auto root_idx = GENERATE(range(-1, 9));
    CAPTURE(root_idx);
    auto qmol1 = getSmartsRootedAtAtom(*mol1, root_idx);
    auto qmol2 = getSmartsRootedAtAtom(*mol2, root_idx);

    CHECK(SubstructMatch(*mol1, *qmol1, sssparams).size() == 1);
    CHECK(SubstructMatch(*mol2, *qmol2, sssparams).size() == 1);
    CHECK(SubstructMatch(*mol2, *qmol1, sssparams).size() ==
          static_cast<size_t>(!sssparams.useChirality));
    CHECK(SubstructMatch(*mol1, *qmol2, sssparams).size() ==
          static_cast<size_t>(!sssparams.useChirality));
  }

  SECTION("partially substituted, asymmetric chiral atoms (2) in ring") {
    auto mol1 = "C[C@H]1CC[C@@H](N)CC1"_smiles;
    auto mol2 = "C[C@H]1CC[C@H](N)CC1"_smiles;
    REQUIRE(mol1);
    REQUIRE(mol2);
    REQUIRE(mol1->getNumAtoms() == 8);
    REQUIRE(mol2->getNumAtoms() == 8);

    auto root_idx = GENERATE(range(-1, 7));
    CAPTURE(root_idx);
    auto qmol1 = getSmartsRootedAtAtom(*mol1, root_idx);
    auto qmol2 = getSmartsRootedAtAtom(*mol2, root_idx);

    CHECK(SubstructMatch(*mol1, *qmol1, sssparams).size() == 1);
    CHECK(SubstructMatch(*mol2, *qmol2, sssparams).size() == 1);
    CHECK(SubstructMatch(*mol2, *qmol1, sssparams).size() ==
          static_cast<size_t>(!sssparams.useChirality));
    CHECK(SubstructMatch(*mol1, *qmol2, sssparams).size() ==
          static_cast<size_t>(!sssparams.useChirality));
  }

  SECTION("Issue #7572: prevent rootAtAtom if more than one fragment exists.") {
    auto mol1 =
        "C(C)Oc1c(Cl)c(C(=O)[O-])c(Cl)c(OCC)c1C(=O)[O-].O=C([O-])C#CC#CC(=O)[O-].[Zn][Zn]"_smiles;
    REQUIRE(mol1);
    auto ps = SmilesWriteParams();
    ps.rootedAtAtom = 20;
    CHECK_THROWS_AS(MolToSmiles(*mol1, ps), Invar::Invariant);
  }
}

TEST_CASE(
    "Github #5499: STEREOANY bonds lead to non-stable SMILES/SMARTS strings") {
  SECTION("as reported") {
    std::string mb = R"CTAB(7643724
     RDKit          2D

  0  0  0  0  0  0  0  0  0  0999 V3000
M  V30 BEGIN CTAB
M  V30 COUNTS 14 15 0 0 0
M  V30 BEGIN ATOM
M  V30 1 N 2.776307 0.296081 0.000000 0
M  V30 2 N 2.708144 -1.320684 0.000000 0
M  V30 3 N 2.976836 -2.283790 0.000000 0
M  V30 4 N 4.328520 -0.579106 0.000000 0
M  V30 5 C 1.812993 0.027197 0.000000 0
M  V30 6 C 1.770944 -0.971719 0.000000 0
M  V30 7 C 3.329205 -0.537040 0.000000 0
M  V30 8 C 3.124872 1.233298 0.000000 0
M  V30 9 C 0.968792 0.563284 0.000000 0
M  V30 10 C 0.884677 -1.434947 0.000000 0
M  V30 11 C 0.082334 0.100263 0.000000 0
M  V30 12 C 0.040268 -0.899052 0.000000 0
M  V30 13 C 2.487521 2.003850 0.000000 0
M  V30 14 C 2.836086 2.941066 0.000000 0
M  V30 END ATOM
M  V30 BEGIN BOND
M  V30 1 1 1 5
M  V30 2 1 1 7
M  V30 3 1 1 8
M  V30 4 1 2 3
M  V30 5 1 2 6
M  V30 6 1 2 7
M  V30 7 2 4 7 CFG=2
M  V30 8 2 5 6
M  V30 9 1 5 9
M  V30 10 1 6 10
M  V30 11 1 8 13
M  V30 12 2 9 11
M  V30 13 2 10 12
M  V30 14 1 11 12
M  V30 15 2 13 14
M  V30 END BOND
M  V30 END CTAB
M  END)CTAB";
    auto params = v2::FileParsers::MolFileParserParams();
    params.sanitize = false;
    auto m = v2::FileParsers::MolFromMolBlock(mb, params);
    REQUIRE(m);
    auto osmi = MolToSmiles(*m);
    auto smiparams = v2::SmilesParse::SmilesParserParams();
    smiparams.sanitize = false;
    auto m2 = v2::SmilesParse::MolFromSmiles(osmi, smiparams);
    REQUIRE(m2);
    CHECK(MolToSmiles(*m2) == osmi);
  }
}

TEST_CASE("leaks on unclosed rings") {
  // These are Ok except for the unmatched ring closures
  SECTION("Ok SMARTS") {
    auto m = "C1.C2.C3.C4.C5"_smarts;
    REQUIRE(!m);
  }
  SECTION("Ok SMILES") {
    auto m = "C1.C2.C3.C4.C5"_smiles;
    REQUIRE(!m);
  }
  // These are bogus, but fail parsing AFTER we've seen
  // the unmatched ring closure
  SECTION("Bad SMARTS") {
    auto m = "C1C)foo"_smarts;
    REQUIRE(!m);
  }
  SECTION("Bad SMILES") {
    auto m = "C1C)foo"_smiles;
    REQUIRE(!m);
  }
}

TEST_CASE("Github #7295") {
  SECTION("basics") {
    auto m = "C[C@@H]1CC[C@@H](C(=O)O)CC1.Cl"_smiles;
    REQUIRE(m);
    auto smi1 = MolToSmiles(*m);
    m->clearComputedProps();
    auto smi2 = MolToSmiles(*m);
    CHECK(smi1 == smi2);
    auto m2(*m);
    bool cleanIt = true;
    MolOps::assignStereochemistry(m2, cleanIt);
    auto smi3 = MolToSmiles(m2);
    CHECK(smi1 == smi3);
  }
}

TEST_CASE("simpleSmiles") {
  SECTION("basics") {
    auto m = "CCN(CCO)CCCCC[C@H]1CC[C@H](N(C)C(=O)Oc2ccc(Cl)cc2)CC1.Cl"_smiles;
    REQUIRE(m);
    CHECK(MolToSmiles(*m) ==
          "CCN(CCO)CCCCC[C@H]1CC[C@H](N(C)C(=O)Oc2ccc(Cl)cc2)CC1.Cl");
  }
}

TEST_CASE("CX_BOND_ATROPISOMER option requires ring info", "[bug][cxsmiles]") {
  std::string rdbase = getenv("RDBASE");
  std::string fName =
      rdbase +
      "/Code/GraphMol/FileParsers/test_data/atropisomers/RP-6306_atrop1.sdf";

  auto m = v2::FileParsers::MolFromMolFile(fName);
  REQUIRE(m);

  auto atropBond = m->getBondWithIdx(3);
  REQUIRE(atropBond->getStereo() == Bond::STEREOATROPCW);

  // Clear ring info to check that atropisomer wedging doesn't fail
  // if the info is not present
  bool includeRingInfo = true;
  m->clearComputedProps(includeRingInfo);

  auto ps = SmilesWriteParams();
  auto flags = SmilesWrite::CXSmilesFields::CX_BOND_ATROPISOMER;

  // This will fail if there's no ring information
  auto smi = MolToCXSmiles(*m, ps, flags);
  CHECK(smi == "Cc1cc2c(C(N)=O)c(N)n(-c3c(C)ccc(O)c3C)c2nc1C |wD:10.9|");
}

TEST_CASE("Github #7372: SMILES output option to disable dative bonds") {
  SECTION("basics") {
    auto m = "[NH3]->[Fe]-[NH2]"_smiles;
    REQUIRE(m);
    auto smi = MolToSmiles(*m);
    CHECK(smi == "N[Fe]<-N");
    SmilesWriteParams ps;
    ps.includeDativeBonds = false;
    auto newSmi = MolToSmiles(*m, ps);
    CHECK(newSmi == "N[Fe][NH3]");
    // ensure that representation round trips:
    auto m2 = v2::SmilesParse::MolFromSmiles(newSmi);
    REQUIRE(m2);
    CHECK(MolToSmiles(*m2) == smi);
  }
  SECTION("SMARTS basics") {
    auto m = "[NH3]->[Fe]-[NH2]"_smiles;
    REQUIRE(m);
    auto smi = MolToSmarts(*m);
    CHECK(smi == "[#7H3]->[Fe]-[#7H2]");
    SmilesWriteParams ps;
    ps.includeDativeBonds = false;
    auto newSmi = MolToSmarts(*m, ps);
    CHECK(newSmi == "[#7H3]-[Fe]-[#7H2]");
  }
}

void strip_atom_properties(RWMol *molecule) {
  for (auto atom : molecule->atoms()) {
    for (auto property : atom->getPropList(false, false)) {
      atom->clearProp(property);
    }
  }

  // return molecule;
}

TEST_CASE("Remove CX extension from SMILES", "[cxsmiles]") {
  SECTION("basics") {
    std::unique_ptr<RWMol> molecule(RDKit::SmilesToMol(
        "N[C@@H]([O-])C1=[CH:1]C(=[13CH]C(=C1)N(=O)=O)C(N)[O-] |$_AV:;bar;;foo;;;;;;;;;;;$,c:5,7,t:3|",
        0, false));
    REQUIRE(molecule);
    strip_atom_properties(molecule.get());
    std::string stripped_smiles = RDKit::MolToCXSmiles(*molecule);

    CHECK(stripped_smiles ==
          "NC([O-])C1=[13CH]C(N(=O)=O)=CC([C@@H](N)[O-])=C1");
  }
}

TEST_CASE("Canonicalization of meso structures") {
  SECTION("basics") {
    std::vector<std::pair<std::vector<std::string>, std::string>> data = {
        {{"N[C@H]1CC[C@@H](O)CC1", "N[C@@H]1CC[C@H](O)CC1"},
         "N[C@H]1CC[C@@H](O)CC1"},
        {{"C[C@@H](Cl)C[C@H](C)Cl", "Cl[C@H](C)C[C@H](C)Cl",
          "C[C@@H](Cl)C[C@@H](Cl)C", "C[C@H](Cl)C[C@@H](C)Cl"},
         "C[C@H](Cl)C[C@@H](C)Cl"},
    };
    for (const auto &[smileses, expected] : data) {
      for (const auto &smi : smileses) {
        auto m = v2::SmilesParse::MolFromSmiles(smi);
        REQUIRE(m);
        auto osmi = MolToSmiles(*m);
        INFO(smi);
        CHECK(osmi == expected);
      }
    }
  }
}

TEST_CASE("Ignore atom map numbers") {
  SmilesWriteParams params;
  auto m1 = "[NH2:1]c1ccccc1"_smiles;
  CHECK(MolToSmiles(*m1, params) == "c1ccc([NH2:1])cc1");
  params.ignoreAtomMapNumbers = true;
  CHECK(MolToSmiles(*m1, params) == "[NH2:1]c1ccccc1");
  auto m2 = "Nc1ccccc1"_smiles;
  m1->getAtomWithIdx(0)->setAtomMapNum(0);
  CHECK(MolToSmiles(*m1, params) == MolToSmiles(*m2, params));
  CHECK(MolToSmiles(*m1, true, false, -1, true, false, false, false, true) ==
        MolToSmiles(*m2, true, false, -1, true, false, false, false, true));
}

TEST_CASE("Github #7340", "[Reaction][CX][CXSmiles]") {
  SECTION("Test getCXExtensions with a Vector") {
    // Create the MOL_SPTR_VECT to hold the molecular pointers
    const auto mols = {
        "CCO* |$;;;_R1$(0,0,0;1.5,0,0;1.5,1.5,0;0,1.5,0)|"_smiles,
        "C1CCCCC1 |$;label2;$|"_smiles,
        "CC(=O)O |$;label1;$|"_smiles,
        "*-C-* |$star_e;;star_e$,Sg:n:1::ht|"_smiles,
    };

    std::vector<ROMol *> mol_vect;
    mol_vect.reserve(mols.size());
    for (const auto &mol : mols) {
      mol_vect.push_back(mol.get());
    }

    // Write to smiles to populate atom and bond output order properties
    for (const auto &entry : mol_vect) {
      MolToSmiles(*entry);
    }

    std::string cxExt = SmilesWrite::getCXExtensions(
        mol_vect, RDKit::SmilesWrite::CXSmilesFields::CX_ALL);

    CHECK(
        cxExt ==
        "|(0,1.5,;1.5,1.5,;1.5,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,;0,0,),$_R1;;;;;label2;;;;;;label1;;;star_e;;star_e$,Sg:n:15::ht:::|");
  }

  SECTION("Expects an error") {
    const auto mols = {
        "CCO* |$;;;_R1$(0,0,0;1.5,0,0;1.5,1.5,0;0,1.5,0)|"_smiles,
        "C1CCCCC1 |$;label2;$|"_smiles,
        "CC(=O)O |$;label1;$|"_smiles,
        "*-C-* |$star_e;;star_e$,Sg:n:1::ht|"_smiles,
    };

    std::vector<ROMol *> mol_vect;
    mol_vect.reserve(mols.size());
    for (const auto &mol : mols) {
      mol_vect.push_back(mol.get());
    }

    CHECK_THROWS_AS(SmilesWrite::getCXExtensions(
                        mol_vect, RDKit::SmilesWrite::CXSmilesFields::CX_ALL),
                    ValueErrorException);
  }
}

TEST_CASE("trimethylcyclohexane") {
  SECTION("Basic") {
    UseLegacyStereoPerceptionFixture useLegacy(false);

    auto smi = "C[C@H]1C[C@@H](C)C[C@@H](C)C1";
    RDKit::v2::SmilesParse::SmilesParserParams smilesParserParams;
    auto m1 = RDKit::v2::SmilesParse::MolFromSmiles(smi, smilesParserParams);
    auto smiOut = RDKit::MolToCXSmiles(*m1);
    CHECK(smiOut == "C[C@@H]1C[C@H](C)C[C@H](C)C1");
  }
  SECTION("WithEnhancedStereo") {
    UseLegacyStereoPerceptionFixture useLegacy(false);

    auto smi = "C[C@H]1C[C@@H](C)C[C@@H](C)C1 |o1:1,o2:6,o3:3|";
    RDKit::v2::SmilesParse::SmilesParserParams smilesParserParams;
    auto m1 = RDKit::v2::SmilesParse::MolFromSmiles(smi, smilesParserParams);
    auto smiOut = RDKit::MolToCXSmiles(*m1);
    CHECK(smiOut == "C[C@H]1C[C@H](C)C[C@H](C)C1 |o1:1,o2:3,o3:6|");
  }
}
