/*
 * Copyright 2022 Google LLC
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include "tests/Test.h"

#include "include/core/SkBitmap.h"
#include "include/core/SkCanvas.h"
#include "include/core/SkImage.h"
#include "include/core/SkSurface.h"
#include "include/gpu/graphite/Context.h"
#include "include/gpu/graphite/Image.h"
#include "include/gpu/graphite/Recorder.h"
#include "include/gpu/graphite/Recording.h"
#include "include/gpu/graphite/Surface.h"
#include "src/core/SkCanvasPriv.h"
#include "src/gpu/graphite/Caps.h"
#include "src/gpu/graphite/ContextPriv.h"
#include "src/gpu/graphite/Device.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/Resource.h"
#include "src/gpu/graphite/ResourceCache.h"
#include "src/gpu/graphite/ResourceProvider.h"
#include "src/gpu/graphite/SharedContext.h"
#include "src/gpu/graphite/Texture.h"
#include "src/gpu/graphite/TextureProxyView.h"
#include "src/gpu/graphite/TextureUtils.h"
#include "src/image/SkImage_Base.h"
#include "tools/Resources.h"
#include "tools/graphite/GraphiteTestContext.h"

namespace skgpu::graphite {

class TestResource : public Resource {
public:
    static sk_sp<TestResource> Make(const SharedContext* sharedContext,
                                    Ownership owned,
                                    skgpu::Budgeted budgeted,
                                    Shareable shareable,
                                    size_t gpuMemorySize = 1) {
        auto resource = sk_sp<TestResource>(new TestResource(sharedContext,
                                                             owned,
                                                             budgeted,
                                                             gpuMemorySize));
        if (!resource) {
            return nullptr;
        }

        GraphiteResourceKey key;
        CreateKey(&key, shareable);

        resource->setKey(key);
        return resource;
    }

    const char* getResourceType() const override { return "Test Resource"; }

    static void CreateKey(GraphiteResourceKey* key, Shareable shareable) {
        // Internally we assert that we don't make the same key twice where the only difference is
        // shareable vs non-shareable. That allows us to now have Shareable be part of the Key's
        // key. So here we make two different resource types so the keys will be different.
        static const ResourceType kType = GraphiteResourceKey::GenerateResourceType();
        static const ResourceType kShareableType = GraphiteResourceKey::GenerateResourceType();
        ResourceType type = shareable == Shareable::kNo ? kType : kShareableType;
        GraphiteResourceKey::Builder(key, type, 0, shareable);
    }

private:
    TestResource(const SharedContext* sharedContext,
                 Ownership owned,
                 skgpu::Budgeted budgeted,
                 size_t gpuMemorySize)
            : Resource(sharedContext, owned, budgeted, gpuMemorySize) {}

    void freeGpuData() override {}
};

static sk_sp<SkData> create_image_data(const SkImageInfo& info) {
    const size_t rowBytes = info.minRowBytes();
    sk_sp<SkData> data(SkData::MakeUninitialized(rowBytes * info.height()));
    {
        SkBitmap bm;
        bm.installPixels(info, data->writable_data(), rowBytes);
        SkCanvas canvas(bm);
        canvas.clear(SK_ColorRED);
    }
    return data;
}

static skgpu::graphite::TextureProxy* top_device_graphite_target_proxy(SkCanvas* canvas) {
    if (auto gpuDevice = SkCanvasPriv::TopDevice(canvas)->asGraphiteDevice()) {
        return gpuDevice->target();
    }
    return nullptr;
}

DEF_CONDITIONAL_GRAPHITE_TEST_FOR_ALL_CONTEXTS(GraphiteBudgetedResourcesTest,
                                               reporter,
                                               context,
                                               testContext,
                                               true,
                                               CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ResourceProvider* resourceProvider = recorder->priv().resourceProvider();
    ResourceCache* resourceCache = resourceProvider->resourceCache();
    const SharedContext* sharedContext = resourceProvider->sharedContext();

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 0);

    // Test making a non budgeted, non shareable resource.
    auto resource = TestResource::Make(
            sharedContext, Ownership::kOwned, skgpu::Budgeted::kNo, Shareable::kNo);
    if (!resource) {
        ERRORF(reporter, "Failed to make TestResource");
        return;
    }
    Resource* resourcePtr = resource.get();

    REPORTER_ASSERT(reporter, resource->budgeted() == skgpu::Budgeted::kNo);
    resourceCache->insertResource(resourcePtr);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    // Resource is not shareable and we have a ref on it. Thus it shouldn't ben findable in the
    // cache.
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 0);

    // When we reset our TestResource it should go back into the cache since it can be used as a
    // scratch texture (since it is not shareable). At that point the budget should be changed to
    // skgpu::Budgeted::kYes.
    resource.reset();
    resourceCache->forceProcessReturnedResources();
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 1);
    // Even though we reset our ref on the resource we still have the ptr to it and should be the
    // resource in the cache. So in general this is dangerous it should be safe for this test to
    // directly access the texture.
    REPORTER_ASSERT(reporter, resourcePtr->budgeted() == skgpu::Budgeted::kYes);

    GraphiteResourceKey key;
    TestResource::CreateKey(&key, Shareable::kNo);
    Resource* resourcePtr2 = resourceCache->findAndRefResource(key, skgpu::Budgeted::kNo);
    REPORTER_ASSERT(reporter, resourcePtr == resourcePtr2);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 0);
    REPORTER_ASSERT(reporter, resourcePtr2->budgeted() == skgpu::Budgeted::kNo);
    resourcePtr2->unref();
    resourceCache->forceProcessReturnedResources();

    // Test making a budgeted, shareable resource.
    resource = TestResource::Make(
            sharedContext, Ownership::kOwned, skgpu::Budgeted::kYes, Shareable::kYes);
    if (!resource) {
        ERRORF(reporter, "Failed to make TestResource");
        return;
    }
    resourcePtr = resource.get();
    REPORTER_ASSERT(reporter, resource->budgeted() == skgpu::Budgeted::kYes);
    resourceCache->insertResource(resourcePtr);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 2);

    resource.reset();
    resourceCache->forceProcessReturnedResources();
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 2);
    REPORTER_ASSERT(reporter, resourcePtr->budgeted() == skgpu::Budgeted::kYes);

    TestResource::CreateKey(&key, Shareable::kYes);
    resourcePtr2 = resourceCache->findAndRefResource(key, skgpu::Budgeted::kYes);
    REPORTER_ASSERT(reporter, resourcePtr == resourcePtr2);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 2);
    REPORTER_ASSERT(reporter, resourcePtr2->budgeted() == skgpu::Budgeted::kYes);
    resourcePtr2->unref();

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Test that SkImage's and SkSurface's underlying Resource's follow the expected budgeted
    // system.
    auto info = SkImageInfo::Make(10, 10, kRGBA_8888_SkColorType, kPremul_SkAlphaType);

    // First test SkImages. Since we can't directly create a Graphite SkImage we first have to make
    // a raster SkImage than convert that to a Graphite SkImage via makeTextureImage.
    sk_sp<SkData> data(create_image_data(info));
    sk_sp<SkImage> image = SkImages::RasterFromData(info, std::move(data), info.minRowBytes());
    REPORTER_ASSERT(reporter, image);

    sk_sp<SkImage> imageGpu = SkImages::TextureFromImage(recorder.get(), image, {});
    REPORTER_ASSERT(reporter, imageGpu);

    TextureProxy* imageProxy = nullptr;
    {
        // We don't want the view holding a ref to the Proxy or else we can't send things back to
        // the cache.
        auto view = skgpu::graphite::AsView(imageGpu.get());
        REPORTER_ASSERT(reporter, view);
        imageProxy = view.proxy();
    }
    // Make sure the proxy is instantiated
    if (!imageProxy->instantiate(resourceProvider)) {
        ERRORF(reporter, "Failed to instantiate Proxy");
        return;
    }
    const Resource* imageResourcePtr = imageProxy->texture();
    REPORTER_ASSERT(reporter, imageResourcePtr);
    // There is an extra resource for the buffer that is uploading the data to the texture
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 4);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 2);
    REPORTER_ASSERT(reporter, imageResourcePtr->budgeted() == skgpu::Budgeted::kNo);

    // Submit all upload work so we can drop refs to the image and get it returned to the cache.
    std::unique_ptr<Recording> recording = recorder->snap();
    if (!recording) {
        ERRORF(reporter, "Failed to make recording");
        return;
    }
    InsertRecordingInfo insertInfo;
    insertInfo.fRecording = recording.get();
    context->insertRecording(insertInfo);
    testContext->syncedSubmit(context);
    recording.reset();
    imageGpu.reset();
    resourceCache->forceProcessReturnedResources();

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 4);
    // Remapping async buffers before returning them to the cache can extend buffer lifetime.
    if (!context->priv().caps()->bufferMapsAreAsync()) {
        REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 4);
    }
    REPORTER_ASSERT(reporter, imageResourcePtr->budgeted() == skgpu::Budgeted::kYes);

    // Now try an SkSurface. This is simpler since we can directly create Graphite SkSurface's.
    sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(recorder.get(), info);
    if (!surface) {
        ERRORF(reporter, "Failed to make surface");
        return;
    }

    TextureProxy* surfaceProxy = top_device_graphite_target_proxy(surface->getCanvas());
    if (!surfaceProxy) {
        ERRORF(reporter, "Failed to get surface proxy");
        return;
    }

    // Make sure the proxy is instantiated
    if (!surfaceProxy->instantiate(resourceProvider)) {
        ERRORF(reporter, "Failed to instantiate surface proxy");
        return;
    }
    const Resource* surfaceResourcePtr = surfaceProxy->texture();

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 5);
    // Remapping async buffers before returning them to the cache can extend buffer lifetime.
    if (!context->priv().caps()->bufferMapsAreAsync()) {
        REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 4);
    }
    REPORTER_ASSERT(reporter, surfaceResourcePtr->budgeted() == skgpu::Budgeted::kNo);

    // The creation of the surface may have added an initial clear to it. Thus if we just reset the
    // surface it will flush the clean on the device and we don't be dropping all our refs to the
    // surface. So we force all the work to happen first.
    recording = recorder->snap();
    insertInfo.fRecording = recording.get();
    context->insertRecording(insertInfo);
    testContext->syncedSubmit(context);
    recording.reset();

    surface.reset();
    resourceCache->forceProcessReturnedResources();
    REPORTER_ASSERT(reporter, surfaceResourcePtr->budgeted() == skgpu::Budgeted::kYes);
}

namespace {
sk_sp<Resource> add_new_resource(skiatest::Reporter* reporter,
                                 const SharedContext* sharedContext,
                                 ResourceCache* resourceCache,
                                 size_t gpuMemorySize,
                                 skgpu::Budgeted budgeted = skgpu::Budgeted::kYes) {
    auto resource = TestResource::Make(sharedContext,
                                       Ownership::kOwned,
                                       budgeted,
                                       Shareable::kNo,
                                       gpuMemorySize);
    if (!resource) {
        ERRORF(reporter, "Failed to make TestResource");
        return nullptr;
    }
    resourceCache->insertResource(resource.get());
    return resource;
}

Resource* add_new_purgeable_resource(skiatest::Reporter* reporter,
                                     const SharedContext* sharedContext,
                                     ResourceCache* resourceCache,
                                     size_t gpuMemorySize) {
    auto resource = add_new_resource(reporter, sharedContext, resourceCache, gpuMemorySize);
    if (!resource) {
        return nullptr;
    }

    Resource* ptr = resource.get();
    resource.reset();
    resourceCache->forceProcessReturnedResources();
    return ptr;
}
} // namespace

DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(GraphitePurgeAsNeededResourcesTest, reporter, context,
                                   CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ResourceProvider* resourceProvider = recorder->priv().resourceProvider();
    ResourceCache* resourceCache = resourceProvider->resourceCache();
    const SharedContext* sharedContext = resourceProvider->sharedContext();

    resourceCache->setMaxBudget(10);

    auto resourceSize10 = add_new_resource(reporter,
                                           sharedContext,
                                           resourceCache,
                                           /*gpuMemorySize=*/10);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == nullptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 10);

    auto resourceSize1 = add_new_resource(reporter,
                                          sharedContext,
                                          resourceCache,
                                          /*gpuMemorySize=*/1);

    // We should now be over budget, but nothing should be purged since neither resource is
    // purgeable.
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == nullptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 11);

    // Dropping the ref to the size 1 resource should cause it to get purged when we add a new
    // resource to the cache.
    resourceSize1.reset();

    auto resourceSize2 = add_new_resource(reporter,
                                          sharedContext,
                                          resourceCache,
                                          /*gpuMemorySize=*/2);

    // The purging should have happened when we return the resource above so we also shouldn't
    // see anything in the purgeable queue.
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == nullptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 12);

    // Reset the cache back to no resources by setting budget to 0.
    resourceSize10.reset();
    resourceSize2.reset();
    resourceCache->forceProcessReturnedResources();
    resourceCache->setMaxBudget(0);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == nullptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 0);

    // Add a bunch of purgeable resources that keeps us under budget. Nothing should ever get purged.
    resourceCache->setMaxBudget(10);
    auto resourceSize1Ptr = add_new_purgeable_resource(reporter,
                                                       sharedContext,
                                                       resourceCache,
                                                       /*gpuMemorySize=*/1);
    /*auto resourceSize2Ptr=*/ add_new_purgeable_resource(reporter,
                                                          sharedContext,
                                                          resourceCache,
                                                          /*gpuMemorySize=*/2);
    auto resourceSize3Ptr = add_new_purgeable_resource(reporter,
                                                       sharedContext,
                                                       resourceCache,
                                                       /*gpuMemorySize=*/3);
    auto resourceSize4Ptr = add_new_purgeable_resource(reporter,
                                                       sharedContext,
                                                       resourceCache,
                                                       /*gpuMemorySize=*/4);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 4);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == resourceSize1Ptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 10);

    // Now add some resources that should cause things to get purged.
    // Add a size 2 resource should purge the original size 1 and size 2
    add_new_purgeable_resource(reporter,
                               sharedContext,
                               resourceCache,
                               /*gpuMemorySize=*/2);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 3);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == resourceSize3Ptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 9);

    // Adding a non-purgeable resource should also trigger resources to be purged from purgeable
    // queue.
    resourceSize10 = add_new_resource(reporter,
                                      sharedContext,
                                      resourceCache,
                                      /*gpuMemorySize=*/10);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == nullptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 10);

    // Adding a resources that is purgeable back to the cache shouldn't trigger the previous
    // non-purgeable resource or itself to be purged yet (since processing our return mailbox
    // doesn't trigger the purgeAsNeeded call)
    resourceSize4Ptr = add_new_purgeable_resource(reporter,
                                                  sharedContext,
                                                  resourceCache,
                                                  /*gpuMemorySize=*/4);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == resourceSize4Ptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 14);

    // Resetting the budget to 0 should trigger purging the size 4 purgeable resource but should
    // leave the non purgeable size 10 alone.
    resourceCache->setMaxBudget(0);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == nullptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 10);

    resourceSize10.reset();
    resourceCache->forceProcessReturnedResources();
    resourceCache->forcePurgeAsNeeded();

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == nullptr);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 0);
}

DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(GraphiteZeroSizedResourcesTest, reporter, context,
                                   CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ResourceProvider* resourceProvider = recorder->priv().resourceProvider();
    ResourceCache* resourceCache = resourceProvider->resourceCache();
    const SharedContext* sharedContext = resourceProvider->sharedContext();

    // First make a normal resource that has a non zero size
    Resource* resourcePtr = add_new_purgeable_resource(reporter,
                                                       sharedContext,
                                                       resourceCache,
                                                       /*gpuMemorySize=*/1);
    if (!resourcePtr) {
        return;
    }

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 1);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == resourcePtr);

    // First confirm if we set the max budget to zero, this sized resource is removed.
    resourceCache->setMaxBudget(0);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 0);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == nullptr);

    // Set the budget back to something higher
    resourceCache->setMaxBudget(100);

    // Now create a zero sized resource and add it to the cache.
    resourcePtr = add_new_purgeable_resource(reporter,
                                             sharedContext,
                                             resourceCache,
                                             /*gpuMemorySize=*/0);
    if (!resourcePtr) {
        return;
    }

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 1);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == resourcePtr);

    // Setting the budget down to 0 should not cause the zero sized resource to be purged
    resourceCache->setMaxBudget(0);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 1);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == resourcePtr);

    // Now add a sized resource to cache. Set budget higher again so that it fits
    resourceCache->setMaxBudget(100);

    Resource* sizedResourcePtr = add_new_purgeable_resource(reporter,
                                                            sharedContext,
                                                            resourceCache,
                                                            /*gpuMemorySize=*/1);
    if (!resourcePtr) {
        return;
    }

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 2);
    // Even though the zero sized resource was added to the cache first, the top of the purgeable
    // stack should be the sized resource.
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == sizedResourcePtr);

    // Add another zero sized resource
    resourcePtr = add_new_purgeable_resource(reporter,
                                             sharedContext,
                                             resourceCache,
                                             /*gpuMemorySize=*/0);
    if (!resourcePtr) {
        return;
    }

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 3);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 3);
    // Again the sized resource should still be the top of the purgeable queue
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue() == sizedResourcePtr);

    // If we set the cache budget to 0, it should clear out the sized resource but leave the two
    // zero-sized resources.
    resourceCache->setMaxBudget(0);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->numFindableResources() == 2);
    REPORTER_ASSERT(reporter, resourceCache->topOfPurgeableQueue()->gpuMemorySize() == 0);
}

