// unit tests for testing scatter/gather operations through the library interface

#include "lammps.h"
#include "library.h"
#include "lmptype.h"
#include "platform.h"
#include <string>

#include "gmock/gmock.h"
#include "gtest/gtest.h"

#include "test_main.h"

#define STRINGIFY(val) XSTR(val)
#define XSTR(val) #val

using ::LAMMPS_NS::bigint;
using ::LAMMPS_NS::tagint;
using ::LAMMPS_NS::platform::path_join;
using ::testing::HasSubstr;
using ::testing::StartsWith;

class GatherProperties : public ::testing::Test {
protected:
    void *lmp;
    std::string INPUT_DIR = STRINGIFY(TEST_INPUT_FOLDER);

    GatherProperties()           = default;
    ~GatherProperties() override = default;

    void SetUp() override
    {
        const char *args[] = {"LAMMPS_test", "-log",      "none",
                              "-echo",       "screen",    "-nocite",
                              "-var",        "input_dir", STRINGIFY(TEST_INPUT_FOLDER),
                              nullptr};

        char **argv = (char **)args;
        int argc    = (sizeof(args) / sizeof(char *)) - 1;

        ::testing::internal::CaptureStdout();
        lmp                = lammps_open_no_mpi(argc, argv, nullptr);
        std::string output = ::testing::internal::GetCapturedStdout();
        if (verbose) std::cout << output;
        EXPECT_THAT(output, StartsWith("LAMMPS ("));
    }
    void TearDown() override
    {
        ::testing::internal::CaptureStdout();
        lammps_close(lmp);
        std::string output = ::testing::internal::GetCapturedStdout();
        EXPECT_THAT(output, HasSubstr("Total wall time:"));
        if (verbose) std::cout << output;
        lmp = nullptr;
    }
};

TEST_F(GatherProperties, gather_bonds_newton_on)
{
    if (!lammps_has_style(lmp, "atom", "full")) GTEST_SKIP();
    std::string input = path_join(INPUT_DIR, "in.fourmol");
    if (!verbose) ::testing::internal::CaptureStdout();
    lammps_command(lmp, "newton on on");
    lammps_file(lmp, input.c_str());
    if (!verbose) ::testing::internal::GetCapturedStdout();

    bigint nbonds = *(bigint *)lammps_extract_global(lmp, "nbonds");
    EXPECT_EQ(nbonds, 24);

    auto *bonds = new tagint[3 * nbonds];
    lammps_gather_bonds(lmp, bonds);

#define CHECK_BOND(idx, type, atom1, atom2)                                 \
    if (((bonds[3 * idx + 1] == atom1) && (bonds[3 * idx + 2] == atom2)) || \
        ((bonds[3 * idx + 1] == atom2) && (bonds[3 * idx + 2] == atom1))) { \
        EXPECT_EQ(bonds[3 * idx], type);                                    \
        ++count;                                                            \
    }

    // check validity of a few bonds by comparing the bond type and counting the matches.
    int count = 0;
    for (bigint i = 0; i < nbonds; ++i) {
        CHECK_BOND(i, 5, 1, 2);
        CHECK_BOND(i, 3, 1, 3);
        CHECK_BOND(i, 2, 3, 4);
        CHECK_BOND(i, 2, 3, 5);
        CHECK_BOND(i, 1, 3, 6);
        CHECK_BOND(i, 3, 6, 8);
        CHECK_BOND(i, 4, 6, 7);
        CHECK_BOND(i, 5, 8, 9);
        CHECK_BOND(i, 5, 27, 28);
        CHECK_BOND(i, 5, 27, 29);
    }
    EXPECT_EQ(count, 10);
    delete[] bonds;
#undef CHECK_BOND
};

