#include <gtest/gtest.h>
#include <algorithm>
#include <atomic>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include "renderive/renderable/Renderable.hpp"
#include "renderive/renderable/Renderable_Test_Harness.hpp"
#include "renderive/scene/Scene.hpp"
#include "Scene_Test_Helpers.hpp"

namespace {

class Pipeline_Renderable : public Renderable_Test_Harness {
public:
    explicit Pipeline_Renderable(bool cached = false)
        : Renderable_Test_Harness({.cache_enabled = cached}) {}
    std::function<void()> prepare_action;
    std::function<void()> paint_action;
    std::atomic<int> prepare_count{};
    std::atomic<int> paint_count{};
protected:
    void build_prepare_graph_for_test(Renderable_Graph_Builder& builder) override {
        builder.emplace("prepare", "Prepare",
                        [this](const Prepare_Render_Context&) {
                            ++prepare_count;
                            if (prepare_action)
                                prepare_action();
                        });
    }
    void build_paint_graph_for_test(Renderable_Graph_Builder& builder) override {
        const auto paint = builder.emplace(
            "paint", "Paint",
            [this](const Paint_Render_Context&) {
                ++paint_count;
                if (paint_action)
                    paint_action();
            });
        builder.precede(builder.find("prepare"), paint);
    }
};

class Dynamic_Renderable : public Renderable_Test_Harness {
public:
    explicit Dynamic_Renderable() = default;
    void set_chunk_count(std::size_t count) {
        chunk_count_ = count;
        rebuild_render_graph_for_test();
    }
    std::shared_ptr<const Renderable_Graph> graph_snapshot() {
        return render_graph_for_test();
    }
protected:
    void build_prepare_graph_for_test(Renderable_Graph_Builder& builder) override {
        const auto root = builder.emplace("prepare", "Prepare",
            [](const Prepare_Render_Context&) {});
        for (std::size_t index = 0; index < chunk_count_; ++index) {
            const auto chunk = builder.emplace(
                "chunk:" + std::to_string(index),
                "Chunk " + std::to_string(index),
                [](const Prepare_Render_Context&) {});
            builder.precede(root, chunk);
        }
    }
private:
    std::size_t chunk_count_{2};
};

class Alternate_Frame final : public Abstract_Frame {
};

const Render_Node& node(const Render_Plan& plan, std::uint64_t owner,
                        Render_Node_Kind kind) {
    const auto iterator = std::find_if(plan.graph.nodes.begin(), plan.graph.nodes.end(),
        [owner, kind](const Render_Node& value) {
            return value.owner_id == owner && value.kind == kind;
        });
    if (iterator == plan.graph.nodes.end())
        throw std::logic_error("render node not found");
    return *iterator;
}

bool has_edge(const Render_Plan& plan, Render_Node_Id from, Render_Node_Id to) {
    return std::find(plan.graph.edges.begin(), plan.graph.edges.end(),
                     Render_Edge{from, to}) != plan.graph.edges.end();
}

}

TEST(render_plan_execution_test, dependency_edges_connect_prepare_only_and_layer_edges_connect_composite) {
    Scene2D_Context<> scene;
    auto parent = renderive_Owner<Pipeline_Renderable>::make();
    auto child = renderive_Owner<Pipeline_Renderable>::make();
    with_attach_builder(scene, [&](auto& attach) {
        attach.attach(parent);
        attach.attach(child);
        attach.set_dependency_parent(child, parent);
        attach.set_display_parent(child, parent);
    });
    scene.render();
    scene.wait_for_render();

    const auto plan = scene.render_plan_snapshot();
    ASSERT_TRUE(plan);
    const auto& parent_prepare = node(*plan, parent->renderable_id(), Render_Node_Kind::prepare);
    const auto& parent_paint = node(*plan, parent->renderable_id(), Render_Node_Kind::paint);
    const auto& parent_composite = node(*plan, parent->renderable_id(), Render_Node_Kind::composite);
    const auto& child_prepare = node(*plan, child->renderable_id(), Render_Node_Kind::prepare);
    const auto& child_composite = node(*plan, child->renderable_id(), Render_Node_Kind::composite);
    EXPECT_TRUE(has_edge(*plan, parent_prepare.node_id, child_prepare.node_id));
    EXPECT_FALSE(has_edge(*plan, parent_paint.node_id, child_prepare.node_id));
    EXPECT_TRUE(has_edge(*plan, parent_composite.node_id, child_composite.node_id));
}