// Depending on the granularity of the clock for a given device, in the
// GraphitePurgeNotUsedSinceResourcesTest we may end up with times that are all equal which messes
// up the expected behavior of the purge calls. So this helper forces us to return a new time that
// is different from a previous one.
skgpu::StdSteadyClock::time_point force_newer_timepoint(
        const skgpu::StdSteadyClock::time_point& prevTime) {
    auto time = skgpu::StdSteadyClock::now();
    while (time <= prevTime) {
        time = skgpu::StdSteadyClock::now();
    }
    return time;
}

DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(GraphitePurgeNotUsedSinceResourcesTest, reporter, context,
                                   CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ResourceProvider* resourceProvider = recorder->priv().resourceProvider();
    ResourceCache* resourceCache = resourceProvider->resourceCache();
    const SharedContext* sharedContext = resourceProvider->sharedContext();

    // Basic test where we purge 1 resource
    auto beforeTime = skgpu::StdSteadyClock::now();

    auto resourcePtr = add_new_purgeable_resource(reporter,
                                                  sharedContext,
                                                  resourceCache,
                                                  /*gpuMemorySize=*/1);
    if (!resourcePtr) {
        return;
    }

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);

    auto afterTime = force_newer_timepoint(skgpu::StdSteadyClock::now());

    // purging beforeTime should not get rid of the resource
    resourceCache->purgeResourcesNotUsedSince(beforeTime);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);

    // purging at afterTime which is after resource became purgeable should purge it.
    resourceCache->purgeResourcesNotUsedSince(afterTime);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);

    // Test making 2 purgeable resources, but asking to purge on a time between the two.
    Resource* resourcePtr1 = add_new_purgeable_resource(reporter,
                                                       sharedContext,
                                                       resourceCache,
                                                       /*gpuMemorySize=*/1);

    auto betweenTime = force_newer_timepoint(skgpu::StdSteadyClock::now());

    Resource* resourcePtr2 = add_new_purgeable_resource(reporter,
                                                        sharedContext,
                                                        resourceCache,
                                                        /*gpuMemorySize=*/1);

    afterTime = force_newer_timepoint(skgpu::StdSteadyClock::now());

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resourcePtr1));
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resourcePtr2));

    resourceCache->purgeResourcesNotUsedSince(betweenTime);

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resourcePtr2));

    resourceCache->purgeResourcesNotUsedSince(afterTime);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);

    // purgeResourcesNotUsedSince should have no impact on non-purgeable resources
    auto resource = add_new_resource(reporter,
                                     sharedContext,
                                     resourceCache,
                                     /*gpuMemorySize=*/1);
    if (!resource) {
        return;
    }
    resourcePtr = resource.get();

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);

    afterTime = force_newer_timepoint(skgpu::StdSteadyClock::now());
    resourceCache->purgeResourcesNotUsedSince(afterTime);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, !resourceCache->testingInPurgeableQueue(resourcePtr));

    resource.reset();
    // purgeResourcesNotUsedSince should check the mailbox for the returned resource. Though the
    // time is set before that happens so nothing should purge.
    resourceCache->purgeResourcesNotUsedSince(skgpu::StdSteadyClock::now());
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resourcePtr));

    // Now it should be purged since it is already purgeable
    resourceCache->purgeResourcesNotUsedSince(force_newer_timepoint(skgpu::StdSteadyClock::now()));
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);
}

