/*
 * Copyright (c) Contributors to the Open 3D Engine Project.
 * For complete copyright and license terms please see the LICENSE at the root of this distribution.
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 *
 */

#include <AzTest/AzTest.h>
#include <Common/RPITestFixture.h>
#include <Common/JsonTestUtils.h>
#include <Common/ShaderAssetTestUtils.h>
#include <Common/ErrorMessageFinder.h>
#include <Common/SerializeTester.h>
#include <Material/MaterialAssetTestUtils.h>

#include <Atom/RPI.Reflect/Material/MaterialAsset.h>
#include <Atom/RPI.Edit/Material/MaterialSourceData.h>
#include <Atom/RPI.Edit/Material/MaterialTypeSourceData.h>
#include <Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h>
#include <Atom/RPI.Reflect/Material/MaterialPropertiesLayout.h>
#include <Atom/RPI.Edit/Material/MaterialUtils.h>

#include <AzCore/Math/Vector2.h>
#include <AzCore/Math/Vector3.h>
#include <AzCore/Math/Vector4.h>
#include <AzCore/Math/Color.h>
#include <AzCore/Utils/Utils.h>

#include <AzFramework/IO/LocalFileIO.h>

namespace UnitTest
{
    using namespace AZ;
    using namespace RPI;

    class MaterialSourceDataTests
        : public RPITestFixture
    {
    protected:
        RHI::Ptr<RHI::ShaderResourceGroupLayout> m_testMaterialSrgLayout;
        Data::Asset<ShaderAsset> m_testShaderAsset;
        Data::Asset<MaterialTypeAsset> m_testMaterialTypeAsset;
        Data::Asset<ImageAsset> m_testImageAsset;

        void Reflect(AZ::ReflectContext* context) override
        {
            RPITestFixture::Reflect(context);
            MaterialTypeSourceData::Reflect(context);
            MaterialSourceData::Reflect(context);
        }

        void SetUp() override
        {
            EXPECT_EQ(nullptr, IO::FileIOBase::GetInstance());

            RPITestFixture::SetUp();

            auto localFileIO = AZ::IO::FileIOBase::GetInstance();
            EXPECT_NE(nullptr, localFileIO);
            char rootPath[AZ_MAX_PATH_LEN];
            AZ::Utils::GetExecutableDirectory(rootPath, AZ_MAX_PATH_LEN);
            localFileIO->SetAlias("@exefolder@", rootPath);

            m_testMaterialSrgLayout = CreateCommonTestMaterialSrgLayout();
            m_testShaderAsset = CreateTestShaderAsset(Uuid::CreateRandom(), m_testMaterialSrgLayout);
            m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.shader", m_testShaderAsset.GetId());

            m_testMaterialTypeAsset = CreateTestMaterialTypeAsset(Uuid::CreateRandom());

            // Since this test doesn't actually instantiate a Material, it won't need to instantiate this ImageAsset, so all we
            // need is an asset reference with a valid ID.
            m_testImageAsset = Data::Asset<ImageAsset>{ Data::AssetId{Uuid::CreateRandom(), StreamingImageAsset::GetImageAssetSubId()}, azrtti_typeid<StreamingImageAsset>() };

            // Register the test assets with the AssetSystemStub so CreateMaterialAsset() can use AssetUtils.
            m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.materialtype", m_testMaterialTypeAsset.GetId());
            m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.streamingimage", m_testImageAsset.GetId());
        }

        void TearDown() override
        {
            m_testMaterialTypeAsset.Reset();
            m_testMaterialSrgLayout = nullptr;
            m_testShaderAsset.Reset();
            m_testImageAsset.Reset();

            RPITestFixture::TearDown();
        }

        Data::Asset<MaterialTypeAsset> CreateTestMaterialTypeAsset(Data::AssetId assetId)
        {
            const char* materialTypeJson = R"(
                    {
                        "version": 10,
                        "propertyLayout": {
                            "propertyGroups": [
                                {
                                    "name": "general",
                                    "properties": [
                                        {"name": "MyBool", "type": "bool"},
                                        {"name": "MyInt", "type": "Int"},
                                        {"name": "MyUInt", "type": "UInt"},
                                        {"name": "MyFloat", "type": "Float"},
                                        {"name": "MyFloat2", "type": "Vector2"},
                                        {"name": "MyFloat3", "type": "Vector3"},
                                        {"name": "MyFloat4", "type": "Vector4"},
                                        {"name": "MyColor", "type": "Color"},
                                        {"name": "MyImage", "type": "Image"},
                                        {"name": "MyEnum", "type": "Enum", "enumValues": ["Enum0", "Enum1", "Enum2"], "defaultValue": "Enum0"}
                                    ]
                                }
                            ]
                        },
                        "shaders": [
                            {
                                "file": "@exefolder@/Temp/test.shader"
                            }
                        ],
                        "versionUpdates": [
                            {
                                "toVersion": 2,
                                "actions": [
                                    {"op": "rename", "from": "general.testColorNameA", "to": "general.testColorNameB"}
                                ]
                            },
                            {
                                "toVersion": 4,
                                "actions": [
                                    {"op": "rename", "from": "general.testColorNameB", "to": "general.testColorNameC"}
                                ]
                            },
                            {
                                "toVersion": 6,
                                "actions": [
                                    {"op": "rename", "from": "oldGroup.MyFloat", "to": "general.MyFloat"},
                                    {"op": "rename", "from": "oldGroup.MyIntOldName", "to": "general.MyInt"}
                                ]
                            },
                            {
                                "toVersion": 10,
                                "actions": [
                                    {"op": "rename", "from": "general.testColorNameC", "to": "general.MyColor"}
                                ]
                            }
                        ]
                    }
                )";

            MaterialTypeSourceData materialTypeSourceData;
            LoadTestDataFromJson(materialTypeSourceData, materialTypeJson);
            return materialTypeSourceData.CreateMaterialTypeAsset(assetId).TakeValue();
        }
    };

    void AddPropertyGroup(MaterialSourceData&, AZStd::string_view)
    {
        // Old function, left blank intentionally
    }

    void AddProperty(MaterialSourceData& material, AZStd::string_view groupName, AZStd::string_view propertyName, const MaterialPropertyValue& value)
    {
        MaterialPropertyId id{groupName, propertyName};
        material.SetPropertyValue(id, value);
    }

    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_BasicProperties)
    {
        MaterialSourceData sourceData;

        sourceData.m_materialType = "@exefolder@/Temp/test.materialtype";
        AddPropertyGroup(sourceData, "general");
        AddProperty(sourceData, "general", "MyBool", true);
        AddProperty(sourceData, "general", "MyInt", -10);
        AddProperty(sourceData, "general", "MyUInt", 25u);
        AddProperty(sourceData, "general", "MyFloat", 1.5f);
        AddProperty(sourceData, "general", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f});
        AddProperty(sourceData, "general", "MyFloat2", AZ::Vector2(2.1f, 2.2f));
        AddProperty(sourceData, "general", "MyFloat3", AZ::Vector3(3.1f, 3.2f, 3.3f));
        AddProperty(sourceData, "general", "MyFloat4", AZ::Vector4(4.1f, 4.2f, 4.3f, 4.4f));
        AddProperty(sourceData, "general", "MyImage", AZStd::string("@exefolder@/Temp/test.streamingimage"));
        AddProperty(sourceData, "general", "MyEnum", AZStd::string("Enum1"));

        auto materialAssetOutcome = sourceData.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true);
        EXPECT_TRUE(materialAssetOutcome.IsSuccess());

        Data::Asset<MaterialAsset> materialAsset = materialAssetOutcome.GetValue();

        EXPECT_TRUE(materialAsset->WasPreFinalized());
        EXPECT_EQ(0, materialAsset->GetRawPropertyValues().size()); // A pre-baked material has no need for the original raw property names and values

        // The order here is based on the order in the MaterialTypeSourceData, as added to the MaterialTypeAssetCreator.
        EXPECT_EQ(materialAsset->GetPropertyValues()[0].GetValue<bool>(), true);
        EXPECT_EQ(materialAsset->GetPropertyValues()[1].GetValue<int32_t>(), -10);
        EXPECT_EQ(materialAsset->GetPropertyValues()[2].GetValue<uint32_t>(), 25u);
        EXPECT_EQ(materialAsset->GetPropertyValues()[3].GetValue<float>(), 1.5f);
        EXPECT_EQ(materialAsset->GetPropertyValues()[4].GetValue<Vector2>(), Vector2(2.1f, 2.2f));
        EXPECT_EQ(materialAsset->GetPropertyValues()[5].GetValue<Vector3>(), Vector3(3.1f, 3.2f, 3.3f));
        EXPECT_EQ(materialAsset->GetPropertyValues()[6].GetValue<Vector4>(), Vector4(4.1f, 4.2f, 4.3f, 4.4f));
        EXPECT_EQ(materialAsset->GetPropertyValues()[7].GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));
        EXPECT_EQ(materialAsset->GetPropertyValues()[8].GetValue<Data::Asset<ImageAsset>>(), m_testImageAsset);
        EXPECT_EQ(materialAsset->GetPropertyValues()[9].GetValue<uint32_t>(), 1u);
    }
    
    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_DeferredBake)
    {
        // This test is similar to CreateMaterialAsset_BasicProperties but uses MaterialAssetProcessingMode::DeferredBake instead of PreBake.

        Data::AssetId materialTypeAssetId = Uuid::CreateRandom();

        // This material type asset will be known by the asset system (stub) but doesn't exist in the AssetManager.
        // This demonstrates that the CreateMaterialAsset does not attempt to access the MaterialTypeAsset data in MaterialAssetProcessingMode::DeferredBake.
        m_assetSystemStub.RegisterSourceInfo("testDeferredBake.materialtype", materialTypeAssetId);

        MaterialSourceData sourceData;

        sourceData.m_materialType = "testDeferredBake.materialtype";
        AddPropertyGroup(sourceData, "general");
        AddProperty(sourceData, "general", "MyBool"  , true);
        AddProperty(sourceData, "general", "MyInt"   , -10);
        AddProperty(sourceData, "general", "MyUInt"  , 25u);
        AddProperty(sourceData, "general", "MyFloat" , 1.5f);
        AddProperty(sourceData, "general", "MyColor" , AZ::Color{0.1f, 0.2f, 0.3f, 0.4f});
        AddProperty(sourceData, "general", "MyFloat2", AZ::Vector2(2.1f, 2.2f));
        AddProperty(sourceData, "general", "MyFloat3", AZ::Vector3(3.1f, 3.2f, 3.3f));
        AddProperty(sourceData, "general", "MyFloat4", AZ::Vector4(4.1f, 4.2f, 4.3f, 4.4f));
        AddProperty(sourceData, "general", "MyImage" , AZStd::string("@exefolder@/Temp/test.streamingimage"));
        AddProperty(sourceData, "general", "MyEnum"  , AZStd::string("Enum1"));

        auto materialAssetOutcome = sourceData.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true);
        EXPECT_TRUE(materialAssetOutcome.IsSuccess());

        Data::Asset<MaterialAsset> materialAsset = materialAssetOutcome.GetValue();

        EXPECT_FALSE(materialAsset->WasPreFinalized());

        // Note we avoid calling  GetPropertyValues() because that will auto-finalize the material. We want to check its raw property values first.

        auto findRawPropertyValue = [materialAsset](const char* propertyId)
        {
            auto iter = AZStd::find_if(materialAsset->GetRawPropertyValues().begin(), materialAsset->GetRawPropertyValues().end(), [propertyId](const AZStd::pair<Name, MaterialPropertyValue>& pair)
                {
                    return pair.first == AZ::Name{propertyId};
                });

            if (iter == materialAsset->GetRawPropertyValues().end())
            {
                return MaterialPropertyValue{};
            }
            else
            {
                return iter->second;
            }
        };

        auto checkRawPropertyValues = [findRawPropertyValue, this]()
        {
            EXPECT_EQ(findRawPropertyValue("general.MyBool"  ).GetValue<bool>(), true);
            EXPECT_EQ(findRawPropertyValue("general.MyInt"   ).GetValue<int32_t>(), -10);
            EXPECT_EQ(findRawPropertyValue("general.MyUInt"  ).GetValue<uint32_t>(), 25u);
            EXPECT_EQ(findRawPropertyValue("general.MyFloat" ).GetValue<float>(), 1.5f);
            EXPECT_EQ(findRawPropertyValue("general.MyFloat2").GetValue<Vector2>(), Vector2(2.1f, 2.2f));
            EXPECT_EQ(findRawPropertyValue("general.MyFloat3").GetValue<Vector3>(), Vector3(3.1f, 3.2f, 3.3f));
            EXPECT_EQ(findRawPropertyValue("general.MyFloat4").GetValue<Vector4>(), Vector4(4.1f, 4.2f, 4.3f, 4.4f));
            EXPECT_EQ(findRawPropertyValue("general.MyColor" ).GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));
            EXPECT_EQ(findRawPropertyValue("general.MyImage" ).GetValue<Data::Asset<ImageAsset>>(), m_testImageAsset);
            // The raw value for an enum is the original string, not the numerical value, because the material type holds the necessary metadata to match the name to the value.
            EXPECT_EQ(findRawPropertyValue("general.MyEnum"  ).GetValue<AZStd::string>(), AZStd::string("Enum1")); 
        };

        // We check the raw property values before the material type asset is even available
        checkRawPropertyValues();

        // Now we'll create the material type asset in memory so the material will have what it needs to finalize itself.
        Data::Asset<MaterialTypeAsset> testMaterialTypeAsset = CreateTestMaterialTypeAsset(materialTypeAssetId);

        // The MaterialAsset is still holding an reference to an unloaded asset, so we run it through the serializer which causes the loaded MaterialAsset
        // to have access to the testMaterialTypeAsset. This is similar to how the AP would save the MaterialAsset to the cache and the runtime would load it.
        SerializeTester<RPI::MaterialAsset> tester(GetSerializeContext());
        tester.SerializeOut(materialAsset.Get());
        materialAsset = tester.SerializeIn(Uuid::CreateRandom(), ObjectStream::FilterDescriptor{AZ::Data::AssetFilterNoAssetLoading});

        // We check that the asset is still in the original un-finalized state after going through the serialization process.
        EXPECT_FALSE(materialAsset->WasPreFinalized());
        checkRawPropertyValues();

        // Now all the property values should be available through the main GetPropertyValues() API.
        EXPECT_EQ(materialAsset->GetPropertyValues()[0].GetValue<bool>(), true);
        EXPECT_EQ(materialAsset->GetPropertyValues()[1].GetValue<int32_t>(), -10);
        EXPECT_EQ(materialAsset->GetPropertyValues()[2].GetValue<uint32_t>(), 25u);
        EXPECT_EQ(materialAsset->GetPropertyValues()[3].GetValue<float>(), 1.5f);
        EXPECT_EQ(materialAsset->GetPropertyValues()[4].GetValue<Vector2>(), Vector2(2.1f, 2.2f));
        EXPECT_EQ(materialAsset->GetPropertyValues()[5].GetValue<Vector3>(), Vector3(3.1f, 3.2f, 3.3f));
        EXPECT_EQ(materialAsset->GetPropertyValues()[6].GetValue<Vector4>(), Vector4(4.1f, 4.2f, 4.3f, 4.4f));
        EXPECT_EQ(materialAsset->GetPropertyValues()[7].GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));
        EXPECT_EQ(materialAsset->GetPropertyValues()[8].GetValue<Data::Asset<ImageAsset>>(), m_testImageAsset);
        EXPECT_EQ(materialAsset->GetPropertyValues()[9].GetValue<uint32_t>(), 1u);
        
        // The raw property values are still available (because they are needed if a hot-reload of the MaterialTypeAsset occurs)
        EXPECT_FALSE(materialAsset->WasPreFinalized());
        checkRawPropertyValues();
    }

    // Can return a Vector4 or a Color as a Vector4
    Vector4 GetAsVector4(const MaterialPropertyValue& value)
    {
        if (value.GetTypeId() == azrtti_typeid<Vector4>())
        {
            return value.GetValue<Vector4>();
        }
        else if (value.GetTypeId() == azrtti_typeid<Color>())
        {
            return value.GetValue<Color>().GetAsVector4();
        }
        else
        {
            return Vector4::CreateZero();
        }
    }
    
    // Can return a Int or a UInt as a Int
    int32_t GetAsInt(const MaterialPropertyValue& value)
    {
        if (value.GetTypeId() == azrtti_typeid<int32_t>())
        {
            return value.GetValue<int32_t>();
        }
        else if (value.GetTypeId() == azrtti_typeid<uint32_t>())
        {
            return aznumeric_cast<int32_t>(value.GetValue<uint32_t>());
        }
        else
        {
            return 0;
        }
    }

    template<typename TargetTypeT>
    bool AreTypesCompatible(const MaterialPropertyValue& a, const MaterialPropertyValue& b)
    {
        auto fixupType = [](TypeId t)
        {
            if (t == azrtti_typeid<uint32_t>())
            {
                return azrtti_typeid<int32_t>();
            }

            if (t == azrtti_typeid<Color>())
            {
                return azrtti_typeid<Vector4>();
            }

            return t;
        };

        TypeId targetTypeId = azrtti_typeid<TargetTypeT>();

        return fixupType(a.GetTypeId()) == fixupType(targetTypeId) && fixupType(b.GetTypeId()) == fixupType(targetTypeId);
    }

    void CheckEqual(const MaterialSourceData& a, const MaterialSourceData& b)
    {
        EXPECT_STREQ(a.m_materialType.c_str(), b.m_materialType.c_str());
        EXPECT_STREQ(a.m_description.c_str(), b.m_description.c_str());
        EXPECT_STREQ(a.m_parentMaterial.c_str(), b.m_parentMaterial.c_str());
        EXPECT_EQ(a.m_materialTypeVersion, b.m_materialTypeVersion);

        EXPECT_EQ(a.GetPropertyValues().size(), b.GetPropertyValues().size());

        for (auto& [propertyId, propertyValue] : a.GetPropertyValues())
        {
            if (!b.HasPropertyValue(propertyId))
            {
                EXPECT_TRUE(false) << "Property '" << propertyId.GetCStr() << "' not found in material B";
                continue;
            }

            auto& propertyA = propertyValue;
            auto& propertyB = b.GetPropertyValue(propertyId);

            AZStd::string propertyReference = AZStd::string::format(" for property '%s'", propertyId.GetCStr());
                
            // We allow some types like Vector4 and Color or Int and UInt to be interchangeable since they serialize the same and can be converted when the MaterialAsset is finalized.

            if (AreTypesCompatible<bool>(propertyA, propertyB))
            {
                EXPECT_EQ(propertyA.GetValue<bool>(), propertyB.GetValue<bool>()) << propertyReference.c_str();
            }
            else if (AreTypesCompatible<int32_t>(propertyA, propertyB))
            {
                EXPECT_EQ(GetAsInt(propertyA), GetAsInt(propertyB)) << propertyReference.c_str();
            }
            else if (AreTypesCompatible<float>(propertyA, propertyB))
            {
                EXPECT_NEAR(propertyA.GetValue<float>(),     propertyB.GetValue<float>(), 0.01) << propertyReference.c_str();
            }
            else if (AreTypesCompatible<Vector2>(propertyA, propertyB))
            {
                EXPECT_TRUE(propertyA.GetValue<Vector2>().IsClose(propertyB.GetValue<Vector2>())) << propertyReference.c_str();
            }
            else if (AreTypesCompatible<Vector3>(propertyA, propertyB))
            {
                EXPECT_TRUE(propertyA.GetValue<Vector3>().IsClose(propertyB.GetValue<Vector3>())) << propertyReference.c_str();
            }
            else if (AreTypesCompatible<Vector4>(propertyA, propertyB))
            {
                EXPECT_TRUE(GetAsVector4(propertyA).IsClose(GetAsVector4(propertyB))) << propertyReference.c_str();
            }
            else if (AreTypesCompatible<AZStd::string>(propertyA, propertyB))
            {
                EXPECT_STREQ(propertyA.GetValue<AZStd::string>().c_str(), propertyB.GetValue<AZStd::string>().c_str()) << propertyReference.c_str();
            }
            else
            {
                ADD_FAILURE();
            }
        }
    }

    TEST_F(MaterialSourceDataTests, TestJsonRoundTrip)
    {
        const char* materialTypeFilePath = "@exefolder@/Temp/roundTripTest.materialtype";

        MaterialSourceData sourceDataOriginal;
        sourceDataOriginal.m_materialType = materialTypeFilePath;
        sourceDataOriginal.m_parentMaterial = materialTypeFilePath;
        sourceDataOriginal.m_description = "This is a description";
        sourceDataOriginal.m_materialTypeVersion = 7;
        AddPropertyGroup(sourceDataOriginal, "groupA");
        AddProperty(sourceDataOriginal, "groupA", "MyBool", true);
        AddProperty(sourceDataOriginal, "groupA", "MyInt", -10);
        AddProperty(sourceDataOriginal, "groupA", "MyUInt", 25u);
        AddPropertyGroup(sourceDataOriginal, "groupB");
        AddProperty(sourceDataOriginal, "groupB", "MyFloat", 1.5f);
        AddProperty(sourceDataOriginal, "groupB", "MyFloat2", AZ::Vector2(2.1f, 2.2f));
        AddProperty(sourceDataOriginal, "groupB", "MyFloat3", AZ::Vector3(3.1f, 3.2f, 3.3f));
        AddPropertyGroup(sourceDataOriginal, "groupC");
        AddProperty(sourceDataOriginal, "groupC", "MyFloat4", AZ::Vector4(4.1f, 4.2f, 4.3f, 4.4f));
        AddProperty(sourceDataOriginal, "groupC", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f});
        AddProperty(sourceDataOriginal, "groupC", "MyImage", AZStd::string("@exefolder@/Temp/test.streamingimage"));

        AZStd::string sourceDataSerialized;
        JsonTestResult storeResult = StoreTestDataToJson(sourceDataOriginal, sourceDataSerialized);

        MaterialSourceData sourceDataCopy;
        JsonTestResult loadResult = LoadTestDataFromJson(sourceDataCopy, sourceDataSerialized);

        CheckEqual(sourceDataOriginal, sourceDataCopy);
    }
    
    TEST_F(MaterialSourceDataTests, TestLoadLegacyFormat)
    {
        const AZStd::string inputJson = R"(
            {
                "materialType": "test.materialtype", // Doesn't matter, this isn't loaded
                "properties": {
                    "groupA": {
                        "myBool": true,
                        "myInt": 5,
                        "myFloat": 0.5
                    },
                    "groupB": {
                        "myFloat2": [0.1, 0.2],
                        "myFloat3": [0.3, 0.4, 0.5],
                        "myFloat4": [0.6, 0.7, 0.8, 0.9],
                        "myString": "Hello"
                    }
                }
            }
        )";

        MaterialSourceData material;
        LoadTestDataFromJson(material, inputJson);
        material.ConvertToNewDataFormat();

        MaterialSourceData expectedMaterial;
        expectedMaterial.m_materialType = "test.materialtype";
        expectedMaterial.SetPropertyValue(Name{"groupA.myBool"}, true);
        expectedMaterial.SetPropertyValue(Name{"groupA.myInt"}, 5);
        expectedMaterial.SetPropertyValue(Name{"groupA.myFloat"}, 0.5f);
        expectedMaterial.SetPropertyValue(Name{"groupB.myFloat2"}, Vector2(0.1f, 0.2f));
        expectedMaterial.SetPropertyValue(Name{"groupB.myFloat3"}, Vector3(0.3f, 0.4f, 0.5f));
        expectedMaterial.SetPropertyValue(Name{"groupB.myFloat4"}, Vector4(0.6f, 0.7f, 0.8f, 0.9f));
        expectedMaterial.SetPropertyValue(Name{"groupB.myString"}, AZStd::string{"Hello"});

        CheckEqual(expectedMaterial, material);
    }
    
    TEST_F(MaterialSourceDataTests, TestPropertyValues)
    {
        MaterialSourceData material;
        
        Name foo{"foo"};
        Name bar{"bar"};
        Name baz{"baz"};

        EXPECT_EQ(0, material.GetPropertyValues().size());
        EXPECT_FALSE(material.HasPropertyValue(foo));
        EXPECT_FALSE(material.HasPropertyValue(bar));
        EXPECT_FALSE(material.HasPropertyValue(baz));
        EXPECT_FALSE(material.GetPropertyValue(foo).IsValid());
        EXPECT_FALSE(material.GetPropertyValue(bar).IsValid());
        EXPECT_FALSE(material.GetPropertyValue(baz).IsValid());

        material.SetPropertyValue(Name{"foo"}, 2);
        material.SetPropertyValue(Name{"bar"}, true);
        material.SetPropertyValue(Name{"baz"}, 0.5f);
        
        EXPECT_EQ(3, material.GetPropertyValues().size());
        EXPECT_TRUE(material.HasPropertyValue(foo));
        EXPECT_TRUE(material.HasPropertyValue(bar));
        EXPECT_TRUE(material.HasPropertyValue(baz));
        EXPECT_TRUE(material.GetPropertyValue(foo).IsValid());
        EXPECT_TRUE(material.GetPropertyValue(bar).IsValid());
        EXPECT_TRUE(material.GetPropertyValue(baz).IsValid());
        EXPECT_EQ(material.GetPropertyValue(foo).GetValue<int32_t>(), 2);
        EXPECT_EQ(material.GetPropertyValue(bar).GetValue<bool>(), true);
        EXPECT_EQ(material.GetPropertyValue(baz).GetValue<float>(), 0.5f);

        material.RemovePropertyValue(bar);
        
        EXPECT_EQ(2, material.GetPropertyValues().size());
        EXPECT_TRUE(material.HasPropertyValue(foo));
        EXPECT_FALSE(material.HasPropertyValue(bar));
        EXPECT_TRUE(material.HasPropertyValue(baz));
        EXPECT_TRUE(material.GetPropertyValue(foo).IsValid());
        EXPECT_FALSE(material.GetPropertyValue(bar).IsValid());
        EXPECT_TRUE(material.GetPropertyValue(baz).IsValid());
        EXPECT_EQ(material.GetPropertyValue(foo).GetValue<int32_t>(), 2);
        EXPECT_EQ(material.GetPropertyValue(baz).GetValue<float>(), 0.5f);
    }

    TEST_F(MaterialSourceDataTests, Load_MaterialTypeAfterPropertyList)
    {
        const AZStd::string simpleMaterialTypeJson = R"(
            {
                "propertyLayout": {
                    "propertyGroups":
                    [
                        {
                            "name": "general",
                            "properties": [
                                {
                                    "name": "testValue",
                                    "type": "Float"
                                }
                            ]
                        }
                    ]
                }
            }
        )";

        AZ::Utils::WriteFile(simpleMaterialTypeJson, "@exefolder@/Temp/simpleMaterialType.materialtype");

        // It shouldn't matter whether the materialType field appears before the property value list. This allows for the possibility
        // that customer scripts generate material data and happen to use an unexpected order.
        const AZStd::string inputJson = R"(
        {
            "propertyValues": {
                "general.testValue": 1.2
            },
            "materialType": "@exefolder@/Temp/simpleMaterialType.materialtype"
        }
        )";

        MaterialSourceData material;
        JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson);

        EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask());
        EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing());

        float testValue = material.GetPropertyValue(Name{"general.testValue"}).GetValue<float>();
        EXPECT_FLOAT_EQ(1.2f, testValue);
    }
    
    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_NoMaterialType)
    {
        const AZStd::string inputJson = R"(
            {
                "materialTypeVersion": 1,
                "propertyValues": {
                    "baseColor.color": [1.0,1.0,1.0]
                }
            }
        )";

        MaterialSourceData material;
        JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson);

        const bool elevateWarnings = false;

        ErrorMessageFinder errorMessageFinder;

        errorMessageFinder.AddExpectedErrorMessage("materialType was not specified");
        auto result = material.CreateMaterialAsset(AZ::Uuid::CreateRandom(), "test.material", AZ::RPI::MaterialAssetProcessingMode::DeferredBake, elevateWarnings);
        EXPECT_FALSE(result.IsSuccess());
        errorMessageFinder.CheckExpectedErrorsFound();

        errorMessageFinder.Reset();
        errorMessageFinder.AddExpectedErrorMessage("materialType was not specified");
        result = material.CreateMaterialAsset(AZ::Uuid::CreateRandom(), "test.material", AZ::RPI::MaterialAssetProcessingMode::PreBake, elevateWarnings);
        EXPECT_FALSE(result.IsSuccess());
        errorMessageFinder.CheckExpectedErrorsFound();
        
        errorMessageFinder.Reset();
        errorMessageFinder.AddExpectedErrorMessage("materialType was not specified");
        result = material.CreateMaterialAssetFromSourceData(AZ::Uuid::CreateRandom(), "test.material", elevateWarnings);
        EXPECT_FALSE(result.IsSuccess());
        errorMessageFinder.CheckExpectedErrorsFound();
    }
    
    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MaterialTypeDoesNotExist)
    {
        const AZStd::string inputJson = R"(
            {
                "materialType": "DoesNotExist.materialtype",
                "materialTypeVersion": 1,
                "propertyValues": {
                    "baseColor.color": [1.0,1.0,1.0]
                }
            }
        )";

        MaterialSourceData material;
        JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson);

        const bool elevateWarnings = false;

        ErrorMessageFinder errorMessageFinder;

        errorMessageFinder.AddExpectedErrorMessage("Could not find asset [DoesNotExist.materialtype]");
        auto result = material.CreateMaterialAsset(AZ::Uuid::CreateRandom(), "test.material", AZ::RPI::MaterialAssetProcessingMode::DeferredBake, elevateWarnings);
        EXPECT_FALSE(result.IsSuccess());
        errorMessageFinder.CheckExpectedErrorsFound();

        errorMessageFinder.Reset();
        errorMessageFinder.AddExpectedErrorMessage("Could not find asset [DoesNotExist.materialtype]");
        result = material.CreateMaterialAsset(AZ::Uuid::CreateRandom(), "test.material", AZ::RPI::MaterialAssetProcessingMode::PreBake, elevateWarnings);
        EXPECT_FALSE(result.IsSuccess());
        errorMessageFinder.CheckExpectedErrorsFound();
        
        errorMessageFinder.Reset();
        errorMessageFinder.AddExpectedErrorMessage("Could not find asset [DoesNotExist.materialtype]");
        errorMessageFinder.AddIgnoredErrorMessage("Failed to create material type asset ID", true);
        result = material.CreateMaterialAssetFromSourceData(AZ::Uuid::CreateRandom(), "test.material", elevateWarnings);
        EXPECT_FALSE(result.IsSuccess());
        errorMessageFinder.CheckExpectedErrorsFound();
    }
    
    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MaterialPropertyNotFound)
    {
        MaterialSourceData material;
        material.m_materialType = "@exefolder@/Temp/test.materialtype";
        AddPropertyGroup(material, "general");
        AddProperty(material, "general", "FieldDoesNotExist", 1.5f);
        
        const bool elevateWarnings = true;

        ErrorMessageFinder errorMessageFinder("\"general.FieldDoesNotExist\" is not found");
        errorMessageFinder.AddIgnoredErrorMessage("Failed to build MaterialAsset", true);
        auto result = material.CreateMaterialAsset(AZ::Uuid::CreateRandom(), "test.material", AZ::RPI::MaterialAssetProcessingMode::PreBake, elevateWarnings);
        EXPECT_FALSE(result.IsSuccess());
        errorMessageFinder.CheckExpectedErrorsFound();
    }

    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MultiLevelDataInheritance)
    {
        MaterialSourceData sourceDataLevel1;
        sourceDataLevel1.m_materialType = "@exefolder@/Temp/test.materialtype";
        AddPropertyGroup(sourceDataLevel1, "general");
        AddProperty(sourceDataLevel1, "general", "MyFloat", 1.5f);
        AddProperty(sourceDataLevel1, "general", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f});

        MaterialSourceData sourceDataLevel2;
        sourceDataLevel2.m_materialType = "@exefolder@/Temp/test.materialtype";
        sourceDataLevel2.m_parentMaterial = "level1.material";
        AddPropertyGroup(sourceDataLevel2, "general");
        AddProperty(sourceDataLevel2, "general", "MyColor", AZ::Color{0.15f, 0.25f, 0.35f, 0.45f});
        AddProperty(sourceDataLevel2, "general", "MyFloat2", AZ::Vector2{4.1f, 4.2f});

        MaterialSourceData sourceDataLevel3;
        sourceDataLevel3.m_materialType = "@exefolder@/Temp/test.materialtype";
        sourceDataLevel3.m_parentMaterial = "level2.material";
        AddPropertyGroup(sourceDataLevel3, "general");
        AddProperty(sourceDataLevel3, "general", "MyFloat", 3.5f);

        auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true);
        EXPECT_TRUE(materialAssetLevel1.IsSuccess());
        EXPECT_TRUE(materialAssetLevel1.GetValue()->WasPreFinalized());

        m_assetSystemStub.RegisterSourceInfo("level1.material", materialAssetLevel1.GetValue().GetId());

        auto materialAssetLevel2 = sourceDataLevel2.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true);
        EXPECT_TRUE(materialAssetLevel2.IsSuccess());
        EXPECT_TRUE(materialAssetLevel2.GetValue()->WasPreFinalized());

        m_assetSystemStub.RegisterSourceInfo("level2.material", materialAssetLevel2.GetValue().GetId());

        auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true);
        EXPECT_TRUE(materialAssetLevel3.IsSuccess());
        EXPECT_TRUE(materialAssetLevel3.GetValue()->WasPreFinalized());

        auto layout = m_testMaterialTypeAsset->GetMaterialPropertiesLayout();
        MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat"));
        MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2"));
        MaterialPropertyIndex myColor = layout->FindPropertyIndex(Name("general.MyColor"));

        AZStd::span<const MaterialPropertyValue> properties;

        // Check level 1 properties
        properties = materialAssetLevel1.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(0.0f, 0.0f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));

        // Check level 2 properties
        properties = materialAssetLevel2.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));

        // Check level 3 properties
        properties = materialAssetLevel3.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 3.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));
    }
    
    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MultiLevelDataInheritance_DeferredBake)
    {
        // This test is similar to CreateMaterialAsset_MultiLevelDataInheritance but uses MaterialAssetProcessingMode::DeferredBake instead of PreBake.

        Data::AssetId materialTypeAssetId = Uuid::CreateRandom();

        // This material type asset will be known by the asset system (stub) but doesn't exist in the AssetManager.
        // This demonstrates that the CreateMaterialAsset does not attempt to access the MaterialTypeAsset data in MaterialAssetProcessingMode::DeferredBake.
        m_assetSystemStub.RegisterSourceInfo("testDeferredBake.materialtype", materialTypeAssetId);

        MaterialSourceData sourceDataLevel1;
        sourceDataLevel1.m_materialType = "testDeferredBake.materialtype";
        AddPropertyGroup(sourceDataLevel1, "general");
        AddProperty(sourceDataLevel1, "general", "MyFloat", 1.5f);
        AddProperty(sourceDataLevel1, "general", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f});

        MaterialSourceData sourceDataLevel2;
        sourceDataLevel2.m_materialType = "testDeferredBake.materialtype";
        sourceDataLevel2.m_parentMaterial = "level1.material";
        AddPropertyGroup(sourceDataLevel2, "general");
        AddProperty(sourceDataLevel2, "general", "MyColor", AZ::Color{0.15f, 0.25f, 0.35f, 0.45f});
        AddProperty(sourceDataLevel2, "general", "MyFloat2", AZ::Vector2{4.1f, 4.2f});

        MaterialSourceData sourceDataLevel3;
        sourceDataLevel3.m_materialType = "testDeferredBake.materialtype";
        sourceDataLevel3.m_parentMaterial = "level2.material";
        AddPropertyGroup(sourceDataLevel3, "general");
        AddProperty(sourceDataLevel3, "general", "MyFloat", 3.5f);

        auto materialAssetLevel1Result = sourceDataLevel1.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true);
        EXPECT_TRUE(materialAssetLevel1Result.IsSuccess());
        Data::Asset<MaterialAsset> materialAssetLevel1 = materialAssetLevel1Result.TakeValue();
        EXPECT_FALSE(materialAssetLevel1->WasPreFinalized());

        m_assetSystemStub.RegisterSourceInfo("level1.material", materialAssetLevel1.GetId());

        auto materialAssetLevel2Result = sourceDataLevel2.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true);
        EXPECT_TRUE(materialAssetLevel2Result.IsSuccess());
        Data::Asset<MaterialAsset> materialAssetLevel2 = materialAssetLevel2Result.TakeValue();
        EXPECT_FALSE(materialAssetLevel2->WasPreFinalized());

        m_assetSystemStub.RegisterSourceInfo("level2.material", materialAssetLevel2.GetId());

        auto materialAssetLevel3Result = sourceDataLevel3.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true);
        EXPECT_TRUE(materialAssetLevel3Result.IsSuccess());
        Data::Asset<MaterialAsset> materialAssetLevel3 = materialAssetLevel3Result.TakeValue();
        EXPECT_FALSE(materialAssetLevel3->WasPreFinalized());

        // Now we'll create the material type asset in memory so the materials will have what they need to finalize.
        Data::Asset<MaterialTypeAsset> testMaterialTypeAsset = CreateTestMaterialTypeAsset(materialTypeAssetId);

        auto layout = testMaterialTypeAsset->GetMaterialPropertiesLayout();
        MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat"));
        MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2"));
        MaterialPropertyIndex myColor = layout->FindPropertyIndex(Name("general.MyColor"));

        
        // The MaterialAsset is still holding an reference to an unloaded asset, so we run it through the serializer which causes the loaded MaterialAsset
        // to have access to the testMaterialTypeAsset. This is similar to how the AP would save the MaterialAsset to the cache and the runtime would load it.
        SerializeTester<RPI::MaterialAsset> tester(GetSerializeContext());
        tester.SerializeOut(materialAssetLevel1.Get());
        materialAssetLevel1 = tester.SerializeIn(Uuid::CreateRandom(), ObjectStream::FilterDescriptor{AZ::Data::AssetFilterNoAssetLoading});
        tester.SerializeOut(materialAssetLevel2.Get());
        materialAssetLevel2 = tester.SerializeIn(Uuid::CreateRandom(), ObjectStream::FilterDescriptor{AZ::Data::AssetFilterNoAssetLoading});
        tester.SerializeOut(materialAssetLevel3.Get());
        materialAssetLevel3 = tester.SerializeIn(Uuid::CreateRandom(), ObjectStream::FilterDescriptor{AZ::Data::AssetFilterNoAssetLoading});

        // The properties will finalize automatically when we call GetPropertyValues()...

        AZStd::span<const MaterialPropertyValue> properties;

        // Check level 1 properties
        properties = materialAssetLevel1->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(0.0f, 0.0f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));

        // Check level 2 properties
        properties = materialAssetLevel2->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));

        // Check level 3 properties
        properties = materialAssetLevel3->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 3.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));
    }

    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MultiLevelDataInheritance_Error_MaterialTypesDontMatch)
    {
        Data::Asset<MaterialTypeAsset> otherMaterialType;
        MaterialTypeAssetCreator materialTypeCreator;
        materialTypeCreator.Begin(Uuid::CreateRandom());
        materialTypeCreator.AddShader(m_testShaderAsset);
        AddCommonTestMaterialProperties(materialTypeCreator, "general.");
        EXPECT_TRUE(materialTypeCreator.End(otherMaterialType));
        m_assetSystemStub.RegisterSourceInfo("otherBase.materialtype", otherMaterialType.GetId());

        MaterialSourceData sourceDataLevel1;
        sourceDataLevel1.m_materialType = "@exefolder@/Temp/test.materialtype";

        MaterialSourceData sourceDataLevel2;
        sourceDataLevel2.m_materialType = "@exefolder@/Temp/test.materialtype";
        sourceDataLevel2.m_parentMaterial = "level1.material";

        MaterialSourceData sourceDataLevel3;
        sourceDataLevel3.m_materialType = "@exefolder@/Temp/otherBase.materialtype";
        sourceDataLevel3.m_parentMaterial = "level2.material";

        auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true);
        EXPECT_TRUE(materialAssetLevel1.IsSuccess());

        m_assetSystemStub.RegisterSourceInfo("level1.material", materialAssetLevel1.GetValue().GetId());

        auto materialAssetLevel2 = sourceDataLevel2.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true);
        EXPECT_TRUE(materialAssetLevel2.IsSuccess());

        m_assetSystemStub.RegisterSourceInfo("level2.material", materialAssetLevel2.GetValue().GetId());

        AZ_TEST_START_ASSERTTEST;
        auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true);
        AZ_TEST_STOP_ASSERTTEST(1);
        EXPECT_FALSE(materialAssetLevel3.IsSuccess());
    }

    TEST_F(MaterialSourceDataTests, CreateMaterialAsset_Error_BadInput)
    {
        // We use local functions to easily start a new MaterialAssetCreator for each test case because
        // the AssetCreator would just skip subsequent operations after the first failure is detected.

        auto expectWarning = [](const char* expectedErrorMessage, AZStd::function<void(MaterialSourceData& materialSourceData)> setOneBadInput, bool warningOccursBeforeFinalize = false)
        {
            MaterialSourceData sourceData;

            sourceData.m_materialType = "@exefolder@/Temp/test.materialtype";

            AddPropertyGroup(sourceData, "general");

            setOneBadInput(sourceData);

            // Check with MaterialAssetProcessingMode::PreBake
            {
                ErrorMessageFinder errorFinder;
                errorFinder.AddExpectedErrorMessage(expectedErrorMessage);
                errorFinder.AddIgnoredErrorMessage("Failed to build", true);
                auto materialAssetOutcome = sourceData.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::PreBake, true);
                errorFinder.CheckExpectedErrorsFound();

                EXPECT_FALSE(materialAssetOutcome.IsSuccess());
            }
            
            // Check with MaterialAssetProcessingMode::DeferredBake, no validation occurs because the MaterialTypeAsset cannot be used and so the MaterialAsset is not finalized
            if(!warningOccursBeforeFinalize)
            {
                auto materialAssetOutcome = sourceData.CreateMaterialAsset(Uuid::CreateRandom(), "", MaterialAssetProcessingMode::DeferredBake, true);
                EXPECT_TRUE(materialAssetOutcome.IsSuccess());
            }
        };

        // Test property does not exist...

        expectWarning("\"general.DoesNotExist\" is not found in the material properties layout",
            [](MaterialSourceData& materialSourceData)
            {
                AddProperty(materialSourceData, "general", "DoesNotExist", true);
            });

        expectWarning("\"general.DoesNotExist\" is not found in the material properties layout",
            [](MaterialSourceData& materialSourceData)
            {
                AddProperty(materialSourceData, "general", "DoesNotExist", -10);
            });

        expectWarning("\"general.DoesNotExist\" is not found in the material properties layout",
            [](MaterialSourceData& materialSourceData)
            {
                AddProperty(materialSourceData, "general", "DoesNotExist", 25u);
            });

        expectWarning("\"general.DoesNotExist\" is not found in the material properties layout",
            [](MaterialSourceData& materialSourceData)
            {
                AddProperty(materialSourceData, "general", "DoesNotExist", 1.5f);
            });

        expectWarning("\"general.DoesNotExist\" is not found in the material properties layout",
            [](MaterialSourceData& materialSourceData)
            {
                AddProperty(materialSourceData, "general", "DoesNotExist", AZ::Color{ 0.1f, 0.2f, 0.3f, 0.4f });
            });

        expectWarning("\"general.DoesNotExist\" is not found in the material properties layout",
            [](MaterialSourceData& materialSourceData)
            {
                AddProperty(materialSourceData, "general", "DoesNotExist", AZStd::string("@exefolder@/Temp/test.streamingimage"));
            });

        // Missing image reference
        expectWarning("Could not find the image 'doesNotExist.streamingimage'",
            [](MaterialSourceData& materialSourceData)
            {
                AddProperty(materialSourceData, "general", "MyImage", AZStd::string("doesNotExist.streamingimage"));
            }, true); // In this case, the warning does happen even when the asset is not finalized, because the image path is checked earlier than that
    }
    
    template<typename PropertyTypeT>
    void CheckSimilar(PropertyTypeT a, PropertyTypeT b);
    
    template<> void CheckSimilar<float>(float a, float b) { EXPECT_FLOAT_EQ(a, b); }
    template<> void CheckSimilar<Vector2>(Vector2 a, Vector2 b) { EXPECT_TRUE(a.IsClose(b)); }
    template<> void CheckSimilar<Vector3>(Vector3 a, Vector3 b) { EXPECT_TRUE(a.IsClose(b)); }
    template<> void CheckSimilar<Vector4>(Vector4 a, Vector4 b) { EXPECT_TRUE(a.IsClose(b)); }
    template<> void CheckSimilar<Color>(Color a, Color b) { EXPECT_TRUE(a.IsClose(b)); }

    template<typename PropertyTypeT> void CheckSimilar(PropertyTypeT a, PropertyTypeT b) { EXPECT_EQ(a, b); }

    template<typename PropertyTypeT>
    void CheckEndToEndDataTypeResolution(const char* propertyName, const char* jsonValue, PropertyTypeT expectedFinalValue)
    {
        const char* groupName = "general";

        const AZStd::string inputJson = AZStd::string::format(R"(
            {
                "materialType": "@exefolder@/Temp/test.materialtype",
                "propertyValues": {
                    "%s.%s": %s
                }
            }
        )", groupName, propertyName, jsonValue);

        MaterialSourceData material;
        JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson);
        auto materialAssetResult = material.CreateMaterialAsset(Uuid::CreateRandom(), "test.material", AZ::RPI::MaterialAssetProcessingMode::PreBake);
        EXPECT_TRUE(materialAssetResult);
        MaterialPropertyIndex propertyIndex = materialAssetResult.GetValue()->GetMaterialPropertiesLayout()->FindPropertyIndex(MaterialPropertyId{groupName, propertyName});
        CheckSimilar(expectedFinalValue, materialAssetResult.GetValue()->GetPropertyValues()[propertyIndex.GetIndex()].GetValue<PropertyTypeT>());
    }

    TEST_F(MaterialSourceDataTests, TestEndToEndDataTypeResolution)
    {
        // Data types in .material files don't have to exactly match the types in .materialtype files as specified in the properties layout.
        // The exact location of the data type resolution has moved around over the life of the project, but the important thing is that
        // the data type in the source .material file gets applied correctly by the time a finalized MaterialAsset comes out the other side.
        
        CheckEndToEndDataTypeResolution("MyBool", "true", true);
        CheckEndToEndDataTypeResolution("MyBool", "false", false);
        CheckEndToEndDataTypeResolution("MyBool", "1", true);
        CheckEndToEndDataTypeResolution("MyBool", "0", false);
        CheckEndToEndDataTypeResolution("MyBool", "1.0", true);
        CheckEndToEndDataTypeResolution("MyBool", "0.0", false);
        
        CheckEndToEndDataTypeResolution("MyInt", "5", 5);
        CheckEndToEndDataTypeResolution("MyInt", "-6", -6);
        CheckEndToEndDataTypeResolution("MyInt", "-7.0", -7);
        CheckEndToEndDataTypeResolution("MyInt", "false", 0);
        CheckEndToEndDataTypeResolution("MyInt", "true", 1);
        
        CheckEndToEndDataTypeResolution("MyUInt", "8", 8u);
        CheckEndToEndDataTypeResolution("MyUInt", "9.0", 9u);
        CheckEndToEndDataTypeResolution("MyUInt", "false", 0u);
        CheckEndToEndDataTypeResolution("MyUInt", "true", 1u);
        
        CheckEndToEndDataTypeResolution("MyFloat", "2", 2.0f);
        CheckEndToEndDataTypeResolution("MyFloat", "-2", -2.0f);
        CheckEndToEndDataTypeResolution("MyFloat", "2.1", 2.1f);
        CheckEndToEndDataTypeResolution("MyFloat", "false", 0.0f);
        CheckEndToEndDataTypeResolution("MyFloat", "true", 1.0f);
        
        CheckEndToEndDataTypeResolution("MyColor", "[0.1,0.2,0.3]", Color{0.1f, 0.2f, 0.3f, 1.0});
        CheckEndToEndDataTypeResolution("MyColor", "[0.1, 0.2, 0.3, 0.5]", Color{0.1f, 0.2f, 0.3f, 0.5f});
        CheckEndToEndDataTypeResolution("MyColor", "{\"RGB8\": [255, 0, 255, 0]}", Color{1.0f, 0.0f, 1.0f, 0.0f});
        
        CheckEndToEndDataTypeResolution("MyFloat2", "[0.1,0.2]", Vector2{0.1f, 0.2f});
        CheckEndToEndDataTypeResolution("MyFloat2", "{\"y\":0.2, \"x\":0.1}", Vector2{0.1f, 0.2f});
        CheckEndToEndDataTypeResolution("MyFloat2", "{\"y\":0.2, \"x\":0.1, \"Z\":0.3}", Vector2{0.1f, 0.2f});
        CheckEndToEndDataTypeResolution("MyFloat2", "{\"y\":0.2, \"W\":0.4, \"x\":0.1, \"Z\":0.3}", Vector2{0.1f, 0.2f});
        
        CheckEndToEndDataTypeResolution("MyFloat3", "[0.1,0.2,0.3]", Vector3{0.1f, 0.2f, 0.3f});
        CheckEndToEndDataTypeResolution("MyFloat3", "{\"y\":0.2, \"x\":0.1}", Vector3{0.1f, 0.2f, 0.0f});
        CheckEndToEndDataTypeResolution("MyFloat3", "{\"y\":0.2, \"x\":0.1, \"Z\":0.3}", Vector3{0.1f, 0.2f, 0.3f});
        CheckEndToEndDataTypeResolution("MyFloat3", "{\"y\":0.2, \"W\":0.4, \"x\":0.1, \"Z\":0.3}", Vector3{0.1f, 0.2f, 0.3f});
        
        CheckEndToEndDataTypeResolution("MyFloat4", "[0.1,0.2,0.3,0.4]", Vector4{0.1f, 0.2f, 0.3f, 0.4f});
        CheckEndToEndDataTypeResolution("MyFloat4", "{\"y\":0.2, \"x\":0.1}", Vector4{0.1f, 0.2f, 0.0f, 0.0f});
        CheckEndToEndDataTypeResolution("MyFloat4", "{\"y\":0.2, \"x\":0.1, \"Z\":0.3}", Vector4{0.1f, 0.2f, 0.3f, 0.0f});
        CheckEndToEndDataTypeResolution("MyFloat4", "{\"y\":0.2, \"W\":0.4, \"x\":0.1, \"Z\":0.3}", Vector4{0.1f, 0.2f, 0.3f, 0.4f});
    }

    TEST_F(MaterialSourceDataTests, CreateMaterialAssetFromSourceData_MultiLevelDataInheritance)
    {
        // Note the data being tested here is based on CreateMaterialAsset_MultiLevelDataInheritance()

        const AZStd::string simpleMaterialTypeJson = R"(
            {
                "propertyLayout": {
                    "propertyGroups":
                    [
                        {
                            "name": "general",
                            "properties": [
                                {
                                    "name": "MyFloat",
                                    "type": "Float"
                                },
                                {
                                    "name": "MyFloat2",
                                    "type": "Vector2"
                                },
                                {
                                    "name": "MyColor",
                                    "type": "Color"
                                }
                            ]
                        }
                    ]
                }
            }
        )";

        AZ::Utils::WriteFile(simpleMaterialTypeJson, "@exefolder@/Temp/test.materialtype");

        const AZStd::string material1Json = R"(
            {
                "materialType": "@exefolder@/Temp/test.materialtype",
                "propertyValues": {
                    "general.MyFloat": 1.5,
                    "general.MyColor": [0.1, 0.2, 0.3, 0.4]
                }
            }
        )";
        
        AZ::Utils::WriteFile(material1Json, "@exefolder@/Temp/m1.material");
        
        const AZStd::string material2Json = R"(
            {
                "materialType": "@exefolder@/Temp/test.materialtype",
                "parentMaterial": "@exefolder@/Temp/m1.material",
                "propertyValues": {
                    "general.MyFloat2": [4.1, 4.2],
                    "general.MyColor": [0.15, 0.25, 0.35, 0.45]
                }
            }
        )";
        
        AZ::Utils::WriteFile(material2Json, "@exefolder@/Temp/m2.material");
        
        const AZStd::string material3Json = R"(
            {
                "materialType": "@exefolder@/Temp/test.materialtype",
                "parentMaterial": "@exefolder@/Temp/m2.material",
                "propertyValues": {
                    "general.MyFloat": 3.5
                }
            }
        )";
        
        AZ::Utils::WriteFile(material3Json, "@exefolder@/Temp/m3.material");
        
        MaterialSourceData sourceDataLevel1 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m1.material").TakeValue();
        MaterialSourceData sourceDataLevel2 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m2.material").TakeValue();
        MaterialSourceData sourceDataLevel3 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m3.material").TakeValue();
        
        auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
        ASSERT_TRUE(materialAssetLevel1.IsSuccess());
        EXPECT_TRUE(materialAssetLevel1.GetValue()->WasPreFinalized());

        auto materialAssetLevel2 = sourceDataLevel2.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
        ASSERT_TRUE(materialAssetLevel2.IsSuccess());
        EXPECT_TRUE(materialAssetLevel2.GetValue()->WasPreFinalized());

        auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
        ASSERT_TRUE(materialAssetLevel3.IsSuccess());
        EXPECT_TRUE(materialAssetLevel3.GetValue()->WasPreFinalized());

        auto layout = materialAssetLevel1.GetValue()->GetMaterialPropertiesLayout();
        MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat"));
        MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2"));
        MaterialPropertyIndex myColor = layout->FindPropertyIndex(Name("general.MyColor"));

        AZStd::span<const MaterialPropertyValue> properties;

        // Check level 1 properties
        properties = materialAssetLevel1.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(0.0f, 0.0f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));

        // Check level 2 properties
        properties = materialAssetLevel2.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));

        // Check level 3 properties
        properties = materialAssetLevel3.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 3.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));
    }
    
    TEST_F(MaterialSourceDataTests, CreateMaterialAssetFromSourceData_MultiLevelDataInheritance_OldFormat)
    {
        // This test is the same as CreateMaterialAssetFromSourceData_MultiLevelDataInheritance except it uses the old format
        // where material property values in the .material file were nested, with properties listed under a group object,
        // rather than using a flat list of property values.
        // Basically, we are making sure that MaterialSourceData::ConvertToNewDataFormat() is getting called.

        const AZStd::string simpleMaterialTypeJson = R"(
            {
                "propertyLayout": {
                    "propertyGroups":
                    [
                        {
                            "name": "general",
                            "properties": [
                                {
                                    "name": "MyFloat",
                                    "type": "Float"
                                },
                                {
                                    "name": "MyFloat2",
                                    "type": "Vector2"
                                },
                                {
                                    "name": "MyColor",
                                    "type": "Color"
                                }
                            ]
                        }
                    ]
                }
            }
        )";

        AZ::Utils::WriteFile(simpleMaterialTypeJson, "@exefolder@/Temp/test.materialtype");

        const AZStd::string material1Json = R"(
            {
                "materialType": "@exefolder@/Temp/test.materialtype",
                "properties": {
                    "general": {
                        "MyFloat": 1.5,
                        "MyColor": [0.1, 0.2, 0.3, 0.4]
                    }
                }
            }
        )";
        
        AZ::Utils::WriteFile(material1Json, "@exefolder@/Temp/m1.material");
        
        const AZStd::string material2Json = R"(
            {
                "materialType": "@exefolder@/Temp/test.materialtype",
                "parentMaterial": "@exefolder@/Temp/m1.material",
                "properties": {
                    "general": {
                        "MyFloat2": [4.1, 4.2],
                        "MyColor": [0.15, 0.25, 0.35, 0.45]
                    }
                }
            }
        )";
        
        AZ::Utils::WriteFile(material2Json, "@exefolder@/Temp/m2.material");
        
        const AZStd::string material3Json = R"(
            {
                "materialType": "@exefolder@/Temp/test.materialtype",
                "parentMaterial": "@exefolder@/Temp/m2.material",
                "properties": {
                    "general": {
                        "MyFloat": 3.5
                    }
                }
            }
        )";
        
        AZ::Utils::WriteFile(material3Json, "@exefolder@/Temp/m3.material");
        
        MaterialSourceData sourceDataLevel1 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m1.material").TakeValue();
        MaterialSourceData sourceDataLevel2 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m2.material").TakeValue();
        MaterialSourceData sourceDataLevel3 = MaterialUtils::LoadMaterialSourceData("@exefolder@/Temp/m3.material").TakeValue();
        
        auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
        EXPECT_TRUE(materialAssetLevel1.IsSuccess());
        EXPECT_TRUE(materialAssetLevel1.GetValue()->WasPreFinalized());

        auto materialAssetLevel2 = sourceDataLevel2.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
        EXPECT_TRUE(materialAssetLevel2.IsSuccess());
        EXPECT_TRUE(materialAssetLevel2.GetValue()->WasPreFinalized());

        auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAssetFromSourceData(Uuid::CreateRandom());
        EXPECT_TRUE(materialAssetLevel3.IsSuccess());
        EXPECT_TRUE(materialAssetLevel3.GetValue()->WasPreFinalized());

        auto layout = materialAssetLevel1.GetValue()->GetMaterialPropertiesLayout();
        MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat"));
        MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2"));
        MaterialPropertyIndex myColor = layout->FindPropertyIndex(Name("general.MyColor"));

        AZStd::span<const MaterialPropertyValue> properties;

        // Check level 1 properties
        properties = materialAssetLevel1.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(0.0f, 0.0f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.1f, 0.2f, 0.3f, 0.4f));

        // Check level 2 properties
        properties = materialAssetLevel2.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 1.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));

        // Check level 3 properties
        properties = materialAssetLevel3.GetValue()->GetPropertyValues();
        EXPECT_EQ(properties[myFloat.GetIndex()].GetValue<float>(), 3.5f);
        EXPECT_EQ(properties[myFloat2.GetIndex()].GetValue<Vector2>(), Vector2(4.1f, 4.2f));
        EXPECT_EQ(properties[myColor.GetIndex()].GetValue<Color>(), Color(0.15f, 0.25f, 0.35f, 0.45f));
    }
}


