/*
 * All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
 * its licensors.
 *
 * For complete copyright and license terms please see the LICENSE at the root of this
 * distribution (the "License"). All use of this software is governed by the License,
 * or, if provided, by the license below or the license accompanying this file. Do not
 * remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *
 */

#include <AzCore/UnitTest/Helpers.h>

#include <AzCore/Interface/Interface.h>
#include <AzCore/UnitTest/UnitTest.h>

#include <UnitTestHelper.h>
#include <TriangleInputHelper.h>

#include <NvCloth/IFabricCooker.h>
#include <System/SystemComponent.h>

namespace NvCloth
{
    namespace Internal
    {
        FabricId ComputeFabricId(
            const AZStd::vector<SimParticleFormat>& particles,
            const AZStd::vector<SimIndexType>& indices,
            const AZ::Vector3& fabricGravity,
            bool useGeodesicTether);
        template <typename T>
        void CopyNvRange(const nv::cloth::Range<const T>& nvRange, AZStd::vector<T>& azVector);
        void CopyCookedData(FabricCookedData::InternalCookedData& azCookedData, const nv::cloth::CookedData& nvCookedData);
        AZStd::optional<FabricCookedData> Cook(
            const AZStd::vector<SimParticleFormat>& particles,
            const AZStd::vector<SimIndexType>& indices,
            const AZ::Vector3& fabricGravity,
            bool useGeodesicTether);
        void WeldVertices(
            const AZStd::vector<SimParticleFormat>& particles,
            const AZStd::vector<SimIndexType>& indices,
            AZStd::vector<SimParticleFormat>& weldedParticles,
            AZStd::vector<SimIndexType>& weldedIndices,
            AZStd::vector<int>& remappedVertices,
            float weldingDistance = AZ::g_fltEps);
        void RemoveStaticTriangles(
            const AZStd::vector<SimParticleFormat>& particles,
            const AZStd::vector<SimIndexType>& indices,
            AZStd::vector<SimParticleFormat>& simplifiedParticles,
            AZStd::vector<SimIndexType>& simplifiedIndices,
            AZStd::vector<int>& remappedVertices);
    } // namespace Internal
} // namespace NvCloth

namespace UnitTest
{
    TEST(NvClothSystem, FactoryCooker_ComputeFabricIdWithNoData_IsValid)
    {
        NvCloth::FabricId fabricId = NvCloth::Internal::ComputeFabricId({}, {}, {}, false);

        EXPECT_TRUE(fabricId.IsValid());
    }

    TEST(NvClothSystem, FactoryCooker_ComputeFabricIdWithData_IsValid)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> particles = {{
            NvCloth::SimParticleFormat(1.0f,0.0f,0.0f,1.0f),
            NvCloth::SimParticleFormat(0.0f,1.0f,0.0f,1.0f),
            NvCloth::SimParticleFormat(0.0f,0.0f,1.0f,1.0f),
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{
            0, 1, 2
        }};
        const AZ::Vector3 gravity(0.0f, 0.0f, -9.8f);
        const bool useGeodesicTether = true;

        NvCloth::FabricId fabricId = NvCloth::Internal::ComputeFabricId(particles, indices, gravity, useGeodesicTether);

        EXPECT_TRUE(fabricId.IsValid());
    }

    TEST(NvClothSystem, FactoryCooker_ComputeFabricIdsWithDifferentGravityParameter_ResultInDifferentIDs)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> particles = {{
            NvCloth::SimParticleFormat(1.0f,0.0f,0.0f,1.0f),
            NvCloth::SimParticleFormat(0.0f,1.0f,0.0f,1.0f),
            NvCloth::SimParticleFormat(0.0f,0.0f,1.0f,1.0f),
        } };
        const AZStd::vector<NvCloth::SimIndexType> indices = {{
            0, 1, 2
        }};
        const AZ::Vector3 gravity(0.0f, 0.0f, -9.8f);
        const bool useGeodesicTether = true;

        NvCloth::FabricId fabricId1 = NvCloth::Internal::ComputeFabricId(particles, indices, gravity, useGeodesicTether);
        NvCloth::FabricId fabricId2 = NvCloth::Internal::ComputeFabricId(particles, indices, 0.5f * gravity, useGeodesicTether);