TEST_F(GatherProperties, gather_bonds_newton_off)
{
    if (!lammps_has_style(lmp, "atom", "full")) GTEST_SKIP();
    std::string input = path_join(INPUT_DIR, "in.fourmol");
    if (!verbose) ::testing::internal::CaptureStdout();
    lammps_command(lmp, "newton off off");
    lammps_file(lmp, input.c_str());
    if (!verbose) ::testing::internal::GetCapturedStdout();

    bigint nbonds = *(bigint *)lammps_extract_global(lmp, "nbonds");
    EXPECT_EQ(nbonds, 24);

    auto *bonds = new tagint[3 * nbonds];
    lammps_gather_bonds(lmp, bonds);

#define CHECK_BOND(idx, type, atom1, atom2)                                 \
    if (((bonds[3 * idx + 1] == atom1) && (bonds[3 * idx + 2] == atom2)) || \
        ((bonds[3 * idx + 1] == atom2) && (bonds[3 * idx + 2] == atom1))) { \
        EXPECT_EQ(bonds[3 * idx], type);                                    \
        ++count;                                                            \
    }

    // check validity of a few bonds by comparing the bond type and counting the matches.
    int count = 0;
    for (bigint i = 0; i < nbonds; ++i) {
        CHECK_BOND(i, 5, 1, 2);
        CHECK_BOND(i, 3, 1, 3);
        CHECK_BOND(i, 2, 3, 4);
        CHECK_BOND(i, 2, 3, 5);
        CHECK_BOND(i, 1, 3, 6);
        CHECK_BOND(i, 3, 6, 8);
        CHECK_BOND(i, 4, 6, 7);
        CHECK_BOND(i, 5, 8, 9);
        CHECK_BOND(i, 5, 27, 28);
        CHECK_BOND(i, 5, 27, 29);
    }
    EXPECT_EQ(count, 10);
    delete[] bonds;
#undef CHECK_BOND
};

TEST_F(GatherProperties, gather_angles_newton_on)
{
    if (!lammps_has_style(lmp, "atom", "full")) GTEST_SKIP();
    std::string input = path_join(INPUT_DIR, "in.fourmol");
    if (!verbose) ::testing::internal::CaptureStdout();
    lammps_command(lmp, "newton on on");
    lammps_file(lmp, input.c_str());
    if (!verbose) ::testing::internal::GetCapturedStdout();

    bigint nangles = *(bigint *)lammps_extract_global(lmp, "nangles");
    EXPECT_EQ(nangles, 30);

    auto *angles = new tagint[4 * nangles];
    lammps_gather_angles(lmp, angles);

#define CHECK_ANGLE(idx, type, atom1, atom2, atom3)                          \
    if (((angles[4 * idx + 1] == atom1) && (angles[4 * idx + 2] == atom2) && \
         (angles[4 * idx + 3] == atom3)) ||                                  \
        ((angles[4 * idx + 1] == atom3) && (angles[4 * idx + 2] == atom2) && \
         (angles[4 * idx + 3] == atom1))) {                                  \
        EXPECT_EQ(angles[4 * idx], type);                                    \
        ++count;                                                             \
    }

    // check validity of a few angles by comparing the angle type and counting the matches.
    int count = 0;
    for (bigint i = 0; i < nangles; ++i) {
        CHECK_ANGLE(i, 4, 2, 1, 3);
        CHECK_ANGLE(i, 4, 1, 3, 5);
        CHECK_ANGLE(i, 4, 1, 3, 4);
        CHECK_ANGLE(i, 4, 13, 12, 15);
        CHECK_ANGLE(i, 4, 13, 12, 14);
        CHECK_ANGLE(i, 2, 5, 3, 6);
        CHECK_ANGLE(i, 2, 4, 3, 6);
        CHECK_ANGLE(i, 3, 3, 6, 7);
        CHECK_ANGLE(i, 3, 3, 6, 8);
        CHECK_ANGLE(i, 1, 22, 21, 23);
    }
    EXPECT_EQ(count, 10);
    delete[] angles;
#undef CHECK_ANGLE
};

TEST_F(GatherProperties, gather_angles_newton_off)
{
    if (!lammps_has_style(lmp, "atom", "full")) GTEST_SKIP();
    std::string input = path_join(INPUT_DIR, "in.fourmol");
    if (!verbose) ::testing::internal::CaptureStdout();
    lammps_command(lmp, "newton off off");
    lammps_file(lmp, input.c_str());
    if (!verbose) ::testing::internal::GetCapturedStdout();

    bigint nangles = *(bigint *)lammps_extract_global(lmp, "nangles");
    EXPECT_EQ(nangles, 30);

    auto *angles = new tagint[4 * nangles];
    lammps_gather_angles(lmp, angles);

#define CHECK_ANGLE(idx, type, atom1, atom2, atom3)                          \
    if (((angles[4 * idx + 1] == atom1) && (angles[4 * idx + 2] == atom2) && \
         (angles[4 * idx + 3] == atom3)) ||                                  \
        ((angles[4 * idx + 1] == atom3) && (angles[4 * idx + 2] == atom2) && \
         (angles[4 * idx + 3] == atom1))) {                                  \
        EXPECT_EQ(angles[4 * idx], type);                                    \
        ++count;                                                             \
    }

    // check validity of a few angles by comparing the angle type and counting the matches.
    int count = 0;
    for (bigint i = 0; i < nangles; ++i) {
        CHECK_ANGLE(i, 4, 2, 1, 3);
        CHECK_ANGLE(i, 4, 1, 3, 5);
        CHECK_ANGLE(i, 4, 1, 3, 4);
        CHECK_ANGLE(i, 4, 13, 12, 15);
        CHECK_ANGLE(i, 4, 13, 12, 14);
        CHECK_ANGLE(i, 2, 5, 3, 6);
        CHECK_ANGLE(i, 2, 4, 3, 6);
        CHECK_ANGLE(i, 3, 3, 6, 7);
        CHECK_ANGLE(i, 3, 3, 6, 8);
        CHECK_ANGLE(i, 1, 22, 21, 23);
    }
    EXPECT_EQ(count, 10);
    delete[] angles;
#undef CHECK_ANGLES
};