// This test is used to check the case where we call purgeNotUsedSince, which triggers us to return
// resources from mailbox. Even though the returned resources aren't purged by the last used, we
// still end up purging things to get under budget.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(GraphitePurgeNotUsedOverBudgetTest, reporter, context,
                                   CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ResourceProvider* resourceProvider = recorder->priv().resourceProvider();
    ResourceCache* resourceCache = resourceProvider->resourceCache();
    const SharedContext* sharedContext = resourceProvider->sharedContext();

    // set resourceCache budget to 10 for testing.
    resourceCache->setMaxBudget(10);

    // First make a purgeable resources
    auto resourcePtr = add_new_purgeable_resource(reporter,
                                                  sharedContext,
                                                  resourceCache,
                                                  /*gpuMemorySize=*/1);
    if (!resourcePtr) {
        return;
    }

    // Now create a bunch of non purgeable (yet) resources that are not budgeted (i.e. in real world
    // they would be wrapped in an SkSurface or SkImage), but will cause us to go over our budget
    // limit when they do return to cache.

    auto resource1 = add_new_resource(reporter,
                                      sharedContext,
                                      resourceCache,
                                      /*gpuMemorySize=*/15,
                                      skgpu::Budgeted::kNo);

    auto resource2 = add_new_resource(reporter,
                                      sharedContext,
                                      resourceCache,
                                      /*gpuMemorySize=*/16,
                                      skgpu::Budgeted::kNo);

    auto resource3 = add_new_resource(reporter,
                                      sharedContext,
                                      resourceCache,
                                      /*gpuMemorySize=*/3,
                                      skgpu::Budgeted::kNo);

    auto resource1Ptr = resource1.get();
    auto resource2Ptr = resource2.get();
    auto resource3Ptr = resource3.get();

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 4);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 1);

    auto timeBeforeReturningToCache = skgpu::StdSteadyClock::now();

    // Now reset all the non budgeted resources so they return to the cache and become budgeted.
    // Returning to the cache will not immedidately trigger a purgeAsNeededCall.
    resource1.reset();
    resource2.reset();
    resource3.reset();

    resourceCache->forceProcessReturnedResources();

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 4);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 35);
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resourcePtr));
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resource1Ptr));
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resource2Ptr));
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resource3Ptr));

    // Now we call purgeNotUsedSince with timeBeforeReturnToCache. The original resource should get
    // purged because it is older than this time. The three originally non budgeted resources are
    // newer than this time so they won't be purged by the time on this call. However, since we are
    // overbudget it should trigger us to purge the first two of these resources to get us back
    // under.
    resourceCache->purgeResourcesNotUsedSince(timeBeforeReturningToCache);
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, resourceCache->currentBudgetedBytes() == 3);
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resource3Ptr));
}

