#include <maps/wikimap/mapspro/services/mrc/browser/lib/dataset_holder.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/sequenced_lifetime_guard.h>

#include <maps/libs/concurrent/include/scoped_guard.h>
#include <maps/libs/log8/include/log8.h>

#include <library/cpp/testing/gmock_in_unittest/gmock.h>
#include <library/cpp/testing/unittest/env.h>
#include <library/cpp/testing/unittest/registar.h>

#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <thread>


namespace maps::mrc::browser::tests {


namespace {

class Event {
public:
    Event() = default;

    void set(bool value)
    {
        {
            std::unique_lock lock(mutex_);
            isSet_ = value;
        }
        condVar_.notify_one();
    }

    void wait()
    {
        std::unique_lock lock{mutex_};
        condVar_.wait(lock, [this] { return isSet_; });
    }

    template<class Duration>
    bool waitFor(const Duration& duration)
    {
        std::unique_lock lock{mutex_};
        condVar_.wait_for(lock, duration, [this] { return isSet_; });
        return isSet_;
    }

private:
    bool isSet_{false};
    std::mutex mutex_;
    std::condition_variable condVar_;
};

} // namespace


Y_UNIT_TEST_SUITE(dataset_holder_should)
{

Y_UNIT_TEST(basic_test)
{
    const std::chrono::milliseconds WAIT_PERIOD(1000);
    Event loaderStartedEvent;
    Event loaderMayLoadEvent;
    Event workerShouldFinishEvent;

    struct Dataset : common::SequencedLifetimeGuard<Dataset> {
        std::string val;
        bool operator==(const std::string& rhs) const { return val == rhs; }
    };

    auto loader = [&](const std::string& path) {
        auto result =  std::make_shared<Dataset>();
        result->val = path;
        loaderStartedEvent.set(true);
        loaderMayLoadEvent.wait();
        return result;
    };

    const std::string TEST_VALUE_1 = "test_value_1";
    ReloadableDatasetHolder<Dataset> holder("test_dataset", loader, TEST_VALUE_1);

    std::thread worker(
        [&](){
            do {
                holder.switchToTargetState();
            } while (!workerShouldFinishEvent.waitFor(WAIT_PERIOD));
        }
    );

    concurrent::ScopedGuard onExit(
        [&]{
            workerShouldFinishEvent.set(true);
            worker.join();
        }
    );

    EXPECT_TRUE(holder.currentDatasetPath() == std::nullopt);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_1);
    EXPECT_FALSE(holder.isInTargetState());
    EXPECT_FALSE(holder.isLoaded());
    EXPECT_THROW(holder.dataset(), RuntimeError);

    loaderStartedEvent.wait();
    loaderMayLoadEvent.set(true);
    std::this_thread::sleep_for(WAIT_PERIOD);

    ASSERT_TRUE(holder.currentDatasetPath().has_value());
    EXPECT_EQ(holder.currentDatasetPath().value(), TEST_VALUE_1);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_1);
    EXPECT_TRUE(holder.isInTargetState());
    EXPECT_TRUE(holder.isLoaded());
    EXPECT_EQ(*holder.dataset(), TEST_VALUE_1);

    loaderStartedEvent.set(false);
    loaderMayLoadEvent.set(false);

    const std::string TEST_VALUE_2 = "test_value_2";

    auto dataset1 = holder.dataset();
    holder.setTargetDatasetPath(TEST_VALUE_2, std::nullopt);
    std::this_thread::sleep_for(WAIT_PERIOD);

    // holder should wait till all clients release their dataset refs

    {
        EXPECT_FALSE(loaderStartedEvent.waitFor(WAIT_PERIOD));
        dataset1.reset();
        EXPECT_TRUE(loaderStartedEvent.waitFor(WAIT_PERIOD));
    }