TEST_F(GatherProperties, gather_dihedrals_newton_on)
{
    if (!lammps_has_style(lmp, "atom", "full")) GTEST_SKIP();
    std::string input = path_join(INPUT_DIR, "in.fourmol");
    if (!verbose) ::testing::internal::CaptureStdout();
    lammps_command(lmp, "newton on on");
    lammps_file(lmp, input.c_str());
    if (!verbose) ::testing::internal::GetCapturedStdout();

    bigint ndihedrals = *(bigint *)lammps_extract_global(lmp, "ndihedrals");
    EXPECT_EQ(ndihedrals, 31);

    auto *dihedrals = new tagint[5 * ndihedrals];
    lammps_gather_dihedrals(lmp, dihedrals);

#define CHECK_DIHEDRAL(idx, type, atom1, atom2, atom3, atom4)                     \
    if ((dihedrals[5 * idx + 1] == atom1) && (dihedrals[5 * idx + 2] == atom2) && \
        (dihedrals[5 * idx + 3] == atom3) && (dihedrals[5 * idx + 4] == atom4)) { \
        EXPECT_EQ(dihedrals[5 * idx], type);                                      \
        ++count;                                                                  \
    }

    // check validity of a few dihedrals by comparing the dihedral type and counting the matches.
    int count = 0;
    for (bigint i = 0; i < ndihedrals; ++i) {
        CHECK_DIHEDRAL(i, 2, 2, 1, 3, 6);
        CHECK_DIHEDRAL(i, 2, 2, 1, 3, 4);
        CHECK_DIHEDRAL(i, 3, 2, 1, 3, 5);
        CHECK_DIHEDRAL(i, 1, 1, 3, 6, 8);
        CHECK_DIHEDRAL(i, 1, 1, 3, 6, 7);
        CHECK_DIHEDRAL(i, 5, 4, 3, 6, 8);
        CHECK_DIHEDRAL(i, 5, 4, 3, 6, 7);
        CHECK_DIHEDRAL(i, 5, 16, 10, 12, 13);
        CHECK_DIHEDRAL(i, 5, 16, 10, 12, 14);
        CHECK_DIHEDRAL(i, 5, 16, 10, 12, 15);
    }
    EXPECT_EQ(count, 10);
    delete[] dihedrals;
#undef CHECK_DIHEDRAL
};

