#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <cstdint>
#include <memory>
#include <thread>
#include <vector>
#include "renderive/base/observer/Observer.hpp"
struct Observer_Test_Observation {
    int value{};
};
struct Observer_Test_Time_Source {
    std::shared_ptr<std::atomic<std::uint64_t>> time{std::make_shared<std::atomic<std::uint64_t>>(10)};
    std::uint64_t now_ns() const noexcept {
        return time->fetch_add(5, std::memory_order_relaxed);
    }
};
struct Observer_Test_Recorder {
    static constexpr bool enabled = true;
    std::shared_ptr<int> value{std::make_shared<int>()};
    void observe(const Observer_Test_Observation& observation) noexcept {
        *value = observation.value;
    }
};
using Observer_Test_State = Observer_State<Observer_Test_Recorder, Observer_Test_Time_Source>;
static_assert(Observation_Struct<Observer_Test_Observation>);
static_assert(Observer_Time_Source<Observer_Test_Time_Source>);
static_assert(Struct_Observer<Observer_Test_Recorder, Observer_Test_Observation>);
static_assert(Timed_Struct_Observer<Observer_Test_State, Observer_Test_Observation>);
TEST(observer_state_test, forwards_observation_structure) {
    Observer_Test_Recorder recorder;
    Observer_Test_State observer(recorder, Observer_Test_Time_Source{});
    observer.observe(Observer_Test_Observation{7});
    EXPECT_EQ(*recorder.value, 7);
}
TEST(observer_state_test, owns_time_source) {
    Observer_Test_State observer;
    EXPECT_EQ(observer.now_ns(), 10);
    EXPECT_EQ(observer.now_ns(), 15);
}
TEST(disabled_observer_test, accepts_any_observation_structure) {
    Observer_State<> observer;
    EXPECT_NO_THROW(observer.observe(Observer_Test_Observation{7}));
    EXPECT_GT(observer.now_ns(), 0);
}
struct Observer_Test_Concurrent_Time_Source_Data {
    std::atomic<int> active{};
    std::atomic<bool> overlap{};
    std::uint64_t value{};
};
struct Observer_Test_Concurrent_Time_Source {
    std::shared_ptr<Observer_Test_Concurrent_Time_Source_Data> data{std::make_shared<Observer_Test_Concurrent_Time_Source_Data>()};
    std::uint64_t now_ns() const noexcept {
        if (data->active.fetch_add(1, std::memory_order_acq_rel) != 0) {
            data->overlap.store(true, std::memory_order_release);
        }
        std::this_thread::sleep_for(std::chrono::milliseconds(2));
        const auto value = data->value++;
        data->active.fetch_sub(1, std::memory_order_acq_rel);
        return value;
    }
};
TEST(observer_state_test, serializes_time_source_access) {
    Observer_Test_Concurrent_Time_Source time_source;
    const auto data = time_source.data;
    Observer_State<Disabled_Observer, Observer_Test_Concurrent_Time_Source> observer(Disabled_Observer{}, std::move(time_source));
    std::atomic<bool> start{};
    std::vector<std::thread> threads;
    for (int index = 0; index < 8; ++index) {
        threads.emplace_back([&] {
            while (!start.load(std::memory_order_acquire)) {
                std::this_thread::yield();
            }
            observer.now_ns();
        });
    }
    start.store(true, std::memory_order_release);
    for (auto& thread : threads) {
        thread.join();
    }
    EXPECT_FALSE(data->overlap.load(std::memory_order_acquire));
    EXPECT_EQ(data->value, 8);
}
struct Observer_Test_Throwing_Move_Time_Source {
    Observer_Test_Throwing_Move_Time_Source() = default;
    Observer_Test_Throwing_Move_Time_Source(Observer_Test_Throwing_Move_Time_Source&&) {
        throw std::runtime_error("time source move failed");
    }
    Observer_Test_Throwing_Move_Time_Source& operator=(Observer_Test_Throwing_Move_Time_Source&&) = delete;
    std::uint64_t now_ns() const noexcept {
        return 0;
    }
};
TEST(observer_state_test, move_constructor_propagates_throwing_time_source_move) {
    using State = Observer_State<Disabled_Observer, Observer_Test_Throwing_Move_Time_Source>;
    static_assert(std::move_constructible<Observer_Test_Throwing_Move_Time_Source>);
    static_assert(!std::is_nothrow_move_constructible_v<State>);
    State source;
    EXPECT_THROW(State(std::move(source)), std::runtime_error);
}
