#include <gtest/gtest.h>
#include <cmath>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <atomic>
#include "renderive/renderable/Renderable.hpp"
#include "renderive/renderable/Renderable_Test_Harness.hpp"
#include "renderive/scene/Scene.hpp"
struct Renderable_Base_Test_Renderable : Renderable_Test_Harness {
    Renderable_Base_Test_Renderable(bool cache_enabled)
        : Renderable_Test_Harness({.cache_enabled = cache_enabled}) {}
    void prepare_for_test(const Prepare_Render_Context&) override {
        ++render_count;
    }
    int render_count{};
};
TEST(renderable_base_test, implementation_binds_scene_and_strategy) {
    Scene2D_Context<> low_latency_scene;
    Scene2D_Context<Manual_Refresh_Strategy<Scene2D_Frame_Data>> manual_scene;
    auto low_latency_renderable = renderive_Owner<Renderable_Base_Test_Renderable>::make(true);
    auto manual_renderable = renderive_Owner<Renderable_Base_Test_Renderable>::make(true);
    {
        auto attach = low_latency_scene.attach_builder();
        attach.attach(low_latency_renderable);
    }
    {
        auto attach = manual_scene.attach_builder();
        attach.attach(manual_renderable);
    }
    EXPECT_EQ(&low_latency_renderable->scene_for_test(), &low_latency_scene);
    EXPECT_EQ(&low_latency_renderable->scene_for_test().frame_control_strategy(),
              &low_latency_scene.frame_control);
    EXPECT_DOUBLE_EQ(low_latency_renderable->scene_for_test()
                         .frame_control_strategy()
                         .frequency_hz(),
                     60.0);
    EXPECT_TRUE(std::isnan(manual_renderable->scene_for_test()
                               .frame_control_strategy()
                               .frequency_hz()));
}
TEST(renderable_base_test, controls_cache_with_configuration) {
    Scene2D_Context<> scene;
    auto cached = renderive_Owner<Renderable_Base_Test_Renderable>::make(true);
    auto uncached = renderive_Owner<Renderable_Base_Test_Renderable>::make(false);
    {
        auto attach = scene.attach_builder();
        attach.attach(cached);
        attach.attach(uncached);
    }
    scene.render();
    scene.wait_for_render();
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(cached->render_count, 1);
    EXPECT_EQ(uncached->render_count, 2);
    cached->invalidate_prepare_for_test();
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(cached->render_count, 2);
    EXPECT_EQ(uncached->render_count, 3);
}
TEST(renderable_base_test, changes_configuration_through_scene) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Renderable_Base_Test_Renderable>::make(false);
    {
        auto attach = scene.attach_builder();
        attach.attach(renderable);
    }
    scene.render();
    scene.wait_for_render();
    renderable->set_configuration_for_test({.cache_enabled = true});
    scene.render();
    scene.wait_for_render();
    EXPECT_TRUE(renderable->configuration_for_test().cache_enabled);
    EXPECT_EQ(renderable->render_count, 2);
}
struct Renderable_Base_Cache_Invalidation_Test_Renderable : Renderable_Test_Harness {
    explicit Renderable_Base_Cache_Invalidation_Test_Renderable()
        : Renderable_Test_Harness({.cache_enabled = true}) {}
    void prepare_for_test(const Prepare_Render_Context&) override {
        const int count = render_count.fetch_add(1, std::memory_order_acq_rel) + 1;
        if (count != 1) {
            return;
        }
        std::unique_lock lock(mutex);
        started = true;
        condition.notify_all();
        condition.wait(lock, [this] {
            return released;
        });
    }
    void wait_started() {
        std::unique_lock lock(mutex);
        condition.wait(lock, [this] {
            return started;
        });
    }
    void release() {
        std::lock_guard lock(mutex);
        released = true;
        condition.notify_all();
    }
    std::atomic<int> render_count{};
    std::mutex mutex;
    std::condition_variable condition;
    bool started{};
    bool released{};
};
TEST(renderable_base_test, preserves_cache_invalidation_that_happens_during_render) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Renderable_Base_Cache_Invalidation_Test_Renderable>::make();
    {
        auto attach = scene.attach_builder();
        attach.attach(renderable);
    }
    scene.render();
    renderable->wait_started();
    renderable->invalidate_prepare_for_test();
    renderable->release();
    scene.wait_for_render();
    EXPECT_FALSE(renderable->prepare_cache_valid_for_test());
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(renderable->render_count.load(std::memory_order_acquire), 2);
    EXPECT_TRUE(renderable->prepare_cache_valid_for_test());
}
struct Renderable_Base_Render_Graph_Rebuild_Test_Renderable : Renderable_Test_Harness {
    explicit Renderable_Base_Render_Graph_Rebuild_Test_Renderable() = default;
    void build_prepare_graph_for_test(Renderable_Graph_Builder& graph) override {
        const int count = build_count.fetch_add(1, std::memory_order_acq_rel) + 1;
        if (count == 1) {
            std::unique_lock lock(mutex);
            build_started = true;
            condition.notify_all();
            condition.wait(lock, [this] {
                return release_build;
            });
        }
        graph.emplace("prepare", "graph_rebuild_test",
                      [](const Prepare_Render_Context&) {});
    }
    void wait_build_started() {
        std::unique_lock lock(mutex);
        condition.wait(lock, [this] {
            return build_started;
        });
    }
    void release() {
        std::lock_guard lock(mutex);
        release_build = true;
        condition.notify_all();
    }
    std::shared_ptr<const Renderable_Graph> graph_snapshot() {
        return render_graph_for_test();
    }
    void request_graph_rebuild() {
        rebuild_render_graph_for_test();
    }
    std::atomic<int> build_count{};
    std::mutex mutex;
    std::condition_variable condition;
    bool build_started{};
    bool release_build{};
};
TEST(renderable_base_test, preserves_rebuild_request_that_arrives_during_render_graph_build) {
    Renderable_Base_Render_Graph_Rebuild_Test_Renderable renderable;
    std::shared_ptr<const Renderable_Graph> first_graph;
    std::thread builder([&] {
        first_graph = renderable.graph_snapshot();
    });
    renderable.wait_build_started();
    std::thread rebuilder([&] {
        renderable.request_graph_rebuild();
    });
    renderable.release();
    builder.join();
    rebuilder.join();
    const auto second_graph = renderable.graph_snapshot();
    EXPECT_EQ(renderable.build_count.load(std::memory_order_acquire), 2);
    EXPECT_NE(first_graph.get(), second_graph.get());
}
TEST(renderable_base_test, render_graph_snapshots_remain_valid_during_concurrent_rebuilds) {
    Renderable_Base_Render_Graph_Rebuild_Test_Renderable renderable;
    renderable.release();
    std::atomic<bool> running{true};
    std::thread rebuilder([&] {
        for (int index = 0; index < 500; ++index) {
            renderable.request_graph_rebuild();
        }
        running.store(false, std::memory_order_release);
    });
    while (running.load(std::memory_order_acquire)) {
        const auto graph = renderable.graph_snapshot();
        ASSERT_TRUE(graph);
        EXPECT_EQ(graph->graph.nodes.size(), 1);
    }
    rebuilder.join();
    const auto graph = renderable.graph_snapshot();
    ASSERT_TRUE(graph);
    EXPECT_EQ(graph->graph.nodes.size(), 1);
}
TEST(renderable_base_test, configuration_snapshot_is_safe_during_scene_updates) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Renderable_Base_Test_Renderable>::make(false);
    {
        auto attach = scene.attach_builder();
        attach.attach(renderable);
    }
    std::atomic<bool> running{true};
    std::thread writer([&] {
        for (int index = 0; index < 1000; ++index) {
            renderable->set_configuration_for_test(
                {.cache_enabled = index % 2 != 0});
        }
        running.store(false, std::memory_order_release);
    });
    while (running.load(std::memory_order_acquire)) {
        const auto configuration = renderable->configuration_for_test();
        static_cast<void>(configuration.cache_enabled);
    }
    writer.join();
    EXPECT_TRUE(renderable->configuration_for_test().cache_enabled);
}