#ifndef RENDERIVE_TASKFLOW_MOCK
TEST(render_plan_execution_test, local_paint_can_run_while_an_unrelated_prepare_is_blocked) {
    Scene2D_Context<> scene;
    auto fast = renderive_Owner<Pipeline_Renderable>::make();
    auto slow = renderive_Owner<Pipeline_Renderable>::make();
    std::mutex mutex;
    std::condition_variable condition;
    bool slow_prepare_started{};
    bool release_slow{};
    std::atomic<bool> fast_painted{};
    slow->prepare_action = [&] {
        std::unique_lock lock(mutex);
        slow_prepare_started = true;
        condition.notify_all();
        condition.wait(lock, [&] { return release_slow; });
    };
    fast->paint_action = [&] { fast_painted.store(true, std::memory_order_release); };
    attach_initial(scene, fast, slow);
    scene.render();
    {
        std::unique_lock lock(mutex);
        condition.wait(lock, [&] { return slow_prepare_started; });
    }
    for (int attempt = 0; attempt < 10'000 &&
                          !fast_painted.load(std::memory_order_acquire); ++attempt)
        std::this_thread::yield();
    EXPECT_TRUE(fast_painted.load(std::memory_order_acquire));
    {
        std::lock_guard lock(mutex);
        release_slow = true;
    }
    condition.notify_all();
    scene.wait_for_render();
}

TEST(render_plan_execution_test, dependent_prepare_does_not_wait_for_dependency_paint) {
    Scene2D_Context<> scene;
    auto parent = renderive_Owner<Pipeline_Renderable>::make();
    auto child = renderive_Owner<Pipeline_Renderable>::make();
    std::mutex mutex;
    std::condition_variable condition;
    bool parent_paint_started{};
    bool release_parent_paint{};
    std::atomic<bool> child_prepared{};
    parent->paint_action = [&] {
        std::unique_lock lock(mutex);
        parent_paint_started = true;
        condition.notify_all();
        condition.wait(lock, [&] { return release_parent_paint; });
    };
    child->prepare_action = [&] { child_prepared.store(true, std::memory_order_release); };
    with_attach_builder(scene, [&](auto& attach) {
        attach.attach(parent);
        attach.attach(child);
        attach.set_dependency_parent(child, parent);
    });
    scene.render();
    {
        std::unique_lock lock(mutex);
        condition.wait(lock, [&] { return parent_paint_started; });
    }
    for (int attempt = 0; attempt < 10'000 &&
                          !child_prepared.load(std::memory_order_acquire); ++attempt)
        std::this_thread::yield();
    EXPECT_TRUE(child_prepared.load(std::memory_order_acquire));
    {
        std::lock_guard lock(mutex);
        release_parent_paint = true;
    }
    condition.notify_all();
    scene.wait_for_render();
}
#endif

