// SPDX-License-Identifier: LGPL-2.1-or-later

#include <FCConfig.h>

#include <Base/Reader.h>
#include <Base/Writer.h>
#include <Mod/Sketcher/App/Constraint.h>

#include <gtest/gtest.h>
#include <xercesc/util/PlatformUtils.hpp>
#include <QTemporaryFile>

#include <src/App/InitApplication.h>


// Ensure Xerces is initialized before running tests which uses xml
class XercesEnvironment: public ::testing::Environment
{
public:
    void SetUp() override
    {
        try {
            xercesc::XMLPlatformUtils::Initialize();
        }
        catch (const xercesc::XMLException& e) {
            FAIL() << "Xerces init failed: " << xercesc::XMLString::transcode(e.getMessage());
        }
    }

    void TearDown() override
    {
        xercesc::XMLPlatformUtils::Terminate();
    }
};

::testing::Environment* const xercesEnv = ::testing::AddGlobalTestEnvironment(new XercesEnvironment);

class ConstraintPointsAccess: public ::testing::Test
{
protected:
    static void SetUpTestSuite()
    {
        tests::initApplication();
    }
};

TEST_F(ConstraintPointsAccess, testDefaultGeoElementIdsAreSane)  // NOLINT
{
    // Arrange
    auto constraint = Sketcher::Constraint();

    // Act - no action needed, we are testing the default state

    // Assert
#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS
    // Old way of accessing elements
    EXPECT_EQ(constraint.First, Sketcher::GeoEnum::GeoUndef);
    EXPECT_EQ(constraint.FirstPos, Sketcher::PointPos::none);

    EXPECT_EQ(constraint.Second, Sketcher::GeoEnum::GeoUndef);
    EXPECT_EQ(constraint.SecondPos, Sketcher::PointPos::none);

    EXPECT_EQ(constraint.Third, Sketcher::GeoEnum::GeoUndef);
    EXPECT_EQ(constraint.ThirdPos, Sketcher::PointPos::none);

    // New way of accessing elements
#endif
    EXPECT_EQ(
        constraint.getElement(0),
        Sketcher::GeoElementId(Sketcher::GeoEnum::GeoUndef, Sketcher::PointPos::none)
    );
    EXPECT_EQ(
        constraint.getElement(1),
        Sketcher::GeoElementId(Sketcher::GeoEnum::GeoUndef, Sketcher::PointPos::none)
    );
    EXPECT_EQ(
        constraint.getElement(2),
        Sketcher::GeoElementId(Sketcher::GeoEnum::GeoUndef, Sketcher::PointPos::none)
    );
}

#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS
TEST_F(ConstraintPointsAccess, testOldWriteIsReadByNew)  // NOLINT
{
    // Arrange
    auto constraint = Sketcher::Constraint();

    // Act
    constraint.First = 23;
    constraint.FirstPos = Sketcher::PointPos::start;
    constraint.Second = 34;
    constraint.SecondPos = Sketcher::PointPos::end;
    constraint.Third = 45;
    constraint.ThirdPos = Sketcher::PointPos::mid;

    // Assert
    EXPECT_EQ(
        constraint.getElement(0),
        Sketcher::GeoElementId(Sketcher::GeoElementId(23, Sketcher::PointPos::start))
    );
    EXPECT_EQ(
        constraint.getElement(1),
        Sketcher::GeoElementId(Sketcher::GeoElementId(34, Sketcher::PointPos::end))
    );
    EXPECT_EQ(
        constraint.getElement(2),
        Sketcher::GeoElementId(Sketcher::GeoElementId(45, Sketcher::PointPos::mid))
    );
}

TEST_F(ConstraintPointsAccess, testNewWriteIsReadByOld)  // NOLINT
{
    // Arrange
    auto constraint = Sketcher::Constraint();

    // Act
    constraint.setElement(0, Sketcher::GeoElementId(23, Sketcher::PointPos::start));
    constraint.setElement(1, Sketcher::GeoElementId(34, Sketcher::PointPos::end));
    constraint.setElement(2, Sketcher::GeoElementId(45, Sketcher::PointPos::mid));

    // Assert
    EXPECT_EQ(constraint.First, 23);
    EXPECT_EQ(constraint.FirstPos, Sketcher::PointPos::start);
    EXPECT_EQ(constraint.Second, 34);
    EXPECT_EQ(constraint.SecondPos, Sketcher::PointPos::end);
    EXPECT_EQ(constraint.Third, 45);
    EXPECT_EQ(constraint.ThirdPos, Sketcher::PointPos::mid);
}
#endif

