/*
* 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 "Vegetation_precompiled.h"

#include "VegetationTest.h"
#include "VegetationMocks.h"

#include <AzCore/Component/Entity.h>
#include <AzTest/AzTest.h>
#include <AzCore/UnitTest/TestTypes.h>
#include <AzCore/Asset/AssetManagerBus.h>
#include <AzCore/Asset/AssetManagerComponent.h>
#include <AzCore/Asset/AssetManager.h>
#include <AzCore/Memory/PoolAllocator.h>
#include <AzFramework/Entity/GameEntityContextBus.h>

#include <Vegetation/DynamicSliceInstanceSpawner.h>
#include <Vegetation/Ebuses/DescriptorNotificationBus.h>

namespace UnitTest
{
    // Mock VegetationSystemComponent is needed to reflect only the DynamicSliceInstanceSpawner.
    class MockDynamicSliceInstanceVegetationSystemComponent
        : public AZ::Component
    {
    public:
        AZ_COMPONENT(MockDynamicSliceInstanceVegetationSystemComponent, "{41BCCB16-1E27-4B8E-9053-762CC5034F18}", AZ::Component);

        void Activate() override {}
        void Deactivate() override {}

        static void Reflect(AZ::ReflectContext* reflect)
        {
            Vegetation::InstanceSpawner::Reflect(reflect);
            Vegetation::DynamicSliceInstanceSpawner::Reflect(reflect);
        }
        static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
        {
            provided.push_back(AZ_CRC("VegetationSystemService", 0xa2322728));
        }
    };

    // To test Dynamic Slice spawning, we need to mock up enough of the asset management system and the dynamic slice
    // asset handling to pretend like we're loading/unloading dynamic slices successfully.
    class DynamicSliceInstanceSpawnerTests
        : public VegetationComponentTests
        , public Vegetation::DescriptorNotificationBus::Handler
        , public AZ::Data::AssetCatalogRequestBus::Handler
        , public AZ::Data::AssetHandler
        , public AZ::Data::AssetCatalog
        , public AzFramework::GameEntityContextRequestBus::Handler
    {
    public:
        void RegisterComponentDescriptors() override
        {
            m_app.RegisterComponentDescriptor(MockDynamicSliceInstanceVegetationSystemComponent::CreateDescriptor());
        }

        void SetUp() override
        {
            VegetationComponentTests::SetUp();

            // Create a real Asset Mananger, and point to ourselves as the handler for DynamicSliceAsset.
            AZ::AllocatorInstance<AZ::PoolAllocator>::Create();
            AZ::AllocatorInstance<AZ::ThreadPoolAllocator>::Create();
            AZ::Data::AssetManager::Descriptor descriptor;
            descriptor.m_maxWorkerThreads = 1;
            AZ::Data::AssetManager::Create(descriptor);
            AZ::Data::AssetManager::Instance().RegisterHandler(this, AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid());
            AZ::Data::AssetManager::Instance().RegisterCatalog(this, AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid());

            m_app.RegisterComponentDescriptor(AZ::SliceComponent::CreateDescriptor());

            // Intercept messages for finding assets by name and creating/destroying slices.
            AZ::Data::AssetCatalogRequestBus::Handler::BusConnect();
            AzFramework::GameEntityContextRequestBus::Handler::BusConnect();
        }

        void TearDown() override
        {
            // Give the AssetManager a chance to fire off any lingering events and perform cleanup for any
            // dynamic slice assets we loaded.
            AZ::Data::AssetManager::Instance().DispatchEvents();

            AzFramework::GameEntityContextRequestBus::Handler::BusDisconnect();
            AZ::Data::AssetManager::Instance().UnregisterCatalog(this);
            AZ::Data::AssetManager::Instance().UnregisterHandler(this);

            AZ::Data::AssetCatalogRequestBus::Handler::BusDisconnect();

            AZ::Data::AssetManager::Destroy();
            AZ::AllocatorInstance<AZ::ThreadPoolAllocator>::Destroy();
            AZ::AllocatorInstance<AZ::PoolAllocator>::Destroy();

            VegetationComponentTests::TearDown();
        }

        // Helper methods:

        // Set up a mock asset with the given name and id and direct the instance spawner to use it.
        void CreateAndSetMockAsset(Vegetation::DynamicSliceInstanceSpawner& instanceSpawner, AZ::Data::AssetId assetId, AZStd::string assetPath)
        {
            // Save these off for use from our mock AssetCatalogRequestBus
            m_assetId = assetId;
            m_assetPath = assetPath;

            Vegetation::DescriptorNotificationBus::Handler::BusConnect(&instanceSpawner);

            // Tell the spawner to use this asset.  Note that this also triggers a LoadAssets() call
            // internally.
            instanceSpawner.SetSliceAssetPath(m_assetPath);

            // Our instance spawner should now have a valid asset reference.
            // It may or may not be loaded already by the time we get here,
            // depending on how quickly the Asset Processor job thread picks it up.
            EXPECT_FALSE(instanceSpawner.HasEmptyAssetReferences());

            // Since the asset load is going through the real AssetManager, there's a delay while a separate
            // job thread executes and actually loads our mock dynamic slice asset.
            // If our asset hasn't loaded successfully after 5 seconds, it's unlikely to succeed.
            // This choice of delay should be *reasonably* safe because it's all CPU-based processing,
            // no actual I/O occurs as a part of the test.
            constexpr int sleepMs = 10;
            constexpr int totalWaitTimeMs = 5000;
            int numRetries = totalWaitTimeMs / sleepMs;
            while ((m_numOnLoadedCalls < 1) && (numRetries >= 0))
            {
                AZ::Data::AssetManager::Instance().DispatchEvents();
                AZ::SystemTickBus::Broadcast(&AZ::SystemTickBus::Events::OnSystemTick);
                AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(sleepMs));
                numRetries--;
            }

            ASSERT_TRUE(m_numOnLoadedCalls == 1);
            EXPECT_TRUE(instanceSpawner.IsLoaded());
            EXPECT_TRUE(instanceSpawner.IsSpawnable());

            Vegetation::DescriptorNotificationBus::Handler::BusDisconnect();
        }


        // AssetHandler
        // Minimalist mocks to look like a Dynamic Slice has been created/loaded/destroyed successfully
        AZ::Data::AssetPtr CreateAsset(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type) override
        {
            AZ::DynamicSliceAsset* sliceAsset = new AZ::DynamicSliceAsset(id);
            AZ::Entity* mockEntity = new AZ::Entity();
            mockEntity->Init();
            auto mockComponent = mockEntity->CreateComponent<AZ::SliceComponent>();
            mockEntity->Activate();
            sliceAsset->SetData(mockEntity, mockComponent);
            MockAssetData* temp = reinterpret_cast<MockAssetData*>(sliceAsset);
            temp->SetStatus(AZ::Data::AssetData::AssetStatus::NotLoaded);

            return sliceAsset;
        }

        void DestroyAsset(AZ::Data::AssetPtr ptr) override { delete ptr; }
        void GetHandledAssetTypes(AZStd::vector<AZ::Data::AssetType>& assetTypes) override { assetTypes.push_back(AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid()); }
        bool LoadAssetData(const AZ::Data::Asset<AZ::Data::AssetData>& asset, const char* assetPath, const AZ::Data::AssetFilterCB& assetLoadFilterCB)
        {
            MockAssetData* temp = reinterpret_cast<MockAssetData*>(asset.GetData());
            temp->SetStatus(AZ::Data::AssetData::AssetStatus::Ready);
            return true;
        }

        // DescriptorNotificationBus
        // Keep track of whether or not the Spawner successfully loaded the asset and notified listeners
        void OnDescriptorAssetsLoaded() override { m_numOnLoadedCalls++; }

        // AssetCatalogRequestBus
        // Minimalist mocks to provide our desired asset path or asset id
        AZStd::string GetAssetPathById(const AZ::Data::AssetId& /*id*/) override { return m_assetPath; }
        AZ::Data::AssetId GetAssetIdByPath(const char* /*path*/, const AZ::Data::AssetType& /*typeToRegister*/, bool /*autoRegisterIfNotFound*/) override { return m_assetId; }
        AZ::Data::AssetInfo GetAssetInfoById(const AZ::Data::AssetId& /*id*/) override
        { 
            AZ::Data::AssetInfo assetInfo;
            assetInfo.m_assetId = m_assetId;
            assetInfo.m_assetType = AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid();
            assetInfo.m_relativePath = m_assetPath;
            return assetInfo;
        }

        // GameEntityContextRequestBus
        // Minimalist mocks to mock out InstantiateDynamicSlice
        AzFramework::EntityContextId GetGameEntityContextId() override { return AzFramework::EntityContextId(); }
        AZ::Entity* CreateGameEntity(const char* /*name*/) override { return nullptr; }
        AzFramework::BehaviorEntity CreateGameEntityForBehaviorContext(const char* /*name*/) override { return AzFramework::BehaviorEntity(); }
        void AddGameEntity(AZ::Entity* /*entity*/) override {}
        void DestroyGameEntity(const AZ::EntityId& /*id*/) override {}
        void DestroyGameEntityAndDescendants(const AZ::EntityId& /*id*/) override {}
        void ActivateGameEntity(const AZ::EntityId& /*id*/) override {}
        void DeactivateGameEntity(const AZ::EntityId& /*id*/) override {}
        bool LoadFromStream(AZ::IO::GenericStream& /*stream*/, bool /*remapIds*/) override { return true; }
        void ResetGameContext() override {}
        void MarkEntityForNoActivation(AZ::EntityId /*entityId*/) override {}
        AZStd::string GetEntityName(const AZ::EntityId&) override { return ""; }
        // These are the only mocks actually needed for unit testing DynamicSliceInstanceSpawner:    
        void CancelDynamicSliceInstantiation(const AzFramework::SliceInstantiationTicket& /*ticket*/) override {}
        bool DestroyDynamicSliceByEntity(const AZ::EntityId& /*id*/) override { return true; }
        AzFramework::SliceInstantiationTicket InstantiateDynamicSlice(
            const AZ::Data::Asset<AZ::Data::AssetData>& /*sliceAsset*/,
            const AZ::Transform& /*worldTransform*/,
            const AZ::IdUtils::Remapper<AZ::EntityId>::IdMapper& /*customIdMapper*/) override
        {
            return AzFramework::SliceInstantiationTicket(AzFramework::EntityContextId::Create(), 1);
        }

        // AssetCatalog
        // Minimalist mock to pretend like we've loaded a Dynamic Slice asset
        AZ::Data::AssetStreamInfo GetStreamInfoForLoad(const AZ::Data::AssetId& id, const AZ::Data::AssetType& type) override
        {
            EXPECT_TRUE(type == AZ::AzTypeInfo<AZ::DynamicSliceAsset>::Uuid());
            AZ::Data::AssetStreamInfo info;
            info.m_dataOffset = 0;
            info.m_streamFlags = AZ::IO::OpenMode::ModeRead;
            info.m_streamName = m_assetPath;
            info.m_dataLen = 0;
            // By setting this to true, we call our custom LoadAssetData() above instead of using actual File IO
            info.m_isCustomStreamType = true;

            return info;
        }

        AZStd::string m_assetPath;
        AZ::Data::AssetId m_assetId;
        int m_numOnLoadedCalls = 0;

    };

    TEST_F(DynamicSliceInstanceSpawnerTests, BasicInitializationTest)
    {
        // Basic test to make sure we can construct / destroy without errors.

        Vegetation::DynamicSliceInstanceSpawner instanceSpawner;
    }

    TEST_F(DynamicSliceInstanceSpawnerTests, DefaultSpawnersAreEqual)
    {
        // Two different instances of the default DynamicSliceInstanceSpawner should
        // be considered data-equivalent.

        Vegetation::DynamicSliceInstanceSpawner instanceSpawner1;
        Vegetation::DynamicSliceInstanceSpawner instanceSpawner2;

        EXPECT_TRUE(instanceSpawner1 == instanceSpawner2);
    }

    // [LY-117648] This test intermittently fails on automated builds with errors like the following:
    // d:\ly\workspace\DEV_shelf_build\dev\Code\Framework\AzCore\AzCore/UnitTest/UnitTest.h(161):
    // error: Asset handler 'AssetHandler' is being removed but there are still 1 active assets being handled by it!
    // d:\ly\workspace\DEV_shelf_build\dev\Code\Framework\AzCore\AzCore\Asset\AssetManager.cpp(1051) :
    // error: No handler was registered for this asset[type:0x6dd991e8 id : {535BED6F - 0CFA - 4C53 - 9B2A - 1A531BBB16F6} : 0]!
    // Until the intermittent failure can be solved, the test is disabled.  
    TEST_F(DynamicSliceInstanceSpawnerTests, DISABLED_DifferentSpawnersAreNotEqual)
    {
        // Two spawners with different data should *not* be data-equivalent.

        Vegetation::DynamicSliceInstanceSpawner instanceSpawner1;
        Vegetation::DynamicSliceInstanceSpawner instanceSpawner2;

        // Give the second instance spawner a non-default asset reference.
        CreateAndSetMockAsset(instanceSpawner2, AZ::Uuid::CreateRandom(), "test");

        // The test is written this way because only the == operator is overloaded.
        EXPECT_TRUE(!(instanceSpawner1 == instanceSpawner2));
    }

    // [LY-118267] This test intermittently fails on automated builds, so disabling temporarily until the root cause
    // can be identified
    TEST_F(DynamicSliceInstanceSpawnerTests, DISABLED_LoadAndUnloadAssets)
    {
        // The spawner should successfully load/unload assets without errors.

        Vegetation::DynamicSliceInstanceSpawner instanceSpawner;

        // Our instance spawner should be empty before we set the assets.
        EXPECT_TRUE(instanceSpawner.HasEmptyAssetReferences());

        // This will test the asset load.
        CreateAndSetMockAsset(instanceSpawner, AZ::Uuid::CreateRandom(), "test");

        // Test the asset unload works too.
        Vegetation::DescriptorNotificationBus::Handler::BusConnect(&instanceSpawner);
        instanceSpawner.UnloadAssets();
        EXPECT_FALSE(instanceSpawner.IsLoaded());
        EXPECT_FALSE(instanceSpawner.IsSpawnable());
        Vegetation::DescriptorNotificationBus::Handler::BusDisconnect();
    }

    // [LY-118267] Any test using CreateAndSetMockAsset can intermittently fail, so disabling them for now.
    TEST_F(DynamicSliceInstanceSpawnerTests, DISABLED_CreateAndDestroyInstance)
    {
        // The spawner should successfully create and destroy an instance without errors.

        Vegetation::DynamicSliceInstanceSpawner instanceSpawner;

        CreateAndSetMockAsset(instanceSpawner, AZ::Uuid::CreateRandom(), "test");

        instanceSpawner.OnRegisterUniqueDescriptor();

        Vegetation::InstanceData instanceData;
        Vegetation::InstancePtr instance = instanceSpawner.CreateInstance(instanceData);
        EXPECT_TRUE(instance);
        instanceSpawner.DestroyInstance(0, instance);

        instanceSpawner.OnReleaseUniqueDescriptor();
    }

    TEST_F(DynamicSliceInstanceSpawnerTests, SpawnerRegisteredWithDescriptor)
    {
        // Validate that the Descriptor successfully gets DynamicSliceInstanceSpawner registered with it,
        // as long as InstanceSpawner and DynamicSliceInstanceSpawner have been reflected.

        MockDynamicSliceInstanceVegetationSystemComponent* component = nullptr;
        auto entity = CreateEntity(&component);

        Vegetation::Descriptor descriptor;
        descriptor.RefreshSpawnerTypeList();
        auto spawnerTypes = descriptor.GetSpawnerTypeList();
        EXPECT_TRUE(spawnerTypes.size() == 1);
        EXPECT_TRUE(spawnerTypes[0].first == Vegetation::DynamicSliceInstanceSpawner::RTTI_Type());
    }

    TEST_F(DynamicSliceInstanceSpawnerTests, DescriptorCreatesCorrectSpawner)
    {
        // Validate that the Descriptor successfully creates a new DynamicSliceInstanceSpawner if we change
        // the spawner type on the Descriptor.

        MockDynamicSliceInstanceVegetationSystemComponent* component = nullptr;
        auto entity = CreateEntity(&component);

        // We expect the Descriptor to start off with a Legacy Vegetation spawner, but then should correctly get an
        // DynamicSliceInstanceSpawner after we change spawnerType.
        Vegetation::Descriptor descriptor;
        EXPECT_TRUE(azrtti_typeid(*(descriptor.GetInstanceSpawner())) != Vegetation::DynamicSliceInstanceSpawner::RTTI_Type());
        descriptor.m_spawnerType = Vegetation::DynamicSliceInstanceSpawner::RTTI_Type();
        descriptor.RefreshSpawnerTypeList();
        descriptor.SpawnerTypeChanged();
        EXPECT_TRUE(azrtti_typeid(*(descriptor.GetInstanceSpawner())) == Vegetation::DynamicSliceInstanceSpawner::RTTI_Type());
    }
}