TEST(render_plan_execution_test, prepare_and_paint_cache_validity_are_independent_and_prune_nodes) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Pipeline_Renderable>::make(true);
    attach_initial(scene, renderable);
    scene.render();
    scene.wait_for_render();
    EXPECT_TRUE(renderable->prepare_cache_valid_for_test());
    EXPECT_TRUE(renderable->paint_cache_valid_for_test());
    const auto first_plan = scene.render_plan_snapshot();
    const Render_Node_Id prepare_id = node(*first_plan, renderable->renderable_id(),
                                           Render_Node_Kind::prepare).node_id;
    const Render_Node_Id paint_id = node(*first_plan, renderable->renderable_id(),
                                         Render_Node_Kind::paint).node_id;

    scene.render();
    scene.wait_for_render();
    const auto cached_plan = scene.render_plan_snapshot();
    EXPECT_GT(cached_plan->version, first_plan->version);
    EXPECT_EQ(std::count_if(cached_plan->graph.nodes.begin(), cached_plan->graph.nodes.end(),
                           [&](const Render_Node& value) {
                               return value.owner_id == renderable->renderable_id() &&
                                      value.kind != Render_Node_Kind::composite;
                           }), 0);

    renderable->invalidate_paint_for_test();
    EXPECT_TRUE(renderable->prepare_cache_valid_for_test());
    EXPECT_FALSE(renderable->paint_cache_valid_for_test());
    scene.render();
    scene.wait_for_render();
    const auto paint_plan = scene.render_plan_snapshot();
    EXPECT_GT(paint_plan->version, cached_plan->version);
    EXPECT_EQ(node(*paint_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id,
              paint_id);
    EXPECT_EQ(std::count_if(paint_plan->graph.nodes.begin(), paint_plan->graph.nodes.end(),
                           [&](const Render_Node& value) {
                               return value.owner_id == renderable->renderable_id() &&
                                      value.kind == Render_Node_Kind::prepare;
                           }), 0);

    renderable->invalidate_prepare_for_test();
    EXPECT_FALSE(renderable->prepare_cache_valid_for_test());
    EXPECT_FALSE(renderable->paint_cache_valid_for_test());
    scene.render();
    scene.wait_for_render();
    const auto rebuilt_plan = scene.render_plan_snapshot();
    EXPECT_GT(rebuilt_plan->version, paint_plan->version);
    EXPECT_EQ(node(*rebuilt_plan, renderable->renderable_id(), Render_Node_Kind::prepare).node_id,
              prepare_id);
    EXPECT_EQ(node(*rebuilt_plan, renderable->renderable_id(), Render_Node_Kind::paint).node_id,
              paint_id);
}

TEST(render_plan_execution_test, logical_node_ids_are_stable_and_retired_ids_are_never_reused) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Dynamic_Renderable>::make();
    attach_initial(scene, renderable);
    const auto first = renderable->graph_snapshot();
    ASSERT_EQ(first->graph.nodes.size(), 3u);
    const Render_Node_Id root = first->graph.nodes[0].node_id;
    const Render_Node_Id first_chunk = first->graph.nodes[1].node_id;
    const Render_Node_Id retired_chunk = first->graph.nodes[2].node_id;

    renderable->set_chunk_count(2);
    scene.render();
    scene.wait_for_render();
    const auto unchanged = renderable->graph_snapshot();
    EXPECT_EQ(unchanged->graph.nodes[0].node_id, root);
    EXPECT_EQ(unchanged->graph.nodes[1].node_id, first_chunk);
    EXPECT_EQ(unchanged->graph.nodes[2].node_id, retired_chunk);

    renderable->set_chunk_count(1);
    scene.render();
    scene.wait_for_render();
    const auto reduced = renderable->graph_snapshot();
    ASSERT_EQ(reduced->graph.nodes.size(), 2u);
    EXPECT_EQ(reduced->graph.nodes[0].node_id, root);
    EXPECT_EQ(reduced->graph.nodes[1].node_id, first_chunk);

    renderable->set_chunk_count(2);
    scene.render();
    scene.wait_for_render();
    const auto expanded = renderable->graph_snapshot();
    ASSERT_EQ(expanded->graph.nodes.size(), 3u);
    EXPECT_EQ(expanded->graph.nodes[0].node_id, root);
    EXPECT_EQ(expanded->graph.nodes[1].node_id, first_chunk);
    EXPECT_NE(expanded->graph.nodes[2].node_id, retired_chunk);
    EXPECT_GT(expanded->graph.nodes[2].node_id, retired_chunk);
}

TEST(render_plan_execution_test, capture_off_keeps_abstract_frame_without_execution_timing) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Pipeline_Renderable>::make();
    attach_initial(scene, renderable);
    Abstract_Frame frame;
    scene.render(frame);
    scene.wait_for_render();
    EXPECT_FALSE(frame.completed_snapshot());
}