TEST_F(ConstraintPointsAccess, testThreeElementsByDefault)  // NOLINT
{
    // Arrange
    auto constraint = Sketcher::Constraint();

    // Act - no action needed, we are testing the default state

    // Assert
    EXPECT_EQ(constraint.getElementsSize(), 3);
}

TEST_F(ConstraintPointsAccess, testFourElementsWhenAddingOne)  // NOLINT
{
    // Arrange
    auto constraint = Sketcher::Constraint();

    // Act
    constraint.addElement(Sketcher::GeoElementId(1, Sketcher::PointPos::start));

    // Assert
    EXPECT_EQ(constraint.getElementsSize(), 4);
}

#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS
TEST_F(ConstraintPointsAccess, testElementSerializationWhenAccessingOldWay)  // NOLINT
{
    // Arrange
    auto constraint = Sketcher::Constraint();

    // Act
    constraint.First = 23;
    constraint.FirstPos = Sketcher::PointPos::start;
    constraint.Second = 34;
    constraint.SecondPos = Sketcher::PointPos::end;
    constraint.Third = 45;
    constraint.ThirdPos = Sketcher::PointPos::mid;

    Base::StringWriter writer = {};
    constraint.Save(writer);

    // Assert
    std::string serialized = writer.getString();
    EXPECT_TRUE(serialized.find("First=\"23\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("FirstPos=\"1\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("Second=\"34\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("SecondPos=\"2\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("Third=\"45\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("ThirdPos=\"3\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("ElementIds=\"23 34 45\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("ElementPositions=\"1 2 3\"") != std::string::npos);
}
#endif

TEST_F(ConstraintPointsAccess, testElementSerializationWhenAccessingNewWay)  // NOLINT
{
    // Arrange
    auto constraint = Sketcher::Constraint();

    // Act
    constraint.setElement(0, Sketcher::GeoElementId(23, Sketcher::PointPos::start));
    constraint.setElement(1, Sketcher::GeoElementId(34, Sketcher::PointPos::end));
    constraint.setElement(2, Sketcher::GeoElementId(45, Sketcher::PointPos::mid));

    Base::StringWriter writer = {};
    constraint.Save(writer);

    // Assert
    std::string serialized = writer.getString();
    EXPECT_TRUE(serialized.find("First=\"23\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("FirstPos=\"1\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("Second=\"34\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("SecondPos=\"2\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("Third=\"45\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("ThirdPos=\"3\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("ElementIds=\"23 34 45\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("ElementPositions=\"1 2 3\"") != std::string::npos);
}

#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS
TEST_F(ConstraintPointsAccess, testElementSerializationWhenMixingOldAndNew)  // NOLINT
{
    // Arrange
    auto constraint = Sketcher::Constraint();

    // Act
    constraint.setElement(0, Sketcher::GeoElementId(23, Sketcher::PointPos::start));
    constraint.setElement(1, Sketcher::GeoElementId(34, Sketcher::PointPos::end));
    constraint.Second = 45;  // Old way
    constraint.SecondPos = Sketcher::PointPos::mid;

    Base::StringWriter writer = {};
    constraint.Save(writer);

    // Assert
    std::string serialized = writer.getString();
    EXPECT_TRUE(serialized.find("First=\"23\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("FirstPos=\"1\"") != std::string::npos);

    // Old way wrote this data
    // ensure mid is 3 for next test
    EXPECT_EQ(Sketcher::PointPos::mid, static_cast<Sketcher::PointPos>(3));
    EXPECT_TRUE(serialized.find("SecondPos=\"3\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("Second=\"45\"") != std::string::npos);

    EXPECT_TRUE(serialized.find("Third=\"-2000\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("ThirdPos=\"0\"") != std::string::npos);

    // Second and SecondPos is reflected in the elements data too
    EXPECT_TRUE(serialized.find("ElementIds=\"23 45 -2000\"") != std::string::npos);
    EXPECT_TRUE(serialized.find("ElementPositions=\"1 3 0\"") != std::string::npos);
}
#endif