        EXPECT_TRUE(fabricId1.IsValid());
        EXPECT_TRUE(fabricId2.IsValid());
        EXPECT_NE(fabricId1, fabricId2);
    }

    TEST(NvClothSystem, FactoryCooker_ComputeFabricIdsWithDifferentUseGeodesicTetherParameter_ResultInDifferentIDs)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> particles = {{
            NvCloth::SimParticleFormat(1.0f,0.0f,0.0f,1.0f),
            NvCloth::SimParticleFormat(0.0f,1.0f,0.0f,1.0f),
            NvCloth::SimParticleFormat(0.0f,0.0f,1.0f,1.0f),
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{
            0, 1, 2
        }};
        const AZ::Vector3 gravity(0.0f, 0.0f, -9.8f);
        const bool useGeodesicTether = true;

        NvCloth::FabricId fabricId1 = NvCloth::Internal::ComputeFabricId(particles, indices, gravity, useGeodesicTether);
        NvCloth::FabricId fabricId2 = NvCloth::Internal::ComputeFabricId(particles, indices, gravity, !useGeodesicTether);

        EXPECT_TRUE(fabricId1.IsValid());
        EXPECT_TRUE(fabricId2.IsValid());
        EXPECT_NE(fabricId1, fabricId2);
    }

    TEST(NvClothSystem, FactoryCooker_CopyNvRangeEmpty_CopiedDataIsEmpty)
    {
        const nv::cloth::Range<const int> nvRange;

        AZStd::vector<int> azVector;
        NvCloth::Internal::CopyNvRange(nvRange, azVector);

        EXPECT_TRUE(azVector.empty());
        ExpectEq(azVector, nvRange);
    }

    TEST(NvClothSystem, FactoryCooker_CopyNvRangeU32Data_CopiedDataMatchesSource)
    {
        const AZ::u32 data[] = { 0, 2, 45, 64, 125 };
        const size_t numDataElements = sizeof(data) / sizeof(data[0]);
        const nv::cloth::Range<const AZ::u32> nvRange(data, data + numDataElements);

        AZStd::vector<AZ::u32> azVector;
        NvCloth::Internal::CopyNvRange(nvRange, azVector);

        ExpectEq(azVector, nvRange);
    }

    TEST(NvClothSystem, FactoryCooker_CopyNvRangeFloatData_CopiedDataMatchesSource)
    {
        const float data[] = { 0.0f, 1.5f, -3.4f, 3.1f, 400.0f };
        const size_t numDataElements = sizeof(data) / sizeof(data[0]);
        const nv::cloth::Range<const float> nvRange(data, data + numDataElements);

        AZStd::vector<float> azVector;
        NvCloth::Internal::CopyNvRange(nvRange, azVector);

        ExpectEq(azVector, nvRange);
    }

    TEST(NvClothSystem, FactoryCooker_CopyInternalCookedDataEmpty_CopiedDataIsEmpty)
    {
        nv::cloth::CookedData nvCookedData;
        nvCookedData.mNumParticles = 0;

        NvCloth::FabricCookedData::InternalCookedData azCookedData;
        NvCloth::Internal::CopyCookedData(azCookedData, nvCookedData);

        EXPECT_EQ(azCookedData.m_numParticles, 0);
        EXPECT_TRUE(azCookedData.m_phaseIndices.empty());
        EXPECT_TRUE(azCookedData.m_phaseTypes.empty());
        EXPECT_TRUE(azCookedData.m_sets.empty());
        EXPECT_TRUE(azCookedData.m_restValues.empty());
        EXPECT_TRUE(azCookedData.m_stiffnessValues.empty());
        EXPECT_TRUE(azCookedData.m_indices.empty());
        EXPECT_TRUE(azCookedData.m_anchors.empty());
        EXPECT_TRUE(azCookedData.m_tetherLengths.empty());
        EXPECT_TRUE(azCookedData.m_triangles.empty());
        ExpectEq(azCookedData, nvCookedData);
    }

    TEST(NvClothSystem, FactoryCooker_CopyInternalCookedData_CopiedDataMatchesSource)
    {
        const AZ::u32 data[] = { 0, 2, 45, 64, 125 };
        const size_t numDataElements = sizeof(data) / sizeof(data[0]);

        nv::cloth::CookedData nvCookedData;
        nvCookedData.mNumParticles = 0;

        NvCloth::FabricCookedData::InternalCookedData azCookedData;
        NvCloth::Internal::CopyCookedData(azCookedData, nvCookedData);

        ExpectEq(azCookedData, nvCookedData);
    }

    TEST(NvClothSystem, FactoryCooker_CookEmptyMesh_ReturnsNoData)
    {
        AZ_TEST_START_TRACE_SUPPRESSION;
        EXPECT_TRUE(NvCloth::SystemComponent::CheckLastClothError());

        AZStd::optional<NvCloth::FabricCookedData> fabricCookedData = NvCloth::Internal::Cook({}, {}, {}, false);

        NvCloth::SystemComponent::ResetLastClothError(); // Reset the nvcloth error left in SystemComponent
        AZ_TEST_STOP_TRACE_SUPPRESSION(1); // Expect 1 error

        EXPECT_FALSE(fabricCookedData.has_value());
    }

    TEST(NvClothSystem, FactoryCooker_CookWithIncorrectIndices_ReturnsNoData)
    {
        const AZStd::vector<NvCloth::SimIndexType> incorrectIndices = {{ 0, 1 }}; // Use incorrect number of indices for a triangle (multiple of 3)

        AZ_TEST_START_TRACE_SUPPRESSION;
        EXPECT_TRUE(NvCloth::SystemComponent::CheckLastClothError());

        AZStd::optional<NvCloth::FabricCookedData> fabricCookedData = NvCloth::Internal::Cook({}, incorrectIndices, {}, false);

        NvCloth::SystemComponent::ResetLastClothError(); // Reset the nvcloth error left in SystemComponent
        AZ_TEST_STOP_TRACE_SUPPRESSION(1); // Expect 1 error

        EXPECT_FALSE(fabricCookedData.has_value());
    }

    TEST(NvClothSystem, FactoryCooker_CookTriangle_CooksDataCorrectly)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat(-1.0f, 0.0f, 0.0f, 1.0f),
            NvCloth::SimParticleFormat(1.0f, 0.0f, 0.0f, 1.0f),
            NvCloth::SimParticleFormat(0.0f, 1.0f, 0.0f, 1.0f),
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = { {0, 1, 2} };
        const AZ::Vector3 gravity(0.0f, 0.0f, -9.8f);
        const bool useGeodesicTether = true;

        AZStd::optional<NvCloth::FabricCookedData> fabricCookedData =
            NvCloth::Internal::Cook(vertices, indices, gravity, useGeodesicTether);

        EXPECT_TRUE(fabricCookedData.has_value());
        EXPECT_TRUE(fabricCookedData->m_id.IsValid());
        EXPECT_THAT(fabricCookedData->m_particles, ::testing::Pointwise(ContainerIsCloseTolerance(Tolerance), vertices));
        EXPECT_EQ(fabricCookedData->m_indices, indices);
        EXPECT_THAT(fabricCookedData->m_gravity, IsCloseTolerance(gravity, Tolerance));
        EXPECT_EQ(fabricCookedData->m_useGeodesicTether, useGeodesicTether);
        EXPECT_EQ(fabricCookedData->m_internalData.m_numParticles, vertices.size());
    }

    TEST(NvClothSystem, FactoryCooker_CookTriangleAllStatic_CooksDataCorrectly)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat(-1.0f, 0.0f, 0.0f, 0.0f),
            NvCloth::SimParticleFormat(1.0f, 0.0f, 0.0f, 0.0f),
            NvCloth::SimParticleFormat(0.0f, 1.0f, 0.0f, 0.0f),
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 0, 1, 2 }};
        const AZ::Vector3 gravity(0.0f, 0.0f, -9.8f);
        const bool useGeodesicTether = true;

        AZStd::optional<NvCloth::FabricCookedData> fabricCookedData =
            NvCloth::Internal::Cook(vertices, indices, gravity, useGeodesicTether);

        EXPECT_TRUE(fabricCookedData.has_value());
        EXPECT_TRUE(fabricCookedData->m_id.IsValid());
        EXPECT_THAT(fabricCookedData->m_particles, ::testing::Pointwise(ContainerIsCloseTolerance(Tolerance), vertices));
        EXPECT_EQ(fabricCookedData->m_indices, indices);
        EXPECT_THAT(fabricCookedData->m_gravity, IsCloseTolerance(gravity, Tolerance));
        EXPECT_EQ(fabricCookedData->m_useGeodesicTether, useGeodesicTether);
        EXPECT_EQ(fabricCookedData->m_internalData.m_numParticles, vertices.size());
    }

    TEST(NvClothSystem, FactoryCooker_CookMesh_CooksDataCorrectly)
    {
        const float width = 1.0f;
        const float height = 1.0f;
        const AZ::u32 segmentsX = 10;
        const AZ::u32 segmentsY = 10;
        const AZ::Vector3 gravity(0.0f, 0.0f, -9.8f);
        const bool useGeodesicTether = true;

        const TriangleInput planeXY = CreatePlane(width, height, segmentsX, segmentsY);

        AZStd::optional<NvCloth::FabricCookedData> fabricCookedData =
            NvCloth::Internal::Cook(planeXY.m_vertices, planeXY.m_indices, gravity, useGeodesicTether);

        EXPECT_TRUE(fabricCookedData.has_value());
        EXPECT_TRUE(fabricCookedData->m_id.IsValid());
        EXPECT_THAT(fabricCookedData->m_particles, ::testing::Pointwise(ContainerIsCloseTolerance(Tolerance), planeXY.m_vertices));
        EXPECT_EQ(fabricCookedData->m_indices, planeXY.m_indices);
        EXPECT_THAT(fabricCookedData->m_gravity, IsCloseTolerance(gravity, Tolerance));
        EXPECT_EQ(fabricCookedData->m_useGeodesicTether, useGeodesicTether);
        EXPECT_EQ(fabricCookedData->m_internalData.m_numParticles, planeXY.m_vertices.size());
    }

    TEST(NvClothSystem, FactoryCooker_WeldVerticesEmptyMesh_ReturnsEmptyData)
    {
        AZStd::vector<NvCloth::SimParticleFormat> weldedVertices;
        AZStd::vector<NvCloth::SimIndexType> weldedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::WeldVertices({}, {}, weldedVertices, weldedIndices, remappedVertices);

        EXPECT_TRUE(weldedVertices.empty());
        EXPECT_TRUE(weldedIndices.empty());
        EXPECT_TRUE(remappedVertices.empty());
    }

    TEST(NvClothSystem, FactoryCooker_WeldVerticesTriangle_KeepsLowestInverseMass)
    {
        const AZ::Vector3 vertexPosition(100.2f, 300.2f, -30.62f);
        const size_t expectedSizeAfterWelding = 1;
        const float lowestInverseMass = 0.2f;

        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat::CreateFromVector3AndFloat(vertexPosition, 1.0f),
            NvCloth::SimParticleFormat::CreateFromVector3AndFloat(vertexPosition, lowestInverseMass), // This vertex has the lowest inverse mass
            NvCloth::SimParticleFormat::CreateFromVector3AndFloat(vertexPosition, 0.5f)
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 0, 1, 2 }};

        AZStd::vector<NvCloth::SimParticleFormat> weldedVertices;
        AZStd::vector<NvCloth::SimIndexType> weldedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::WeldVertices(vertices, indices, weldedVertices, weldedIndices, remappedVertices);

        EXPECT_EQ(weldedVertices.size(), expectedSizeAfterWelding);
        EXPECT_THAT(weldedVertices[0].GetAsVector3(), IsCloseTolerance(vertexPosition, Tolerance));
        EXPECT_NEAR(weldedVertices[0].GetW(), lowestInverseMass, Tolerance);
    }

    TEST(NvClothSystem, FactoryCooker_WeldVerticesSquareWithDuplicatedVertices_DuplicatedVerticesAreRemoved)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat(-1.0f, 1.0f, 0.0f, 1.0f),
            NvCloth::SimParticleFormat( 1.0f, 1.0f, 0.0f, 1.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 1.0f),

            NvCloth::SimParticleFormat( 1.0f, 1.0f, 0.0f, 1.0f), // Duplicated vertex
            NvCloth::SimParticleFormat( 1.0f,-1.0f, 0.0f, 1.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 1.0f)  // Duplicated vertex
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 0, 1, 2, 3, 4, 5 }};
        const size_t expectedSizeAfterWelding = vertices.size() - 2;

        AZStd::vector<NvCloth::SimParticleFormat> weldedVertices;
        AZStd::vector<NvCloth::SimIndexType> weldedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::WeldVertices(vertices, indices, weldedVertices, weldedIndices, remappedVertices);

        EXPECT_EQ(weldedVertices.size(), expectedSizeAfterWelding);
        EXPECT_EQ(weldedIndices.size(), indices.size());
        EXPECT_EQ(remappedVertices.size(), vertices.size());

        for (size_t i = 0; i < remappedVertices.size(); ++i)
        {
            EXPECT_GE(remappedVertices[i], 0);
            EXPECT_LT(remappedVertices[i], weldedVertices.size());
            EXPECT_THAT(weldedVertices[remappedVertices[i]], IsCloseTolerance(vertices[i], Tolerance));
        }

        for (size_t i = 0; i < weldedIndices.size(); ++i)
        {
            EXPECT_GE(weldedIndices[i], 0);
            EXPECT_LT(weldedIndices[i], weldedVertices.size());
            EXPECT_EQ(weldedIndices[i], remappedVertices[indices[i]]);
            EXPECT_THAT(weldedVertices[weldedIndices[i]], IsCloseTolerance(vertices[indices[i]], Tolerance));
        }
    }

    TEST(NvClothSystem, FactoryCooker_WeldVerticesTrianglesWithoutDuplicatedVertices_ResultIsTheSame)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat(-1.0f, 1.0f, 0.0f, 1.0f),
            NvCloth::SimParticleFormat(1.0f, 1.0f, 0.0f, 1.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 1.0f),

            NvCloth::SimParticleFormat(1.0f, 1.0f, 1.0f, 1.0f),
            NvCloth::SimParticleFormat(1.0f,-1.0f, 1.0f, 1.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 1.0f, 1.0f)
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 0, 1, 2, 3, 4, 5 }};

        AZStd::vector<NvCloth::SimParticleFormat> weldedVertices;
        AZStd::vector<NvCloth::SimIndexType> weldedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::WeldVertices(vertices, indices, weldedVertices, weldedIndices, remappedVertices);

        // The result after calling WeldVertices is expected to have the same size.
        // The vertices inside will be reordered though due to the welding process.
        EXPECT_EQ(weldedVertices.size(), vertices.size());
        EXPECT_EQ(weldedIndices.size(), indices.size());
        EXPECT_EQ(remappedVertices.size(), vertices.size());

        for (size_t i = 0; i < remappedVertices.size(); ++i)
        {
            EXPECT_GE(remappedVertices[i], 0);
            EXPECT_LT(remappedVertices[i], weldedVertices.size());
            EXPECT_THAT(weldedVertices[remappedVertices[i]], IsCloseTolerance(vertices[i], Tolerance));
        }

        for (size_t i = 0; i < weldedIndices.size(); ++i)
        {
            EXPECT_GE(weldedIndices[i], 0);
            EXPECT_LT(weldedIndices[i], weldedVertices.size());
            EXPECT_EQ(weldedIndices[i], remappedVertices[indices[i]]);
            EXPECT_THAT(weldedVertices[weldedIndices[i]], IsCloseTolerance(vertices[indices[i]], Tolerance));
        }
    }

    TEST(NvClothSystem, FactoryCooker_RemoveStaticTrianglesEmptyMesh_ReturnsEmptyData)
    {
        AZStd::vector<NvCloth::SimParticleFormat> simplifiedVertices;
        AZStd::vector<NvCloth::SimIndexType> simplifiedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::RemoveStaticTriangles({}, {}, simplifiedVertices, simplifiedIndices, remappedVertices);

        EXPECT_TRUE(simplifiedVertices.empty());
        EXPECT_TRUE(simplifiedIndices.empty());
        EXPECT_TRUE(remappedVertices.empty());
    }

    TEST(NvClothSystem, FactoryCooker_RemoveStaticTrianglesWithOneStaticTriangle_RemovesAllVerticesAndIndices)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat(-1.0f, 1.0f, 0.0f, 0.0f),
            NvCloth::SimParticleFormat(1.0f, 1.0f, 0.0f, 0.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 0.0f)
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 0, 1, 2 }};

        AZStd::vector<NvCloth::SimParticleFormat> simplifiedVertices;
        AZStd::vector<NvCloth::SimIndexType> simplifiedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::RemoveStaticTriangles(vertices, indices, simplifiedVertices, simplifiedIndices, remappedVertices);

        EXPECT_TRUE(simplifiedVertices.empty());
        EXPECT_TRUE(simplifiedIndices.empty());
        EXPECT_EQ(remappedVertices.size(), vertices.size());
        for (const auto& remappedVertex : remappedVertices)
        {
            EXPECT_LT(remappedVertex, 0); // Remapping must be negative, meaning vertex has been removed.
        }
    }

    TEST(NvClothSystem, FactoryCooker_RemoveStaticTrianglesWithStaticTriangles_StaticTriangleAndVerticesAreRemoved)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat(-1.0f, 1.0f, 0.0f, 0.0f), // This static vertex will be removed
            NvCloth::SimParticleFormat(1.0f, 1.0f, 0.0f, 0.0f),  // This static vertex will be removed
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 0.0f), // This static vertex will be removed

            NvCloth::SimParticleFormat(1.0f, 1.0f, 1.0f, 1.0f),
            NvCloth::SimParticleFormat(1.0f,-1.0f, 1.0f, 0.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 1.0f, 1.0f)
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 0, 1, 2, 3, 4, 5 }}; // First triangle from the triplet 0,1,2 uses all static vertices and will be removed
        const size_t expectedVerticesSizeAfterSimplification = vertices.size() - 3;
        const size_t expectedIndicesSizeAfterSimplification = indices.size() - 3; // 1 triangles less is 3 indices less.

        AZStd::vector<NvCloth::SimParticleFormat> simplifiedVertices;
        AZStd::vector<NvCloth::SimIndexType> simplifiedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::RemoveStaticTriangles(vertices, indices, simplifiedVertices, simplifiedIndices, remappedVertices);

        EXPECT_EQ(simplifiedVertices.size(), expectedVerticesSizeAfterSimplification);
        EXPECT_EQ(simplifiedIndices.size(), expectedIndicesSizeAfterSimplification);
        EXPECT_EQ(remappedVertices.size(), vertices.size());

        for (size_t i = 0; i < remappedVertices.size(); ++i)
        {
            // The first 3 vertices should have been removed, so the remapping should be negative
            if (i < 3)
            {
                EXPECT_LT(remappedVertices[i], 0);
            }
            else
            {
                EXPECT_GE(remappedVertices[i], 0);
                EXPECT_LT(remappedVertices[i], simplifiedVertices.size());
                EXPECT_THAT(simplifiedVertices[remappedVertices[i]], IsCloseTolerance(vertices[i], Tolerance));
            }
        }

        for (size_t i = 0; i < simplifiedIndices.size(); ++i)
        {
            EXPECT_GE(simplifiedIndices[i], 0);
            EXPECT_LT(simplifiedIndices[i], simplifiedVertices.size());
        }

        for (size_t i = 0; i < indices.size(); ++i)
        {
            int remappedVertex = remappedVertices[indices[i]];
            if (remappedVertex >= 0) // If the vertex has not been removed
            {
                EXPECT_THAT(simplifiedVertices[remappedVertex], IsCloseTolerance(vertices[indices[i]], Tolerance));
            }
        }
    }

    TEST(NvClothSystem, FactoryCooker_RemoveStaticTrianglesWithStaticTrianglesSharedVertices_StaticTriangleAndVerticesAreRemoved)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat(-1.0f, 1.0f, 0.0f, 0.0f), // This static vertex will be removed
            NvCloth::SimParticleFormat(1.0f, 1.0f, 0.0f, 0.0f),  // This static vertex will remain because it's also used in the third triangle too
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 0.0f), // This static vertex will be removed

            NvCloth::SimParticleFormat(1.0f, 1.0f, 1.0f, 1.0f),
            NvCloth::SimParticleFormat(1.0f,-1.0f, 1.0f, 0.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 1.0f, 1.0f)
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 3, 4, 5, 0, 1, 2, 3, 1, 5 }}; // Second triangle from the triplet 0,1,2 uses all static vertices and will be removed
        const size_t expectedVerticesSizeAfterSimplification = vertices.size() - 2;
        const size_t expectedIndicesSizeAfterSimplification = indices.size() - 3; // 1 triangles less is 3 indices less.

        AZStd::vector<NvCloth::SimParticleFormat> simplifiedVertices;
        AZStd::vector<NvCloth::SimIndexType> simplifiedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::RemoveStaticTriangles(vertices, indices, simplifiedVertices, simplifiedIndices, remappedVertices);

        EXPECT_EQ(simplifiedVertices.size(), expectedVerticesSizeAfterSimplification);
        EXPECT_EQ(simplifiedIndices.size(), expectedIndicesSizeAfterSimplification);
        EXPECT_EQ(remappedVertices.size(), vertices.size());

        for (size_t i = 0; i < remappedVertices.size(); ++i)
        {
            // The first and third vertex should have been removed, so the remapping should be negative.
            if (i == 0 || i == 2)
            {
                EXPECT_LT(remappedVertices[i], 0);
            }
            else
            {
                EXPECT_GE(remappedVertices[i], 0);
                EXPECT_LT(remappedVertices[i], simplifiedVertices.size());
                EXPECT_THAT(simplifiedVertices[remappedVertices[i]], IsCloseTolerance(vertices[i], Tolerance));
            }
        }

        for (size_t i = 0; i < simplifiedIndices.size(); ++i)
        {
            EXPECT_GE(simplifiedIndices[i], 0);
            EXPECT_LT(simplifiedIndices[i], simplifiedVertices.size());
        }

        for (size_t i = 0; i < indices.size(); ++i)
        {
            int remappedVertex = remappedVertices[indices[i]];
            if (remappedVertex >= 0) // If the vertex has not been removed
            {
                EXPECT_THAT(simplifiedVertices[remappedVertex], IsCloseTolerance(vertices[indices[i]], Tolerance));
            }
        }
    }

    TEST(NvClothSystem, FactoryCooker_RemoveStaticTrianglesWithNonStaticTriangles_ResultIsTheSame)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = {{
            NvCloth::SimParticleFormat(-1.0f, 1.0f, 0.0f, 0.0f),
            NvCloth::SimParticleFormat(1.0f, 1.0f, 0.0f, 1.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 1.0f),

            NvCloth::SimParticleFormat(1.0f, 1.0f, 1.0f, 1.0f),
            NvCloth::SimParticleFormat(1.0f,-1.0f, 1.0f, 0.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 1.0f, 1.0f)
        }};
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 0, 1, 2, 3, 4, 5 }};

        AZStd::vector<NvCloth::SimParticleFormat> simplifiedVertices;
        AZStd::vector<NvCloth::SimIndexType> simplifiedIndices;
        AZStd::vector<int> remappedVertices;
        NvCloth::Internal::RemoveStaticTriangles(vertices, indices, simplifiedVertices, simplifiedIndices, remappedVertices);

        // The result after calling RemoveStaticTriangles is expected to have the same size.
        // The vertices will be reordered though due to the processing during simplification.
        EXPECT_EQ(simplifiedVertices.size(), vertices.size());
        EXPECT_EQ(simplifiedIndices.size(), indices.size());
        EXPECT_EQ(remappedVertices.size(), vertices.size());

        for (size_t i = 0; i < remappedVertices.size(); ++i)
        {
            EXPECT_GE(remappedVertices[i], 0);
            EXPECT_LT(remappedVertices[i], simplifiedVertices.size());
            EXPECT_THAT(simplifiedVertices[remappedVertices[i]], IsCloseTolerance(vertices[i], Tolerance));
        }

        for (size_t i = 0; i < simplifiedIndices.size(); ++i)
        {
            EXPECT_GE(simplifiedIndices[i], 0);
            EXPECT_LT(simplifiedIndices[i], simplifiedVertices.size());
            EXPECT_EQ(simplifiedIndices[i], remappedVertices[indices[i]]);
            EXPECT_THAT(simplifiedVertices[simplifiedIndices[i]], IsCloseTolerance(vertices[indices[i]], Tolerance));
        }
    }

    TEST(NvClothSystem, FactoryCooker_SimplifiyMeshWithDuplicatedVerticesAndStaticTriangles_DuplicatedVerticesAndStaticTrianglesAreRemoved)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = { {
            NvCloth::SimParticleFormat(-1.0f, 1.0f, 0.0f, 0.0f), // This static vertex will be removed
            NvCloth::SimParticleFormat(1.0f, 1.0f, 0.0f, 0.0f),  // This static vertex will remain because it's also used in the third triangle too
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 0.0f), // This static vertex will be removed

            NvCloth::SimParticleFormat(1.0f, 1.0f, 1.0f, 1.0f),
            NvCloth::SimParticleFormat(1.0f,-1.0f, 1.0f, 0.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 1.0f, 1.0f),

            NvCloth::SimParticleFormat(1.0f, 1.0f, 1.0f, 1.0f), // Duplicated vertex
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 1.0f, 1.0f) // Duplicated vertex
        } };
        const AZStd::vector<NvCloth::SimIndexType> indices = {{ 3, 4, 5, 0, 1, 2, 6, 1, 7 }}; // Second triangle from the triplet 0,1,2 uses all static vertices and will be removed
        const size_t expectedVerticesSizeAfterSimplification = vertices.size() - 4;
        const size_t expectedIndicesSizeAfterSimplification = indices.size() - 3; // 1 triangles less is 3 indices less.
        const bool removeStaticTriangles = true;

        AZStd::vector<NvCloth::SimParticleFormat> simplifiedVertices;
        AZStd::vector<NvCloth::SimIndexType> simplifiedIndices;
        AZStd::vector<int> remappedVertices;
        AZ::Interface<NvCloth::IFabricCooker>::Get()->SimplifyMesh(vertices, indices, simplifiedVertices, simplifiedIndices, remappedVertices, removeStaticTriangles);

        EXPECT_EQ(simplifiedVertices.size(), expectedVerticesSizeAfterSimplification);
        EXPECT_EQ(simplifiedIndices.size(), expectedIndicesSizeAfterSimplification);
        EXPECT_EQ(remappedVertices.size(), vertices.size());

        for (size_t i = 0; i < remappedVertices.size(); ++i)
        {
            // The first and third vertex should have been removed, so the remapping should be negative.
            if (i == 0 || i == 2)
            {
                EXPECT_LT(remappedVertices[i], 0);
            }
            else
            {
                EXPECT_GE(remappedVertices[i], 0);
                EXPECT_LT(remappedVertices[i], simplifiedVertices.size());
                EXPECT_THAT(simplifiedVertices[remappedVertices[i]], IsCloseTolerance(vertices[i], Tolerance));
            }
        }

        for (size_t i = 0; i < simplifiedIndices.size(); ++i)
        {
            EXPECT_GE(simplifiedIndices[i], 0);
            EXPECT_LT(simplifiedIndices[i], simplifiedVertices.size());
        }

        for (size_t i = 0; i < indices.size(); ++i)
        {
            int remappedVertex = remappedVertices[indices[i]];
            if (remappedVertex >= 0) // If the vertex has not been removed
            {
                EXPECT_THAT(simplifiedVertices[remappedVertex], IsCloseTolerance(vertices[indices[i]], Tolerance));
            }
        }
    }

    TEST(NvClothSystem, FactoryCooker_SimplifiyMeshWithoutRemovingStaticTriangles_DuplicatedVerticesRemovedAndStaticTrianglesRemain)
    {
        const AZStd::vector<NvCloth::SimParticleFormat> vertices = { {
            NvCloth::SimParticleFormat(-1.0f, 1.0f, 0.0f, 0.0f), // This static vertex will remain because we won't remove static triangles
            NvCloth::SimParticleFormat(1.0f, 1.0f, 0.0f, 0.0f),  // This static vertex will remain because it's also used in the third triangle too
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 0.0f, 0.0f), // This static vertex will remain because we won't remove static triangles

            NvCloth::SimParticleFormat(1.0f, 1.0f, 1.0f, 1.0f),
            NvCloth::SimParticleFormat(1.0f,-1.0f, 1.0f, 0.0f),
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 1.0f, 1.0f),

            NvCloth::SimParticleFormat(1.0f, 1.0f, 1.0f, 1.0f), // Duplicated vertex
            NvCloth::SimParticleFormat(-1.0f,-1.0f, 1.0f, 1.0f) // Duplicated vertex
        } };
        const AZStd::vector<NvCloth::SimIndexType> indices = { { 3, 4, 5, 0, 1, 2, 6, 1, 7 } }; // Second triangle from the triplet 0,1,2 uses all static vertices and will be removed
        const size_t expectedVerticesSizeAfterSimplification = vertices.size() - 2;
        const size_t expectedIndicesSizeAfterSimplification = indices.size();
        const bool removeStaticTriangles = false;

        AZStd::vector<NvCloth::SimParticleFormat> simplifiedVertices;
        AZStd::vector<NvCloth::SimIndexType> simplifiedIndices;
        AZStd::vector<int> remappedVertices;
        AZ::Interface<NvCloth::IFabricCooker>::Get()->SimplifyMesh(vertices, indices, simplifiedVertices, simplifiedIndices, remappedVertices, removeStaticTriangles);

        EXPECT_EQ(simplifiedVertices.size(), expectedVerticesSizeAfterSimplification);
        EXPECT_EQ(simplifiedIndices.size(), expectedIndicesSizeAfterSimplification);
        EXPECT_EQ(remappedVertices.size(), vertices.size());

        for (size_t i = 0; i < remappedVertices.size(); ++i)
        {
            EXPECT_GE(remappedVertices[i], 0);
            EXPECT_LT(remappedVertices[i], simplifiedVertices.size());
            EXPECT_THAT(simplifiedVertices[remappedVertices[i]], IsCloseTolerance(vertices[i], Tolerance));
        }

        for (size_t i = 0; i < simplifiedIndices.size(); ++i)
        {
            EXPECT_GE(simplifiedIndices[i], 0);
            EXPECT_LT(simplifiedIndices[i], simplifiedVertices.size());
            EXPECT_EQ(simplifiedIndices[i], remappedVertices[indices[i]]);
            EXPECT_THAT(simplifiedVertices[simplifiedIndices[i]], IsCloseTolerance(vertices[indices[i]], Tolerance));
        }
    }
} // namespace UnitTest