// Test call purgeResources on the ResourceCache and make sure all unlocked resources are getting
// purged regardless of when they were last used.
DEF_GRAPHITE_TEST_FOR_ALL_CONTEXTS(GraphitePurgeResourcesTest, reporter, context,
                                   CtsEnforcement::kApiLevel_V) {
    std::unique_ptr<Recorder> recorder = context->makeRecorder();
    ResourceProvider* resourceProvider = recorder->priv().resourceProvider();
    ResourceCache* resourceCache = resourceProvider->resourceCache();
    const SharedContext* sharedContext = resourceProvider->sharedContext();

    // set resourceCache budget to 10 for testing.
    resourceCache->setMaxBudget(10);

    // Basic test where we purge 1 resource
    auto resourcePtr = add_new_purgeable_resource(reporter,
                                                  sharedContext,
                                                  resourceCache,
                                                  /*gpuMemorySize=*/1);
    if (!resourcePtr) {
        return;
    }

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);

    // purging should purge the one unlocked resource.
    resourceCache->purgeResources();
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);

    // Test making 2 purgeable resources
    Resource* resourcePtr1 = add_new_purgeable_resource(reporter,
                                                        sharedContext,
                                                        resourceCache,
                                                        /*gpuMemorySize=*/1);

    Resource* resourcePtr2 = add_new_purgeable_resource(reporter,
                                                        sharedContext,
                                                        resourceCache,
                                                        /*gpuMemorySize=*/1);
    if (!resourcePtr1 || !resourcePtr2) {
        return;
    }

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 2);
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resourcePtr1));
    REPORTER_ASSERT(reporter, resourceCache->testingInPurgeableQueue(resourcePtr2));

    resourceCache->purgeResources();
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);

    // purgeResources should have no impact on non-purgeable resources
    auto resource = add_new_resource(reporter,
                                     sharedContext,
                                     resourceCache,
                                     /*gpuMemorySize=*/1);
    if (!resource) {
        return;
    }
    resourcePtr = resource.get();

    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);

    resourceCache->purgeResources();
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 1);
    REPORTER_ASSERT(reporter, !resourceCache->testingInPurgeableQueue(resourcePtr));

    resource.reset();
    resourceCache->purgeResources();
    REPORTER_ASSERT(reporter, resourceCache->getResourceCount() == 0);
}

}  // namespace skgpu::graphite