TEST_F(ConstraintPointsAccess, testElementsRestoredFromSerialization)  // NOLINT
{
    // Arrange
    Sketcher::Constraint constraint;
    constraint.setElement(0, Sketcher::GeoElementId(23, Sketcher::PointPos::start));
    constraint.setElement(1, Sketcher::GeoElementId(34, Sketcher::PointPos::end));
    constraint.setElement(2, Sketcher::GeoElementId(45, Sketcher::PointPos::mid));

    Base::StringWriter writer;
    writer.Stream() << "<root>\n";  // Wrap in a root element to make constraint.Save happy
    constraint.Save(writer);
    writer.Stream() << "</root>";

    // Write to temporary file
    QTemporaryFile tempFile;
    tempFile.setAutoRemove(true);
    ASSERT_TRUE(tempFile.open());
    tempFile.write(writer.getString().c_str(), writer.getString().size());
    tempFile.flush();

    // Open with std::ifstream and parse
    std::string filename = tempFile.fileName().toStdString();
    std::ifstream inputFile(filename);
    ASSERT_TRUE(inputFile.is_open());

    Base::XMLReader reader(tempFile.fileName().toStdString().c_str(), inputFile);
    Sketcher::Constraint restoredConstraint;
    restoredConstraint.Restore(reader);

    // Assert
    EXPECT_EQ(restoredConstraint.getElement(0), Sketcher::GeoElementId(23, Sketcher::PointPos::start));
    EXPECT_EQ(restoredConstraint.getElement(1), Sketcher::GeoElementId(34, Sketcher::PointPos::end));
    EXPECT_EQ(restoredConstraint.getElement(2), Sketcher::GeoElementId(45, Sketcher::PointPos::mid));

    inputFile.close();
}

TEST_F(
    ConstraintPointsAccess,
    testElementsRestoredFromSerializationWithoutNewElementStorage
)  // NOLINT
{
    // Arrange

    // Manually craft a serialized version, only parts in "{}" are important.
    // New way of storing elements is not present, like if it is an older file.
    std::string serializedConstraint = fmt::format(
        "<Constrain "
        R"(Name="" )"
        R"(Type="0" )"
        R"(Value="0" )"
        R"(LabelDistance="10" )"
        R"(LabelPosition="0" )"
        R"(IsDriving="1" )"
        R"(IsInVirtualSpace="0" )"
        R"(IsActive="1" )"

        R"(First="{}" )"
        R"(Second="{}" )"
        R"(Third="{}" )"
        R"(FirstPos="{}" )"
        R"(SecondPos="{}" )"
        R"(ThirdPos="{}" )"

        "/>",

        67,
        78,
        89,
        static_cast<int>(Sketcher::PointPos::mid),
        static_cast<int>(Sketcher::PointPos::start),
        static_cast<int>(Sketcher::PointPos::end)
    );

    Base::StringWriter writer;
    auto& stream {writer.Stream()};
    stream << "<root>\n";  // Wrap in a root element to make constraint.
    stream << serializedConstraint;
    stream << "</root>";

    // Write to temporary file
    QTemporaryFile tempFile;
    tempFile.setAutoRemove(true);
    ASSERT_TRUE(tempFile.open());
    tempFile.write(writer.getString().c_str(), writer.getString().size());
    tempFile.flush();

    // Open with std::ifstream and parse
    std::string filename = tempFile.fileName().toStdString();
    std::ifstream inputFile(filename);
    ASSERT_TRUE(inputFile.is_open());

    Base::XMLReader reader(tempFile.fileName().toStdString().c_str(), inputFile);
    Sketcher::Constraint restoredConstraint;
    restoredConstraint.Restore(reader);

    // Assert
    EXPECT_EQ(restoredConstraint.getElement(0), Sketcher::GeoElementId(67, Sketcher::PointPos::mid));
    EXPECT_EQ(restoredConstraint.getElement(1), Sketcher::GeoElementId(78, Sketcher::PointPos::start));
    EXPECT_EQ(restoredConstraint.getElement(2), Sketcher::GeoElementId(89, Sketcher::PointPos::end));

    inputFile.close();
}

