BareGit
#include "data_source_sqlite.h"

#include <atomic>
#include <cstdlib>
#include <filesystem>
#include <gtest/gtest.h>
#include <thread>
#include <unistd.h>

namespace
{

TEST(DataSourceSqlite, History)
{
    auto opened = DataSourceSqlite::open(":memory:");
    ASSERT_TRUE(opened.has_value()) << "Open memory database";
    DataSourceInterface& source = **opened;
    ASSERT_TRUE(!source.latest("missing").value()) << "Missing latest result";
    ASSERT_TRUE(source.history("missing", 0, 100).value().empty())
        << "Missing history";
    const std::string service_id = "blog'; DROP TABLE status_history; --";
    const StatusRecord newer{service_id, 2200000000, 5000000000,
                             ProbeStatus::GOOD};
    const StatusRecord older{service_id, 2199999999, 0, ProbeStatus::BAD};
    const StatusRecord tied{service_id, 2200000000, 42, ProbeStatus::OTHER};
    ASSERT_TRUE(source.save(newer).has_value()) << "Save 64-bit fields";
    ASSERT_TRUE(source.save(older).has_value()) << "Save out of order";
    ASSERT_TRUE(source.save(tied).has_value()) << "Retain same-second results";
    ASSERT_TRUE(source.save({"other", 2200000001, 1, ProbeStatus::GOOD})
                .has_value()) << "Save separate service";
    ASSERT_TRUE(source.latest(service_id).value().value() == tied)
        << "Latest uses timestamp and insertion order";
    const auto rows = source.history(service_id, 2199999999, 2200000001)
                          .value();
    ASSERT_TRUE((rows == std::vector<StatusRecord>{older, newer, tied}))
        << "Ordered history and bound service ID";
    ASSERT_TRUE((source.history(service_id, 2199999999, 2200000000).value() ==
                std::vector<StatusRecord>{older})) << "Exclusive upper bound";
    ASSERT_TRUE(source.history(service_id, 0, 0).value().empty())
        << "Empty range";
    ASSERT_TRUE(!source.history(service_id, 1, 0)) << "Reject reversed bounds";
    ASSERT_TRUE(!source.save({"", 0, 0, ProbeStatus::GOOD}))
        << "Reject empty ID";
    ASSERT_TRUE(!source.save({"x", 0, -1, ProbeStatus::GOOD}))
        << "Reject negative duration";
    ASSERT_TRUE(!source.save({"x", 0, 0, static_cast<ProbeStatus>(3)}))
        << "Reject former NA value";
    ASSERT_TRUE(!source.save({"x", 0, 0, static_cast<ProbeStatus>(99)}))
        << "Reject invalid status";
    ASSERT_TRUE(!DataSourceSqlite::open("")) << "Reject empty path";
}

void writeResults(DataSourceInterface& source, std::atomic<bool>& success,
                  int worker)
{
    for(int i = 0; i < 100; ++i)
    {
        if(!source.save({"service", worker * 100 + i, 1,
                         ProbeStatus::GOOD}) || !source.latest("service"))
        {
            success = false;
        }
    }
}

TEST(DataSourceSqlite, ConcurrentAccess)
{
    auto source = DataSourceSqlite::open(":memory:").value();
    std::atomic<bool> success{true};
    std::vector<std::thread> workers;
    for(int worker = 0; worker < 8; ++worker)
    {
        workers.emplace_back(writeResults, std::ref(*source),
                             std::ref(success), worker);
    }
    for(auto& worker : workers)
    {
        worker.join();
    }
    EXPECT_TRUE(success.load()) << "Concurrent reads and writes";
    ASSERT_TRUE(source->history("service", 0, 800).value().size() == 800)
        << "Concurrent writes retain all results";
}

class DataSourceSqliteFile : public testing::Test
{
protected:
    void SetUp() override
    {
        path = (std::filesystem::temp_directory_path() /
                "status_tracker_XXXXXX").string();
        const int descriptor = mkstemp(path.data());
        ASSERT_GE(descriptor, 0);
        close(descriptor);
    }

    void TearDown() override
    {
        std::error_code error;
        std::filesystem::remove(path, error);
        std::filesystem::remove(path + "-wal", error);
        std::filesystem::remove(path + "-shm", error);
    }

    std::string path;
};

TEST_F(DataSourceSqliteFile, Persistence)
{
    const StatusRecord record{"persistent", 123, 456, ProbeStatus::GOOD};
    {
        auto opened = DataSourceSqlite::open(path);
        ASSERT_TRUE(opened) << opened.error().msg();
        auto& source = *opened;
        ASSERT_TRUE(source->save(record));
        auto second = DataSourceSqlite::open(path);
        ASSERT_TRUE(second) << second.error().msg();
        auto latest = (*second)->latest("persistent");
        ASSERT_TRUE(latest);
        ASSERT_TRUE(*latest);
        EXPECT_EQ(**latest, record);
    }
    {
        auto opened = DataSourceSqlite::open(path);
        ASSERT_TRUE(opened) << opened.error().msg();
        auto latest = (*opened)->latest("persistent");
        ASSERT_TRUE(latest);
        ASSERT_TRUE(*latest);
        EXPECT_EQ(**latest, record);
    }
    EXPECT_FALSE(DataSourceSqlite::open(path + "/missing.sqlite"));
}

}