#include <gtest/gtest.h>
#include <atomic>
#include <memory>
#include <thread>
#include <utility>
#include "renderive/renderable/Renderable.hpp"
#include "renderive/renderable/Renderable_Test_Harness.hpp"
#include "renderive/scene/Scene.hpp"
#include "Scene_Test_Helpers.hpp"
static_assert(std::same_as<decltype(std::declval<const Scene2D_Context<>&>().topology_snapshot()), Scene_Base::Topology_Snapshot>);
struct Scene2D_Context_Test_Renderable : Renderable_Test_Harness {
    explicit Scene2D_Context_Test_Renderable()
        : Renderable_Test_Harness({.cache_enabled = false}) {}
    void prepare_for_test(const Prepare_Render_Context&) override {
        ++render_count;
        scene_is_2d = dynamic_cast<Scene_2D_Base*>(&scene_for_test()) != nullptr;
        render_thread = std::this_thread::get_id();
    }
    std::atomic<int> render_count{};
    bool scene_is_2d{};
    std::thread::id render_thread;
};
TEST(scene2d_context_test, swaps_renderable_cache_and_renders_in_background) {
    Scene2D_Context<> scene;
    const auto caller_thread = std::this_thread::get_id();
    auto first = renderive_Owner<Scene2D_Context_Test_Renderable>::make();
    auto second = renderive_Owner<Scene2D_Context_Test_Renderable>::make();
    attach_initial(scene, first);
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(first->render_count.load(), 1);
    EXPECT_TRUE(first->scene_is_2d);
    EXPECT_NE(first->render_thread, caller_thread);
    wait_renderable_edit(scene, [first, second](auto& editor) {
        editor.detach(first);
        editor.attach(second);
    });
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(first->render_count.load(), 1);
    EXPECT_EQ(second->render_count.load(), 1);
    EXPECT_EQ(scene.frame_control_strategy().frame_control_state().publish_revision, 2);
}
struct Scene2D_Final_Cache_Blocking_Renderable : Renderable_Test_Harness {
    explicit Scene2D_Final_Cache_Blocking_Renderable()
        : Renderable_Test_Harness({.cache_enabled = false}) {}
    void prepare_for_test(const Prepare_Render_Context&) override {
        while (!release.load(std::memory_order_acquire)) {
            started.store(true, std::memory_order_release);
            std::this_thread::yield();
        }
    }
    std::atomic<bool> started{};
    std::atomic<bool> release{};
};
TEST(scene2d_context_test, final_color_cache_access_waits_for_render_completion) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Scene2D_Final_Cache_Blocking_Renderable>::make();
    attach_initial(scene, renderable);
    scene.render();
    while (!renderable->started.load(std::memory_order_acquire)) {
        std::this_thread::yield();
    }
    std::atomic<bool> access_started{};
    std::atomic<bool> cache_accessed{};
    std::thread reader([&] {
        access_started.store(true, std::memory_order_release);
        scene.with_final_color_cache([&](const Recording_Color_Cache&) {
            cache_accessed.store(true, std::memory_order_release);
        });
    });
    while (!access_started.load(std::memory_order_acquire)) {
        std::this_thread::yield();
    }
    EXPECT_FALSE(cache_accessed.load(std::memory_order_acquire));
    renderable->release.store(true, std::memory_order_release);
    reader.join();
    EXPECT_TRUE(cache_accessed.load(std::memory_order_acquire));
}
struct Scene2D_Cached_Renderable : Renderable_Test_Harness {
    Scene2D_Cached_Renderable(std::uint64_t value)
        : Renderable_Test_Harness({.cache_enabled = true}), value(value) {}
    void prepare_for_test(const Prepare_Render_Context&) override {
        ++render_count;
    }
    void build_paint_graph_for_test(Renderable_Graph_Builder& graph) override {
        graph.emplace("paint", "Paint",
                      [this](const Paint_Render_Context& context) {
        dynamic_cast<Recording_Color_Cache&>(context.color_cache).append(value);
        });
    }
    std::uint64_t value;
    int render_count{};
};
TEST(scene2d_context_test, reattached_cached_renderable_rebuilds_removed_color_cache) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Scene2D_Cached_Renderable>::make(7);
    attach_initial(scene, renderable);
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(renderable->render_count, 1);
    wait_renderable_edit(scene, [renderable](auto& editor) {
        editor.detach(renderable);
        editor.attach(renderable);
    });
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(renderable->render_count, 2);
    scene.with_final_color_cache([](const Recording_Color_Cache& cache) {
        EXPECT_EQ(cache.values.size(), 1);
        EXPECT_EQ(cache.values.at(0), 7);
    });
}
TEST(scene2d_context_test, dependency_reparent_invalidates_cached_child) {
    Scene2D_Context<> scene;
    auto first_parent = renderive_Owner<Scene2D_Cached_Renderable>::make(1);
    auto second_parent = renderive_Owner<Scene2D_Cached_Renderable>::make(2);
    auto child = renderive_Owner<Scene2D_Cached_Renderable>::make(3);
    with_attach_builder(scene, [&](auto& attach) {
        attach.attach(first_parent);
        attach.attach(second_parent);
        attach.attach(child);
        attach.set_dependency_parent(child, first_parent);
    });
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(first_parent->render_count, 1);
    EXPECT_EQ(second_parent->render_count, 1);
    EXPECT_EQ(child->render_count, 1);
    wait_renderable_edit(scene, [child, second_parent](auto& editor) {
        editor.set_dependency_parent(child, second_parent);
    });
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(first_parent->render_count, 1);
    EXPECT_EQ(second_parent->render_count, 1);
    EXPECT_EQ(child->render_count, 2);
}
TEST(scene2d_context_test, multiple_dependency_parents_invalidate_cached_child) {
    Scene2D_Context<> scene;
    auto first_parent = renderive_Owner<Scene2D_Cached_Renderable>::make(1);
    auto second_parent = renderive_Owner<Scene2D_Cached_Renderable>::make(2);
    auto child = renderive_Owner<Scene2D_Cached_Renderable>::make(3);
    with_attach_builder(scene, [&](auto& attach) {
        attach.attach(first_parent);
        attach.attach(second_parent);
        attach.attach(child);
        attach.set_dependency_parent(child, first_parent);
        attach.add_dependency_parent(child, second_parent);
    });
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(child->render_count, 1);
    first_parent->invalidate_prepare_for_test();
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(child->render_count, 2);
    second_parent->invalidate_prepare_for_test();
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(child->render_count, 3);
    int dependency_edges{};
    for (const auto& relation : scene.topology_snapshot().dependency) {
        if (relation.child.get() == child.get() && relation.parent) {
            ++dependency_edges;
        }
    }
    EXPECT_EQ(dependency_edges, 2);
}
TEST(scene2d_context_test, final_color_cache_callback_can_reenter_scene_control_api) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Scene2D_Cached_Renderable>::make(9);
    attach_initial(scene, renderable);
    scene.render();
    scene.wait_for_render();
    scene.with_final_color_cache([&](const Recording_Color_Cache&) {
        renderable->set_configuration_for_test({.cache_enabled = false});
    });
    EXPECT_FALSE(renderable->configuration_for_test().cache_enabled);
}
TEST(scene2d_context_test, detach_dependency_parent_invalidates_promoted_cached_child) {
    Scene2D_Context<> scene;
    auto grandparent = renderive_Owner<Scene2D_Cached_Renderable>::make(1);
    auto parent = renderive_Owner<Scene2D_Cached_Renderable>::make(2);
    auto child = renderive_Owner<Scene2D_Cached_Renderable>::make(3);
    with_attach_builder(scene, [&](auto& attach) {
        attach.attach(grandparent);
        attach.attach(parent);
        attach.attach(child);
        attach.set_dependency_parent(parent, grandparent);
        attach.set_dependency_parent(child, parent);
    });
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(grandparent->render_count, 1);
    EXPECT_EQ(parent->render_count, 1);
    EXPECT_EQ(child->render_count, 1);
    wait_renderable_edit(scene, [grandparent, parent, child](auto& editor) {
        editor.clear_dependency_parent(child);
        editor.clear_dependency_parent(parent);
        editor.set_dependency_parent(child, grandparent);
        editor.detach(parent);
    });
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(grandparent->render_count, 1);
    EXPECT_EQ(parent->render_count, 1);
    EXPECT_EQ(child->render_count, 2);
    const auto topology = scene.topology_snapshot();
    for (const auto& relation : topology.dependency) {
        if (relation.child.get() == child.get()) {
            EXPECT_EQ(relation.parent.get(), grandparent.get());
        }
    }
}
struct Scene2D_Attach_Throwing_Cache : Color_Cache {
    explicit Scene2D_Attach_Throwing_Cache(std::pmr::memory_resource&) {
        if (++construction_count == throw_on_construction) {
            throw std::runtime_error("cache construction failed");
        }
    }
    void clear() override {}
    void composite(const Color_Cache&) override {}
    inline static int construction_count{};
    inline static int throw_on_construction{};
};
TEST(scene2d_context_test, failed_attach_leaves_renderable_fully_detached) {
    Scene2D_Attach_Throwing_Cache::construction_count = 0;
    Scene2D_Attach_Throwing_Cache::throw_on_construction = 2;
    Scene2D_Context<Low_Latency_Strategy<Scene2D_Frame_Data>, Scene2D_Attach_Throwing_Cache> scene;
    auto renderable = renderive_Owner<Scene2D_Context_Test_Renderable>::make();
    EXPECT_THROW(attach_initial(scene, renderable), std::runtime_error);
    EXPECT_EQ(scene.renderable_count(), 0);
    EXPECT_TRUE(scene.topology_snapshot().renderables.empty());
    Scene2D_Attach_Throwing_Cache::throw_on_construction = 0;
    EXPECT_NO_THROW(attach_initial(scene, renderable));
    EXPECT_EQ(scene.renderable_count(), 1);
}
struct Scene2D_Viewport_Snapshot_Renderable : Renderable_Test_Harness {
    explicit Scene2D_Viewport_Snapshot_Renderable() = default;
    void prepare_for_test(const Prepare_Render_Context& context) override {
        viewport = context.frame.viewport;
    }
    Frame_Viewport viewport;
};
TEST(scene2d_context_test, published_scene_viewport_is_frozen_for_render) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Scene2D_Viewport_Snapshot_Renderable>::make();
    attach_initial(scene, renderable);
    scene.Scene_State_Strategy::set<&Scene2D_State::viewport>(Frame_Viewport{320, 180});
    scene.publish_frame_state();
    scene.Scene_State_Strategy::set<&Scene2D_State::viewport>(Frame_Viewport{640, 360});
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(renderable->viewport, (Frame_Viewport{320, 180}));
    scene.publish_frame_state();
    renderable->invalidate_prepare_for_test();
    scene.render();
    scene.wait_for_render();
    EXPECT_EQ(renderable->viewport, (Frame_Viewport{640, 360}));
}