TEST_F(
    ConstraintPointsAccess,
    testLegacyIsPreferedDuringSerializationWithoutLegacyElementStorage
)  // NOLINT
{
    // Arrange

    // Manually craft a serialized version, only parts in "{}" are important.
    // Only new way of storing elements is present.
    std::string serializedConstraint = fmt::format(
        "<Constrain "
        R"(Name="" )"
        R"(Type="0" )"
        R"(Value="0" )"
        R"(LabelDistance="10" )"
        R"(LabelPosition="0" )"
        R"(IsDriving="1" )"
        R"(IsInVirtualSpace="0" )"
        R"(IsActive="1" )"

        // New way
        R"(ElementIds="{} {} {}" )"
        R"(ElementPositions="{} {} {}" )"

        "/>",
        // New way data
        23,
        34,
        45,
        static_cast<int>(Sketcher::PointPos::start),
        static_cast<int>(Sketcher::PointPos::end),
        static_cast<int>(Sketcher::PointPos::mid)
    );

    Base::StringWriter writer;
    auto& stream {writer.Stream()};
    stream << "<root>\n";  // Wrap in a root element to make constraint.
    stream << serializedConstraint;
    stream << "</root>";

    // Write to temporary file
    QTemporaryFile tempFile;
    tempFile.setAutoRemove(true);
    ASSERT_TRUE(tempFile.open());
    tempFile.write(writer.getString().c_str(), writer.getString().size());
    tempFile.flush();

    // Open with std::ifstream and parse
    std::string filename = tempFile.fileName().toStdString();
    std::ifstream inputFile(filename);
    ASSERT_TRUE(inputFile.is_open());

    Base::XMLReader reader(tempFile.fileName().toStdString().c_str(), inputFile);
    Sketcher::Constraint restoredConstraint;
    restoredConstraint.Restore(reader);

    // Assert
    EXPECT_EQ(restoredConstraint.getElement(0), Sketcher::GeoElementId(23, Sketcher::PointPos::start));
    EXPECT_EQ(restoredConstraint.getElement(1), Sketcher::GeoElementId(34, Sketcher::PointPos::end));
    EXPECT_EQ(restoredConstraint.getElement(2), Sketcher::GeoElementId(45, Sketcher::PointPos::mid));

    inputFile.close();
}

TEST_F(ConstraintPointsAccess, testLegacyIsPreferedDuringSerializationIfContradicting)  // NOLINT
{
    // Arrange

    // Manually craft a serialized version, only parts in "{}" are important.
    // It is not important if legacy is included before or after, legacy should always be preferred.
    std::string serializedConstraint = fmt::format(
        "<Constrain "
        R"(Name="" )"
        R"(Type="0" )"
        R"(Value="0" )"
        R"(LabelDistance="10" )"
        R"(LabelPosition="0" )"
        R"(IsDriving="1" )"
        R"(IsInVirtualSpace="0" )"
        R"(IsActive="1" )"

        // New way
        R"(ElementIds="{} {} {}" )"
        R"(ElementPositions="{} {} {}" )"

        // Legacy
        R"(First="{}" )"
        R"(Second="{}" )"
        R"(Third="{}" )"
        R"(FirstPos="{}" )"
        R"(SecondPos="{}" )"
        R"(ThirdPos="{}" )"

        "/>",
        // New way data
        23,
        34,
        45,
        static_cast<int>(Sketcher::PointPos::start),
        static_cast<int>(Sketcher::PointPos::end),
        static_cast<int>(Sketcher::PointPos::mid),

        // Contradicting legacy data, this should be preferred if available
        67,
        78,
        89,
        static_cast<int>(Sketcher::PointPos::mid),
        static_cast<int>(Sketcher::PointPos::start),
        static_cast<int>(Sketcher::PointPos::end)
    );

    Base::StringWriter writer;
    auto& stream {writer.Stream()};
    stream << "<root>\n";  // Wrap in a root element to make constraint.
    stream << serializedConstraint;
    stream << "</root>";

    // Write to temporary file
    QTemporaryFile tempFile;
    tempFile.setAutoRemove(true);
    ASSERT_TRUE(tempFile.open());
    tempFile.write(writer.getString().c_str(), writer.getString().size());
    tempFile.flush();

    // Open with std::ifstream and parse
    std::string filename = tempFile.fileName().toStdString();
    std::ifstream inputFile(filename);
    ASSERT_TRUE(inputFile.is_open());

    Base::XMLReader reader(tempFile.fileName().toStdString().c_str(), inputFile);
    Sketcher::Constraint restoredConstraint;
    restoredConstraint.Restore(reader);

    // Assert
    EXPECT_EQ(restoredConstraint.getElement(0), Sketcher::GeoElementId(67, Sketcher::PointPos::mid));
    EXPECT_EQ(restoredConstraint.getElement(1), Sketcher::GeoElementId(78, Sketcher::PointPos::start));
    EXPECT_EQ(restoredConstraint.getElement(2), Sketcher::GeoElementId(89, Sketcher::PointPos::end));

    inputFile.close();
}

