#include <gtest/gtest.h>
#include <cstdint>
#include <memory>
#include "State_Test_Types.hpp"
TEST(double_state_strategy_test, builds_without_state_validator) {
    auto product = State_Plain_Product::Builder{}.set<&State_Plain_Value::count>(10).set<&State_Plain_Value::name>("alpha").build();
    EXPECT_EQ(product->get<&State_Plain_Value::count>(), 10);
    EXPECT_EQ(product->get<&State_Plain_Value::name>(), "alpha");
}
TEST(double_state_strategy_test, calls_optional_state_validator_when_present) {
    auto builder = State_Checked_Product::Builder{}.set<&State_Checked_Value::minimum>(10).set<&State_Checked_Value::maximum>(5);
    EXPECT_THROW(builder.build(1, "invalid"), std::invalid_argument);
}
TEST(double_state_strategy_test, keeps_cache_separate_until_publish) {
    State_Render_State_Reader render;
    auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state");
    product->set<&State_Checked_Value::percent>(60);
    EXPECT_EQ(product->get<&State_Checked_Value::percent>(), 60);
    EXPECT_EQ(render.read(*product).percent.get(), 20);
    product->publish();
    EXPECT_EQ(render.read(*product).percent.get(), 60);
}
TEST(double_state_strategy_test, keeps_acquired_render_reference_stable_until_publish) {
    State_Render_State_Reader render;
    State_Plain_Strategy strategy(State_Plain_Value{.count = 1, .name = "snapshot"});
    const auto snapshot = render.read(strategy);
    strategy.set<&State_Plain_Value::count>(2);
    strategy.publish();
    EXPECT_EQ(snapshot.count, 1);
    EXPECT_EQ(render.read(strategy).count, 2);
}
TEST(double_state_strategy_test, keeps_cached_value_when_runtime_validation_fails) {
    auto product = State_Checked_Product::Builder{}.set<&State_Checked_Value::percent>(20).set<&State_Checked_Value::batch_size>(4).build(1, "state");
    EXPECT_THROW((product->set<&State_Checked_Value::percent>(200)), std::out_of_range);
    EXPECT_EQ(product->get<&State_Checked_Value::percent>(), 20);
}
TEST(double_state_strategy_test, supports_atomic_wait_mutex) {
    State_Render_State_Reader render;
    auto product = State_Wait_Product::Builder{State_Plain_Value{.count = 1, .name = "wait"}}.build();
    product->set<&State_Plain_Value::count>(2);
    product->publish();
    EXPECT_EQ(render.read(*product).count, 2);
}
TEST(double_state_strategy_test, passes_additional_builder_template_arguments) {
    static_assert(std::same_as<typename State_Tagged_Product::Builder::Tag, State_Builder_Tag>);
    static_assert(std::same_as<typename State_Tagged_Product::Builder::Mode, State_Builder_Mode>);
    auto product = State_Tagged_Product::Builder{State_Plain_Value{.count = 3, .name = "tagged"}}.build();
    EXPECT_EQ(product->get<&State_Plain_Value::count>(), 3);
}
struct Double_State_Observer_Test_Time_Source {
    std::uint64_t value{100};
    std::uint64_t now_ns() const noexcept {
        return value;
    }
};
struct Double_State_Observer_Test_Data {
    std::uint64_t event_count{};
    std::uint64_t publish_count{};
    int value{};
};
struct Double_State_Observer_Test_Recorder {
    static constexpr bool enabled = true;
    std::shared_ptr<Double_State_Observer_Test_Data> data{std::make_shared<Double_State_Observer_Test_Data>()};
    template <class Observation>
    void observe(const Observation& observation) noexcept {
        ++data->event_count;
        data->publish_count = observation.publish_count;
        data->value = observation.state.count;
    }
};
TEST(double_state_strategy_test, notifies_observer_for_cache_update_and_publish) {
    using Observer = Observer_State<Double_State_Observer_Test_Recorder, Double_State_Observer_Test_Time_Source>;
    using Strategy = Double_State_Strategy<State_Plain_Base, State_Plain_Value, Atomic_Spin_Mutex, Observer>;
    Double_State_Observer_Test_Recorder recorder;
    Strategy strategy(State_Plain_Value{}, With_Observer(Observer(recorder, Double_State_Observer_Test_Time_Source{})));
    strategy.set<&State_Plain_Value::count>(8);
    strategy.publish();
    EXPECT_EQ(recorder.data->event_count, 2);
    EXPECT_EQ(recorder.data->publish_count, 1);
    EXPECT_EQ(recorder.data->value, 8);
}
struct Double_State_Throwing_Assignment_State {
    int first{};
    int second{};
    Double_State_Throwing_Assignment_State() = default;
    Double_State_Throwing_Assignment_State(int first, int second) : first(first), second(second) {}
    Double_State_Throwing_Assignment_State(const Double_State_Throwing_Assignment_State&) = default;
    Double_State_Throwing_Assignment_State(Double_State_Throwing_Assignment_State&&) noexcept = default;
    Double_State_Throwing_Assignment_State& operator=(const Double_State_Throwing_Assignment_State& other) {
        first = other.first;
        if (throw_on_copy_assignment) {
            throw std::runtime_error("state assignment failed");
        }
        second = other.second;
        return *this;
    }
    Double_State_Throwing_Assignment_State& operator=(Double_State_Throwing_Assignment_State&&) noexcept = default;
    inline static bool throw_on_copy_assignment{};
};
TEST(double_state_strategy_test, failed_publish_keeps_render_state_and_revision_unchanged) {
    State_Render_State_Reader render;
    using Strategy = Double_State_Strategy<State_Plain_Base, Double_State_Throwing_Assignment_State>;
    Double_State_Throwing_Assignment_State::throw_on_copy_assignment = false;
    Strategy strategy(Double_State_Throwing_Assignment_State(1, 2));
    strategy.set<&Double_State_Throwing_Assignment_State::first>(10);
    strategy.set<&Double_State_Throwing_Assignment_State::second>(20);
    Double_State_Throwing_Assignment_State::throw_on_copy_assignment = true;
    EXPECT_THROW(strategy.publish(), std::runtime_error);
    Double_State_Throwing_Assignment_State::throw_on_copy_assignment = false;
    EXPECT_EQ(strategy.state_revision(), 0);
    EXPECT_EQ(render.read(strategy).first, 1);
    EXPECT_EQ(render.read(strategy).second, 2);
    strategy.publish();
    EXPECT_EQ(strategy.state_revision(), 1);
    EXPECT_EQ(render.read(strategy).first, 10);
    EXPECT_EQ(render.read(strategy).second, 20);
}