    EXPECT_TRUE(holder.currentDatasetPath() == std::nullopt);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_2);
    EXPECT_FALSE(holder.isInTargetState());
    EXPECT_FALSE(holder.isLoaded());
    EXPECT_THROW(holder.dataset(), RuntimeError);

    loaderMayLoadEvent.set(true);
    std::this_thread::sleep_for(WAIT_PERIOD);

    ASSERT_TRUE(holder.currentDatasetPath().has_value());
    EXPECT_EQ(holder.currentDatasetPath().value(), TEST_VALUE_2);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_2);
    EXPECT_TRUE(holder.isInTargetState());
    EXPECT_TRUE(holder.isLoaded());
    EXPECT_EQ(*holder.dataset(), TEST_VALUE_2);

    loaderStartedEvent.set(false);
    loaderMayLoadEvent.set(false);
    // check switching dataset with delay
    const std::string TEST_VALUE_3 = "test_value_3";
    auto timeToSwitchDataset = std::chrono::steady_clock::now() + WAIT_PERIOD * 5;
    holder.setTargetDatasetPath(TEST_VALUE_3, timeToSwitchDataset);
    std::this_thread::sleep_for(WAIT_PERIOD);

    ASSERT_TRUE(holder.currentDatasetPath().has_value());
    EXPECT_EQ(holder.currentDatasetPath().value(), TEST_VALUE_2);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_3);
    EXPECT_FALSE(holder.isInTargetState());
    EXPECT_TRUE(holder.isLoaded());
    EXPECT_EQ(*holder.dataset(), TEST_VALUE_2);

    loaderStartedEvent.wait();

    EXPECT_TRUE(holder.currentDatasetPath() == std::nullopt);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_3);
    EXPECT_FALSE(holder.isInTargetState());
    EXPECT_FALSE(holder.isLoaded());
    EXPECT_THROW(holder.dataset(), RuntimeError);

    loaderMayLoadEvent.set(true);

    std::this_thread::sleep_for(WAIT_PERIOD);

    ASSERT_TRUE(holder.currentDatasetPath().has_value());
    EXPECT_EQ(holder.currentDatasetPath().value(), TEST_VALUE_3);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_3);
    EXPECT_TRUE(holder.isInTargetState());
    EXPECT_TRUE(holder.isLoaded());
    EXPECT_EQ(*holder.dataset(), TEST_VALUE_3);

    // switch to empty dataset

    holder.setTargetDatasetPath(std::nullopt, std::nullopt);

    std::this_thread::sleep_for(2 * WAIT_PERIOD);

    EXPECT_FALSE(holder.currentDatasetPath().has_value());
    EXPECT_FALSE(holder.targetDatasetPath().has_value());
    EXPECT_TRUE(holder.isInTargetState());
    EXPECT_FALSE(holder.isLoaded());
    EXPECT_THROW(holder.dataset(), RuntimeError);
}

Y_UNIT_TEST(dataset_switch_does_not_block_read_methods)
{
    const std::chrono::milliseconds WAIT_PERIOD(500);
    Event workerShouldFinishEvent;
    const std::string TEST_VALUE_1 = "path1";
    const std::string TEST_VALUE_2 = "path2";

    class Dataset : common::SequencedLifetimeGuard<Dataset> {
    public:
        Dataset(Event& waitForEvent, const std::string& value)
            : waitForEvent_(waitForEvent)
            , value_(value)
        {}

        const std::string& value() const { return value_; }

        ~Dataset()
        {
            waitForEvent_.wait();
        }

    private:
        Event& waitForEvent_;
        std::string value_;
    };

    auto loader = [&](const std::string& value) {
        return std::make_shared<Dataset>(workerShouldFinishEvent, value);
    };

    ReloadableDatasetHolder<Dataset> holder("test_dataset", loader, TEST_VALUE_1);

    std::thread worker(
        [&](){
            do {
                holder.switchToTargetState();
            } while (!workerShouldFinishEvent.waitFor(WAIT_PERIOD));
        }
    );

    concurrent::ScopedGuard onExit(
        [&]{
            workerShouldFinishEvent.set(true);
            worker.join();
        }
    );


    std::this_thread::sleep_for(WAIT_PERIOD);

    ASSERT_TRUE(holder.currentDatasetPath().has_value());
    EXPECT_EQ(holder.currentDatasetPath().value(), TEST_VALUE_1);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_1);
    EXPECT_TRUE(holder.isInTargetState());
    EXPECT_TRUE(holder.isLoaded());
    EXPECT_EQ(holder.dataset()->value(), TEST_VALUE_1);

    holder.setTargetDatasetPath(TEST_VALUE_2, std::nullopt);
    std::this_thread::sleep_for(WAIT_PERIOD);

    ASSERT_FALSE(holder.currentDatasetPath().has_value());
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_2);
    EXPECT_FALSE(holder.isInTargetState());
    EXPECT_FALSE(holder.isLoaded());

    workerShouldFinishEvent.set(true);

    std::this_thread::sleep_for(WAIT_PERIOD);

    ASSERT_TRUE(holder.currentDatasetPath().has_value());
    EXPECT_EQ(holder.currentDatasetPath().value(), TEST_VALUE_2);
    ASSERT_TRUE(holder.targetDatasetPath().has_value());
    EXPECT_EQ(holder.targetDatasetPath().value(), TEST_VALUE_2);
    EXPECT_TRUE(holder.isInTargetState());
    EXPECT_TRUE(holder.isLoaded());
    EXPECT_EQ(holder.dataset()->value(), TEST_VALUE_2);
}

} // Y_UNIT_TEST_SUITE


} // namespace maps::mrc::browser::tests