TEST_F(ConstraintPointsAccess, testSubstituteIndex)  // NOLINT
{
    // Arrange
    Sketcher::Constraint constraint;
    constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start));
    constraint.setElement(1, Sketcher::GeoElementId(20, Sketcher::PointPos::end));
    constraint.setElement(2, Sketcher::GeoElementId(10, Sketcher::PointPos::mid));  // same GeoId as 0

    // Act
    constraint.substituteIndex(10, 99);

    // Assert
    EXPECT_EQ(constraint.getElement(0), Sketcher::GeoElementId(99, Sketcher::PointPos::start));
    EXPECT_EQ(constraint.getElement(1), Sketcher::GeoElementId(20, Sketcher::PointPos::end));
    EXPECT_EQ(constraint.getElement(2), Sketcher::GeoElementId(99, Sketcher::PointPos::mid));
}

TEST_F(ConstraintPointsAccess, testSubstituteIndexAndPos)  // NOLINT
{
    // Arrange
    Sketcher::Constraint constraint;
    constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start));
    constraint.setElement(1, Sketcher::GeoElementId(20, Sketcher::PointPos::start));
    constraint.setElement(2, Sketcher::GeoElementId(10, Sketcher::PointPos::mid));

    // Act
    constraint.substituteIndexAndPos(10, Sketcher::PointPos::start, 42, Sketcher::PointPos::end);

    // Assert
    EXPECT_EQ(constraint.getElement(0), Sketcher::GeoElementId(42, Sketcher::PointPos::end));
    EXPECT_EQ(constraint.getElement(1), Sketcher::GeoElementId(20, Sketcher::PointPos::start));
    EXPECT_EQ(constraint.getElement(2), Sketcher::GeoElementId(10, Sketcher::PointPos::mid));  // unchanged
}

TEST_F(ConstraintPointsAccess, testInvolvesGeoId)  // NOLINT
{
    // Arrange
    Sketcher::Constraint constraint;
    constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start));
    constraint.setElement(1, Sketcher::GeoElementId(20, Sketcher::PointPos::end));

    // Act & Assert
    EXPECT_TRUE(constraint.involvesGeoId(10));
    EXPECT_TRUE(constraint.involvesGeoId(20));
    EXPECT_FALSE(constraint.involvesGeoId(99));
}

TEST_F(ConstraintPointsAccess, testInvolvesGeoIdAndPosId)  // NOLINT
{
    // Arrange
    Sketcher::Constraint constraint;
    constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start));
    constraint.setElement(1, Sketcher::GeoElementId(20, Sketcher::PointPos::mid));
    constraint.setElement(2, Sketcher::GeoElementId(30, Sketcher::PointPos::end));

    // Act & Assert
    EXPECT_TRUE(constraint.involvesGeoIdAndPosId(10, Sketcher::PointPos::start));
    EXPECT_TRUE(constraint.involvesGeoIdAndPosId(20, Sketcher::PointPos::mid));
    EXPECT_FALSE(constraint.involvesGeoIdAndPosId(20, Sketcher::PointPos::start));
    EXPECT_FALSE(constraint.involvesGeoIdAndPosId(99, Sketcher::PointPos::end));
}

#if SKETCHER_CONSTRAINT_USE_LEGACY_ELEMENTS
TEST_F(ConstraintPointsAccess, testLegacyWriteReflectedInInvolvesAndSubstitute)  // NOLINT
{
    // Arrange
    Sketcher::Constraint constraint;
    constraint.First = 10;
    constraint.FirstPos = Sketcher::PointPos::start;
    constraint.Second = 20;
    constraint.SecondPos = Sketcher::PointPos::end;

    // Act & Assert
    EXPECT_TRUE(constraint.involvesGeoId(10));
    EXPECT_TRUE(constraint.involvesGeoIdAndPosId(20, Sketcher::PointPos::end));

    // Substitute the legacy-indexed element
    constraint.substituteIndex(10, 99);

    // Should now reflect the substituted value
    EXPECT_TRUE(constraint.involvesGeoId(99));
    EXPECT_FALSE(constraint.involvesGeoId(10));
}

TEST_F(ConstraintPointsAccess, testSubstituteUpdatesLegacyFieldsToo)  // NOLINT
{
    // Arrange
    Sketcher::Constraint constraint;
    constraint.setElement(0, Sketcher::GeoElementId(10, Sketcher::PointPos::start));

    // Act
    constraint.substituteIndex(10, 42);

    // Assert
    EXPECT_EQ(constraint.getElement(0), Sketcher::GeoElementId(42, Sketcher::PointPos::start));
    EXPECT_EQ(constraint.First, 42);
    EXPECT_EQ(constraint.FirstPos, Sketcher::PointPos::start);
}
#endif