TEST_F(GatherProperties, gather_dihedrals_newton_off)
{
    if (!lammps_has_style(lmp, "atom", "full")) GTEST_SKIP();
    std::string input = path_join(INPUT_DIR, "in.fourmol");
    if (!verbose) ::testing::internal::CaptureStdout();
    lammps_command(lmp, "newton off off");
    lammps_file(lmp, input.c_str());
    if (!verbose) ::testing::internal::GetCapturedStdout();

    bigint ndihedrals = *(bigint *)lammps_extract_global(lmp, "ndihedrals");
    EXPECT_EQ(ndihedrals, 31);

    auto *dihedrals = new tagint[5 * ndihedrals];
    lammps_gather_dihedrals(lmp, dihedrals);

#define CHECK_DIHEDRAL(idx, type, atom1, atom2, atom3, atom4)                     \
    if ((dihedrals[5 * idx + 1] == atom1) && (dihedrals[5 * idx + 2] == atom2) && \
        (dihedrals[5 * idx + 3] == atom3) && (dihedrals[5 * idx + 4] == atom4)) { \
        EXPECT_EQ(dihedrals[5 * idx], type);                                      \
        ++count;                                                                  \
    }
    // check validity of a few dihedrals by comparing the dihedral type and counting the matches.
    int count = 0;
    for (bigint i = 0; i < ndihedrals; ++i) {
        CHECK_DIHEDRAL(i, 2, 2, 1, 3, 6);
        CHECK_DIHEDRAL(i, 2, 2, 1, 3, 4);
        CHECK_DIHEDRAL(i, 3, 2, 1, 3, 5);
        CHECK_DIHEDRAL(i, 1, 1, 3, 6, 8);
        CHECK_DIHEDRAL(i, 1, 1, 3, 6, 7);
        CHECK_DIHEDRAL(i, 5, 4, 3, 6, 8);
        CHECK_DIHEDRAL(i, 5, 4, 3, 6, 7);
        CHECK_DIHEDRAL(i, 5, 16, 10, 12, 13);
        CHECK_DIHEDRAL(i, 5, 16, 10, 12, 14);
        CHECK_DIHEDRAL(i, 5, 16, 10, 12, 15);
    }
    EXPECT_EQ(count, 10);
    delete[] dihedrals;
#undef CHECK_DIHEDRALS
};

TEST_F(GatherProperties, gather_impropers_newton_on)
{
    if (!lammps_has_style(lmp, "atom", "full")) GTEST_SKIP();
    std::string input = path_join(INPUT_DIR, "in.fourmol");
    if (!verbose) ::testing::internal::CaptureStdout();
    lammps_command(lmp, "newton on on");
    lammps_file(lmp, input.c_str());
    if (!verbose) ::testing::internal::GetCapturedStdout();

    bigint nimpropers = *(bigint *)lammps_extract_global(lmp, "nimpropers");
    EXPECT_EQ(nimpropers, 2);

    auto *impropers = new tagint[5 * nimpropers];
    lammps_gather_impropers(lmp, impropers);

#define CHECK_IMPROPER(idx, type, atom1, atom2, atom3, atom4)                     \
    if ((impropers[5 * idx + 1] == atom1) && (impropers[5 * idx + 2] == atom2) && \
        (impropers[5 * idx + 3] == atom3) && (impropers[5 * idx + 4] == atom4)) { \
        EXPECT_EQ(impropers[5 * idx], type);                                      \
        ++count;                                                                  \
    }

    // check validity of a few impropers by comparing the improper type and counting the matches.
    int count = 0;
    for (bigint i = 0; i < nimpropers; ++i) {
        CHECK_IMPROPER(i, 1, 6, 3, 8, 7);
        CHECK_IMPROPER(i, 2, 8, 6, 10, 9);
    }
    EXPECT_EQ(count, 2);
    delete[] impropers;
#undef CHECK_IMPROPER
};

TEST_F(GatherProperties, gather_impropers_newton_off)
{
    if (!lammps_has_style(lmp, "atom", "full")) GTEST_SKIP();
    std::string input = path_join(INPUT_DIR, "in.fourmol");
    if (!verbose) ::testing::internal::CaptureStdout();
    lammps_command(lmp, "newton off off");
    lammps_file(lmp, input.c_str());
    if (!verbose) ::testing::internal::GetCapturedStdout();

    bigint nimpropers = *(bigint *)lammps_extract_global(lmp, "nimpropers");
    EXPECT_EQ(nimpropers, 2);

    auto *impropers = new tagint[5 * nimpropers];
    lammps_gather_impropers(lmp, impropers);

#define CHECK_IMPROPER(idx, type, atom1, atom2, atom3, atom4)                     \
    if ((impropers[5 * idx + 1] == atom1) && (impropers[5 * idx + 2] == atom2) && \
        (impropers[5 * idx + 3] == atom3) && (impropers[5 * idx + 4] == atom4)) { \
        EXPECT_EQ(impropers[5 * idx], type);                                      \
        ++count;                                                                  \
    }
    // check validity of a few impropers by comparing the improper type and counting the matches.
    int count = 0;
    for (bigint i = 0; i < nimpropers; ++i) {
        CHECK_IMPROPER(i, 1, 6, 3, 8, 7);
        CHECK_IMPROPER(i, 2, 8, 6, 10, 9);
    }
    EXPECT_EQ(count, 2);
    delete[] impropers;
#undef CHECK_IMPROPERS
};