TEST(render_plan_execution_test, measures_capture_off_and_on_cost_separately) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Pipeline_Renderable>::make();
    attach_initial(scene, renderable);
    constexpr std::size_t warmup_count = 32;
    constexpr std::size_t frame_count = 512;
    const auto render_frames = [&scene](std::size_t count) {
        const auto start = std::chrono::steady_clock::now();
        for (std::size_t index = 0; index < count; ++index) {
            scene.render();
            scene.wait_for_render();
        }
        return std::chrono::duration_cast<std::chrono::nanoseconds>(
            std::chrono::steady_clock::now() - start).count();
    };
    render_frames(warmup_count);
    const auto capture_off_ns = render_frames(frame_count);
    const Capture_Session_Id session_id = scene.capture_frames(frame_count);
    const auto capture_on_ns = render_frames(frame_count);
    const auto session = scene.capture_session(session_id);
    ASSERT_TRUE(session);
    ASSERT_EQ(session->captured_count(), frame_count);
    const auto capture_off_per_frame = capture_off_ns / frame_count;
    const auto capture_on_per_frame = capture_on_ns / frame_count;
    std::cout << "[ PERF ] capture_off_ns_per_frame=" << capture_off_per_frame
              << " capture_on_ns_per_frame=" << capture_on_per_frame << '\n';
    EXPECT_GT(capture_off_per_frame, 0);
    EXPECT_GT(capture_on_per_frame, 0);
}

TEST(render_plan_execution_test, capture_requests_publish_exact_immutable_abstract_frame_snapshots) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Pipeline_Renderable>::make(true);
    attach_initial(scene, renderable);
    constexpr std::size_t capture_count = 20;
    const Capture_Session_Id session_id = scene.capture_frames(capture_count);
    Alternate_Frame alternate_frame;
    scene.render(alternate_frame);
    scene.wait_for_render();
    for (std::size_t index = 1; index < capture_count + 1; ++index) {
        scene.render();
        scene.wait_for_render();
    }

    const auto session = scene.capture_session(session_id);
    ASSERT_TRUE(session);
    ASSERT_EQ(session->captured_count(), capture_count);
    EXPECT_FALSE(session->active());
    ASSERT_TRUE(alternate_frame.completed_snapshot());
    EXPECT_EQ(alternate_frame.completed_snapshot(), session->frames.front().snapshot);
    ASSERT_GE(session->frames.size(), 2u);
    EXPECT_NE(session->frames[0].snapshot->render_plan_version,
              session->frames[1].snapshot->render_plan_version);
    for (const auto& frame : session->frames) {
        ASSERT_TRUE(frame.snapshot);
        EXPECT_FALSE(frame.snapshot->node_executions.empty());
        EXPECT_EQ(frame.snapshot->render_plan_version,
                  frame.analysis.render_plan_version);
        EXPECT_TRUE(scene.find_render_plan(frame.snapshot->render_plan_version));
    }
}

TEST(render_plan_execution_test, failed_render_discards_abstract_frame_capture_and_retries_ticket) {
    Scene2D_Context<> scene;
    auto renderable = renderive_Owner<Pipeline_Renderable>::make();
    std::atomic<bool> fail_once{true};
    renderable->paint_action = [&] {
        if (fail_once.exchange(false))
            throw std::runtime_error("paint failed");
    };
    attach_initial(scene, renderable);
    const Capture_Session_Id session_id = scene.capture_next_frame();
    Alternate_Frame failed_frame;
    scene.render(failed_frame);
    EXPECT_THROW(scene.wait_for_render(), std::runtime_error);
    EXPECT_FALSE(failed_frame.completed_snapshot());
    EXPECT_TRUE(scene.capture_state().enabled());

    Alternate_Frame completed_frame;
    scene.render(completed_frame);
    scene.wait_for_render();
    const auto session = scene.capture_session(session_id);
    ASSERT_TRUE(session);
    ASSERT_EQ(session->captured_count(), 1u);
    EXPECT_EQ(completed_frame.completed_snapshot(), session->frames.front().snapshot);
    EXPECT_FALSE(scene.capture_state().enabled());
}
