#include <library/cpp/testing/gtest/gtest.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/introspection/include/comparison.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/opencv.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/queued_photo_id_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ride_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/toloka/task_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/track_point_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/ugc_account_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/object/include/mock_loader.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/feature_publisher/lib/context.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ride_inspector/lib/hypotheses.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ride_inspector/lib/process_queue.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ride_inspector/lib/ride.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/ride_inspector/lib/utility.h>
#include <yandex/maps/mrc/unittest/database_fixture.h>
#include <yandex/maps/mrc/unittest/local_server.h>

#include <random>

using namespace std::literals::chrono_literals;

namespace maps::mrc::db {
using introspection::operator==;
}  // namespace maps::mrc::db

namespace maps::mrc::ride_inspector::tests {

constexpr double BAD_QUALITY = 0.04;
constexpr double GOOD_QUALITY = 0.5;
constexpr double ROAD_PROBABILITY = 1.0;
constexpr double NOT_ROAD_PROBABILITY = 0.0;
constexpr double FORBIDDEN_CONTENT = 1.0;
constexpr double NOT_FORBIDDEN_CONTENT = 0.0;

const std::string SOURCE_ID = "iPhone100500";
const std::string USER_ID = "Rene Raymond";
const std::string CLIENT_RIDE_ID = "b0c2d8c8-6fc6-45d0-9e8e-45e37bd29636";
const std::string TEST_PHOTO = common::getTestImage<std::string>();
const std::string TEST_GRAPH_PATH = BinaryPath("maps/data/test/graph3");
const std::string TEST_GEOID_PATH =
    BinaryPath("maps/data/test/geoid/geoid.mms.1");

db::TrackPoints makeTrackPoints(chrono::TimePoint date)
{
    db::TrackPoints result;
    result.emplace_back()
        .setSourceId(SOURCE_ID)
        .setTimestamp(date - 1s)
        .setGeodeticPos(geolib3::Point2(37.669884, 55.728137));
    result.emplace_back()
        .setSourceId(SOURCE_ID)
        .setTimestamp(date)
        .setGeodeticPos(geolib3::Point2(37.670128, 55.728180));
    result.emplace_back()
        .setSourceId(SOURCE_ID)
        .setTimestamp(date + 1s)
        .setGeodeticPos(geolib3::Point2(37.670373, 55.7282245));
    return result;
}

db::Feature makePhoto(boost::optional<double> quality,
                      boost::optional<double> roadProbability,
                      boost::optional<double> forbiddenProbability,
                      chrono::TimePoint date)
{
    auto gtwAccess = sql_chemistry::GatewayAccess<db::Feature>::construct()
                         .setDataset(db::Dataset::Rides)
                         .setSourceId(SOURCE_ID)
                         .setTimestamp(date)
                         .setCameraDeviation(db::CameraDeviation::Front)
                         .setUserId(USER_ID);
    if (quality) {
        gtwAccess.setQuality(quality.value());
    }
    if (roadProbability) {
        gtwAccess.setRoadProbability(roadProbability.value());
    }
    if (forbiddenProbability) {
        gtwAccess.setForbiddenProbability(forbiddenProbability.value());
    }
    return gtwAccess;
}

struct MockCameraDeviationClassifier
    : feature_publisher::ICameraDeviationClassifier {
    MOCK_METHOD(db::CameraDeviation,
                evalForPassage,
                (db::Features::iterator,
                 db::Features::iterator,
                 feature_publisher::LoadImageFn),
                (override));
};

struct RideInspectorTest : testing::Test {
    using TestFixture = unittest::WithUnittestConfig<unittest::DatabaseFixture,
                                                     unittest::MdsStubFixture>;

    RideInspectorTest()
    {
        testFixture_.reset(new TestFixture);
        ctx_.reset(new Context(testFixture_->config()));
        auto loader = object::MockLoader{};
        featurePublisherCtx_.reset(new feature_publisher::Context{
            testFixture_->config(),
            TEST_GRAPH_PATH,
            TEST_GRAPH_PATH,
            privacy::makeCachingRegionPrivacy(loader, TEST_GEOID_PATH),
            std::make_unique<MockCameraDeviationClassifier>()});
    }

    void insertTrackPoints(chrono::TimePoint date)
    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        auto trackPoints = makeTrackPoints(date);
        db::TrackPointGateway{*txn}.insert(trackPoints);
        txn->commit();
    }

    db::TId insertPhoto(boost::optional<double> quality,
                        boost::optional<double> roadProbability,
                        boost::optional<double> forbiddenProbability,
                        chrono::TimePoint date)
    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        mds::Mds mds(testFixture_->config().makeMdsClient());
        auto photo =
            makePhoto(quality, roadProbability, forbiddenProbability, date);
        auto resp =
            mds.post("mrc/test" + std::to_string(++photosNumber_), TEST_PHOTO);
        photo.setMdsKey(resp.key());
        db::FeatureGateway{*txn}.insert(photo);
        db::QueuedPhotoIdGateway{*txn}.insert(db::rides::QueuedPhotoId{
            photo.id(), chrono::TimePoint::clock::now()});
        txn->commit();
        return photo.id();
    }

    void fillData(double quality,
                  double roadProbability,
                  double forbiddenProbability)
    {
        auto date = chrono::TimePoint::clock::now() - 1min;
        insertTrackPoints(date);
        insertPhoto(quality, roadProbability, forbiddenProbability, date);
        // Photos without quality or roadProbability or forbiddenProbability
        // shall be ignored
        insertPhoto(boost::none, boost::none, boost::none, date);
        insertPhoto(boost::none, roadProbability, forbiddenProbability, date);
        insertPhoto(quality, boost::none, forbiddenProbability, date);
        insertPhoto(quality, roadProbability, boost::none, date);
    }

    size_t shouldBePublishedFeatureCount()
    {
        auto txn = testFixture_->pool().masterReadOnlyTransaction();
        return db::FeatureGateway{*txn}
            .load(db::table::Feature::shouldBePublished.is(true))
            .size();
    }

    void processQueue()
    {
        featurePublisherCtx_->processClassifiedPhotos();
        ride_inspector::processQueue(*ctx_);
    }

    void publish()
    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        auto gtw = db::FeatureGateway{*txn};
        auto photos = gtw.load(db::table::Feature::shouldBePublished.is(true));
        for (auto& photo : photos) {
            photo.setIsPublished(true);
        }
        gtw.update(photos, db::UpdateFeatureTxn::Yes);
        txn->commit();
    }

    size_t secretFeatureCount()
    {
        auto txn = testFixture_->pool().masterReadOnlyTransaction();
        return db::FeatureGateway{*txn}
            .load(db::table::Feature::privacy == db::FeaturePrivacy::Secret)
            .size();
    }

    size_t queueSize()
    {
        auto txn = testFixture_->pool().masterReadOnlyTransaction();
        return db::QueuedPhotoIdGateway{*txn}.count();
    }

    size_t featureTransactionCount()
    {
        auto txn = testFixture_->pool().masterReadOnlyTransaction();
        return db::FeatureTransactionGateway{*txn}.count();
    }

    size_t processedFeatureCount()
    {
        auto txn = testFixture_->pool().masterReadOnlyTransaction();
        return db::FeatureGateway{*txn}.count(
            db::table::Feature::processedAt.isNotNull());
    }

    db::Feature insertPublishedPhoto(
        chrono::TimePoint time,
        const std::optional<std::string>& clientRideId)
    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        auto photo = makePhoto(
            GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, time);
        photo.setGeodeticPos(geolib3::Point2{37.670128, 55.728180})
            .setHeading(geolib3::Heading{45})
            .setSize({6, 9})
            .setProcessedAt(photo.timestamp())
            .setAutomaticShouldBePublished(true)
            .setIsPublished(true);
        if (clientRideId.has_value()) {
            photo.setClientRideId(clientRideId.value());
        }
        else {
            photo.resetClientRideId();
        }
        auto mds = mds::Mds{testFixture_->config().makeMdsClient()};
        auto resp =
            mds.post("mrc/test" + std::to_string(++photosNumber_), TEST_PHOTO);
        photo.setMdsKey(resp.key());
        db::FeatureGateway{*txn}.insert(photo);
        db::QueuedPhotoIdGateway{*txn}.insert(
            db::rides::QueuedPhotoId{photo.id(), photo.timestamp()});
        txn->commit();
        return photo;
    }

    size_t notDeletedRidesNumber()
    {
        return db::RideGateway{
            *testFixture_->pool().masterReadOnlyTransaction()}
            .count(!db::table::Ride::isDeleted.is(true));
    }

    size_t notDeletedByUserFeaturesNumber()
    {
        return db::FeatureGateway{
            *testFixture_->pool().masterReadOnlyTransaction()}
            .count(!db::table::Feature::deletedByUser.is(true));
    }

    db::eye::Hypothesis makeHypothesis(const db::Features& features)
    {
        static auto feedbackTaskId = db::TId{};

        ASSERT(!features.empty());
        auto txn = testFixture_->pool().masterWriteableTransaction();
        auto device = db::eye::Device{db::eye::MrcDeviceAttrs{"M1"}};
        db::eye::DeviceGateway(*txn).insertx(device);
        auto detections = db::eye::Detections{};
        for (const auto& feature : features) {
            auto frame = db::eye::Frame{
                device.id(),
                feature.orientation(),
                db::eye::MrcUrlContext{.featureId = feature.id(),
                                       .mdsGroupId = feature.mdsGroupId(),
                                       .mdsPath = feature.mdsPath()},
                feature.size(),
                feature.timestamp()};
            db::eye::FrameGateway(*txn).insertx(frame);
            db::eye::FeatureToFrameGateway(*txn).insert(
                db::eye::FeatureToFrame{feature.id(), frame.id()});
            auto group = db::eye::DetectionGroup{
                frame.id(), db::eye::DetectionType::HouseNumber};
            db::eye::DetectionGroupGateway(*txn).insertx(group);
            detections.emplace_back(
                group.id(),
                db::eye::DetectedHouseNumbers({db::eye::DetectedHouseNumber{
                    common::ImageBox(100, 100, 200, 200), 0.98, "22"}}));
        }
        db::eye::DetectionGateway(*txn).insertx(detections);
        auto primaryDetectionId = detections.front().id();
        for (const auto& detection : detections) {
            if (primaryDetectionId != detection.id()) {
                auto primaryDetectionRelation =
                    db::eye::PrimaryDetectionRelation(primaryDetectionId,
                                                      detection.id());
                db::eye::PrimaryDetectionRelationGateway{*txn}.insertx(
                    primaryDetectionRelation);
            }
        }
        auto object = db::eye::Object{primaryDetectionId,
                                      db::eye::HouseNumberAttrs{"22"}};
        db::eye::ObjectGateway(*txn).insertx(object);
        auto hypothesis = db::eye::Hypothesis{
            geolib3::Point2{200, 300},
            db::eye::AbsentHouseNumberAttrs{"22"},
        };
        db::eye::HypothesisGateway(*txn).insertx(hypothesis);
        db::eye::HypothesisObjectGateway(*txn).insertx(
            db::eye::HypothesisObject{hypothesis.id(), object.id()});
        auto hypothesisFeedback =
            db::eye::HypothesisFeedback{hypothesis.id(), ++feedbackTaskId};
        db::eye::HypothesisFeedbackGateway(*txn).insertx(hypothesisFeedback);
        txn->commit();
        return hypothesis;
    }

    std::unique_ptr<TestFixture> testFixture_;
    std::unique_ptr<Context> ctx_;
    std::unique_ptr<feature_publisher::Context> featurePublisherCtx_;

    static int photosNumber_;
};

int RideInspectorTest::photosNumber_ = 0;

TEST_F(RideInspectorTest, testGoodPhoto)
{
    fillData(GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT);
    processQueue();
    EXPECT_EQ(shouldBePublishedFeatureCount(), 1u);
    EXPECT_EQ(secretFeatureCount(), 0u);
    EXPECT_EQ(queueSize(), 5u);
    EXPECT_EQ(featureTransactionCount(), 1u);
    publish();
    processQueue();
    EXPECT_EQ(queueSize(), 4u);
}

TEST_F(RideInspectorTest, testNotRoadPhoto)
{
    fillData(GOOD_QUALITY, NOT_ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT);
    processQueue();
    EXPECT_EQ(shouldBePublishedFeatureCount(), 1u);
    EXPECT_EQ(secretFeatureCount(), 0u);
    EXPECT_EQ(queueSize(), 5u);
    EXPECT_EQ(featureTransactionCount(), 1u);
    publish();
    processQueue();
    EXPECT_EQ(queueSize(), 4u);
}

TEST_F(RideInspectorTest, testForbiddenContentPhoto)
{
    fillData(GOOD_QUALITY, ROAD_PROBABILITY, FORBIDDEN_CONTENT);
    processQueue();
    EXPECT_EQ(shouldBePublishedFeatureCount(), 0u);
    EXPECT_EQ(secretFeatureCount(), 0u);
    EXPECT_EQ(queueSize(), 4u);
    EXPECT_EQ(featureTransactionCount(), 1u);
}

TEST_F(RideInspectorTest, testBadPhoto)
{
    fillData(BAD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT);
    processQueue();
    EXPECT_EQ(shouldBePublishedFeatureCount(), 0u);
    EXPECT_EQ(secretFeatureCount(), 0u);
    EXPECT_EQ(queueSize(), 4u);
    EXPECT_EQ(featureTransactionCount(), 1u);
}

TEST_F(RideInspectorTest, testGoodPhotoWithoutTrackPoints)
{
    auto date = chrono::TimePoint::clock::now() - 1min;
    // Insert good photo without track points
    insertPhoto(GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, date);
    processQueue();
    EXPECT_EQ(shouldBePublishedFeatureCount(), 0u);
    // Photo is removed from queue without waiting for track points
    EXPECT_EQ(queueSize(), 0u);
    EXPECT_EQ(featureTransactionCount(), 1u);
}

TEST_F(RideInspectorTest, testBadPhotoWithoutTrackPoints)
{
    auto date = chrono::TimePoint::clock::now() - 1min;
    // Insert photo without track points
    insertPhoto(BAD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, date);
    processQueue();
    EXPECT_EQ(shouldBePublishedFeatureCount(), 0u);
    // Photo is removed from queue without waiting for track points
    EXPECT_EQ(queueSize(), 0u);
    EXPECT_EQ(featureTransactionCount(), 1u);
}

TEST_F(RideInspectorTest, testPhotoWithBadDate)
{
    auto now = chrono::TimePoint::clock::now();
    auto tomorrow = now + 24h;
    auto twoYearsAgo = now - 17520h;

    // Insert photos with bad date
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, tomorrow);
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, twoYearsAgo);
    EXPECT_EQ(processedFeatureCount(), 0u);
    processQueue();
    EXPECT_EQ(shouldBePublishedFeatureCount(), 0u);
    // Photos are removed from queue without waiting for track points
    EXPECT_EQ(queueSize(), 0u);
    EXPECT_EQ(featureTransactionCount(), 2u);
    EXPECT_EQ(processedFeatureCount(), 2u);
}

TEST_F(RideInspectorTest, testPhotoInPrivateRegion)
{
    auto date = chrono::TimePoint::clock::now() - 1h;

    db::TrackPoints trackPoints;
    trackPoints.emplace_back()
        .setSourceId(SOURCE_ID)
        .setTimestamp(date - 1s)
        .setGeodeticPos(geolib3::Point2(34.774566, 32.076640))
        .setHeading(geolib3::Heading(340.0));
    trackPoints.emplace_back()
        .setSourceId(SOURCE_ID)
        .setTimestamp(date)
        .setGeodeticPos(geolib3::Point2(34.774512, 32.076797))
        .setHeading(geolib3::Heading(340.0));
    trackPoints.emplace_back()
        .setSourceId(SOURCE_ID)
        .setTimestamp(date + 1s)
        .setGeodeticPos(geolib3::Point2(34.774461, 32.076964))
        .setHeading(geolib3::Heading(340.0));

    auto txn = testFixture_->pool().masterWriteableTransaction();
    db::TrackPointGateway{*txn}.insert(trackPoints);
    txn->commit();

    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, date - 500ms);
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, date + 500ms);

    processQueue();
    EXPECT_EQ(shouldBePublishedFeatureCount(), 2u);
    EXPECT_EQ(secretFeatureCount(), 2u);
}

TEST_F(RideInspectorTest, testRides)
{
    auto times = std::array<chrono::TimePoint, 4>{};
    times[0] = chrono::TimePoint::clock::now() - 1h;
    times[2] = times[0] + MIN_TIME_GAP_BETWEEN_RIDES + 1s;
    times[1] = times[0] + (times[2] - times[0]) / 2;
    times[3] = times[1] + MIN_TIME_GAP_BETWEEN_RIDES + 1s;
    for (auto time : times) {
        insertTrackPoints(time);
    }

    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        db::UgcAccountGateway{*txn}.upsert(
            db::UgcAccount(USER_ID).setShowAuthorship(true));
        txn->commit();
    }

    // 0
    insertPhoto(BAD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, times[0]);
    processQueue();
    {
        auto rides =
            db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
                .load(!db::table::Ride::isDeleted.is(true));
        ASSERT_EQ(rides.size(), 1u);
        EXPECT_TRUE(rides.front().startTime() == times[0]);
        EXPECT_TRUE(rides.front().endTime() == times[0]);
        EXPECT_EQ(rides.front().photos(), 1u);
        EXPECT_TRUE(rides.front().showAuthorship());
        EXPECT_EQ(rides.front().status(), db::RideStatus::Processed);
    }

    // 2
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, times[2]);
    processQueue();
    EXPECT_EQ(db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
                  .count(!db::table::Ride::isDeleted.is(true)),
              2u);
    EXPECT_EQ(
        db::FeatureGateway{*testFixture_->pool().masterReadOnlyTransaction()}
            .count(not db::table::Feature::showAuthorship.is(true)),
        0u);

    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        db::UgcAccountGateway{*txn}.upsert(
            db::UgcAccount(USER_ID).setShowAuthorship(false));
        txn->commit();
    }

    // 1, 3
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, times[1]);
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, times[3]);
    processQueue();
    EXPECT_EQ(db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
                  .count(db::table::Ride::isDeleted.is(true)),
              1u);
    EXPECT_EQ(
        db::FeatureGateway{*testFixture_->pool().masterReadOnlyTransaction()}
            .count(not db::table::Feature::showAuthorship.is(true)),
        4u);
    {
        auto rides =
            db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
                .load(!db::table::Ride::isDeleted.is(true));
        ASSERT_EQ(rides.size(), 1u);
        EXPECT_TRUE(rides.front().startTime() == times[0]);
        EXPECT_TRUE(rides.front().endTime() == times[3]);
        EXPECT_EQ(rides.front().photos(), 4u);
        EXPECT_EQ(rides.front().publishedPhotos(), 0u);
        EXPECT_FALSE(rides.front().showAuthorship());
        EXPECT_EQ(rides.front().status(), db::RideStatus::Pending);
    }

    publish();
    processQueue();
    {
        auto rides =
            db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
                .load(!db::table::Ride::isDeleted.is(true));
        ASSERT_EQ(rides.size(), 1u);
        EXPECT_EQ(rides.front().publishedPhotos(), 3u);
        EXPECT_EQ(rides.front().status(), db::RideStatus::Processed);
    }
}

TEST_F(RideInspectorTest, testDeletedIntervals)
{
    auto times = std::array<chrono::TimePoint, 4>{};
    times[0] = chrono::TimePoint::clock::now() - 1h;
    times[2] = times[0] + MIN_TIME_GAP_BETWEEN_RIDES + 1s;
    times[1] = times[0] + (times[2] - times[0]) / 2;
    times[3] = times[1] + MIN_TIME_GAP_BETWEEN_RIDES + 1s;
    for (auto time : times) {
        insertTrackPoints(time);
    }
    {
        auto deletedIntervals =
            db::DeletedIntervals{{USER_ID,
                                  SOURCE_ID,
                                  std::nullopt,  //< clientRideId
                                  times[1] - 1s,
                                  times[1] + 1s},
                                 {USER_ID,
                                  SOURCE_ID,
                                  std::nullopt,  //< clientRideId
                                  times[2] + 1s,
                                  times[3] - 1s}};
        auto txn = testFixture_->pool().masterWriteableTransaction();
        db::DeletedIntervalGateway{*txn}.insert(deletedIntervals);
        txn->commit();
    }

    // 0
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, times[0]);
    processQueue();
    {
        auto rides =
            db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
                .load(!db::table::Ride::isDeleted.is(true));
        ASSERT_EQ(rides.size(), 1u);
        EXPECT_TRUE(rides.front().startTime() == times[0]);
        EXPECT_TRUE(rides.front().endTime() == times[0]);
        EXPECT_EQ(rides.front().photos(), 1u);
    }

    // 2
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, times[2]);
    processQueue();
    EXPECT_EQ(db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
                  .count(!db::table::Ride::isDeleted.is(true)),
              2u);

    // 1, 3
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, times[1]);
    insertPhoto(
        GOOD_QUALITY, ROAD_PROBABILITY, NOT_FORBIDDEN_CONTENT, times[3]);
    processQueue();
    {
        auto rides =
            db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
                .load(not db::table::Ride::isDeleted.is(true),
                      orderBy(db::table::Ride::startTime));
        ASSERT_EQ(rides.size(), 2u);
        EXPECT_TRUE(rides.front().startTime() == times[0]);
        EXPECT_TRUE(rides.front().endTime() == times[0]);
        EXPECT_EQ(rides.front().photos(), 1u);
        EXPECT_TRUE(rides.back().startTime() == times[2]);
        EXPECT_TRUE(rides.back().endTime() == times[3]);
        EXPECT_EQ(rides.back().photos(), 2u);

        auto deletedPhotos =
            db::FeatureGateway{
                *testFixture_->pool().masterReadOnlyTransaction()}
                .load(db::table::Feature::deletedByUser.is(true));
        ASSERT_EQ(deletedPhotos.size(), 1u);
        EXPECT_TRUE(deletedPhotos.front().timestamp() == times[1]);
    }
}

TEST_F(RideInspectorTest, testRideMergingWithClientRideId)
{
    auto time = chrono::TimePoint::clock::now() - 12h;
    EXPECT_EQ(notDeletedRidesNumber(), 0u);
    auto photo1 =
        insertPublishedPhoto(time - MIN_TIME_GAP_BETWEEN_RIDES, CLIENT_RIDE_ID);
    ride_inspector::processQueue(*ctx_);
    EXPECT_EQ(queueSize(), 0u);
    EXPECT_EQ(notDeletedRidesNumber(), 1u);
    auto photo2 =
        insertPublishedPhoto(time + MIN_TIME_GAP_BETWEEN_RIDES, CLIENT_RIDE_ID);
    ride_inspector::processQueue(*ctx_);
    EXPECT_EQ(queueSize(), 0u);
    EXPECT_EQ(notDeletedRidesNumber(), 1u);
    auto ride =
        db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
            .loadOne(db::table::Ride::clientId == CLIENT_RIDE_ID);
    EXPECT_EQ(ride.startTime(), photo1.timestamp());
    EXPECT_EQ(ride.endTime(), photo2.timestamp());
}

TEST_F(RideInspectorTest, testRideDeletionWithClientRideId)
{
    auto time = chrono::TimePoint::clock::now() - 12h;
    EXPECT_EQ(notDeletedRidesNumber(), 0u);
    auto photo = insertPublishedPhoto(time, CLIENT_RIDE_ID);
    ride_inspector::processQueue(*ctx_);
    EXPECT_EQ(notDeletedRidesNumber(), 1u);
    auto ride =
        db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
            .loadOne(db::table::Ride::clientId == CLIENT_RIDE_ID);
    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        photo.setDeletedByUser(true);
        db::FeatureGateway{*txn}.update(photo, db::UpdateFeatureTxn::Yes);
        txn->commit();
    }
    updateRides(testFixture_->pool(),
                ride.userId(),
                ride.sourceId(),
                ride.clientId(),
                ride.startTime(),
                ride.endTime());
    EXPECT_EQ(notDeletedRidesNumber(), 0u);
}

TEST_F(RideInspectorTest, testFeatureDeletionWithClientRideId)
{
    auto time = chrono::TimePoint::clock::now() - 12h;
    EXPECT_EQ(notDeletedByUserFeaturesNumber(), 0u);
    insertPublishedPhoto(time, CLIENT_RIDE_ID);
    ride_inspector::processQueue(*ctx_);
    EXPECT_EQ(notDeletedByUserFeaturesNumber(), 1u);
    auto ride =
        db::RideGateway{*testFixture_->pool().masterReadOnlyTransaction()}
            .loadOne(db::table::Ride::clientId == CLIENT_RIDE_ID);
    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        ride.setIsDeleted(true);
        db::RideGateway{*txn}.updatex(ride);
        txn->commit();
    }
    updateRides(testFixture_->pool(),
                ride.userId(),
                ride.sourceId(),
                ride.clientId(),
                ride.startTime(),
                ride.endTime());
    EXPECT_EQ(notDeletedByUserFeaturesNumber(), 0u);
}

TEST_F(RideInspectorTest, testDriveFeature)
{
    auto photo = makePhoto(GOOD_QUALITY,
                           ROAD_PROBABILITY,
                           NOT_FORBIDDEN_CONTENT,
                           chrono::TimePoint::clock::now() - 1min)
                     .setDataset(db::Dataset::Drive);
    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        db::FeatureGateway{*txn}.insert(photo);
        txn->commit();
    }
    updateRidesByPhotoIds(testFixture_->pool(), {photo.id()});
    EXPECT_EQ(notDeletedRidesNumber(), 0u);
}

TEST_F(RideInspectorTest, testHypotheses)
{
    auto time = chrono::TimePoint::clock::now() - 12h;
    auto clientRideIds =
        std::vector<std::optional<std::string>>{std::nullopt, CLIENT_RIDE_ID};
    for (const auto& clientRideId : clientRideIds) {
        // hypotheses initialization
        auto features = db::Features{};
        auto hypothesisIds = db::TIds{};
        for (int i = 0; i < 5; ++i) {
            auto feature = insertPublishedPhoto(time + i * 1s, clientRideId);
            features.push_back(feature);
            if (i % 2 == 0) {
                hypothesisIds.push_back(makeHypothesis({feature}).id());
            }
        }

        // test hypotheses queue
        EXPECT_EQ(
            hypothesisIds.size(),
            hypothesesQueueSize(*testFixture_->pool().slaveTransaction()));
        for (auto hypothesisId : hypothesisIds) {
            EXPECT_TRUE(hypothesesQueuePop(
                testFixture_->pool(),
                [&](sql_chemistry::Transaction&, db::TId queueHypothesisId) {
                    EXPECT_EQ(hypothesisId, queueHypothesisId);
                }));
        }
        EXPECT_EQ(
            0u, hypothesesQueueSize(*testFixture_->pool().slaveTransaction()));
        EXPECT_FALSE(
            hypothesesQueuePop(testFixture_->pool(), [](auto&&...) {}));

        // test RideHypothesis making
        for (auto hypothesisId : hypothesisIds) {
            EXPECT_FALSE(tryMakeRideHypothesis(
                *testFixture_->pool().slaveTransaction(), hypothesisId));
        }
        auto ride = makeRide(features.begin(), features.end());
        {
            auto txn = testFixture_->pool().masterWriteableTransaction();
            db::RideGateway{*txn}.insertx(ride);
            txn->commit();
        }
        auto rideHypotheses = db::RideHypotheses{};
        for (auto hypothesisId : hypothesisIds) {
            auto rideHypothesis = tryMakeRideHypothesis(
                *testFixture_->pool().slaveTransaction(), hypothesisId);
            EXPECT_TRUE(rideHypothesis);
            EXPECT_EQ(rideHypothesis->rideId(), ride.rideId());
            EXPECT_EQ(rideHypothesis->hypothesisId(), hypothesisId);
            rideHypotheses.push_back(*rideHypothesis);
        }
        EXPECT_EQ(
            rideHypotheses,
            makeRideHypotheses(*testFixture_->pool().slaveTransaction(), ride));
    }
}

TEST_F(RideInspectorTest, testMultipleUsersHypothesis)
{
    const auto TIME = chrono::TimePoint::clock::now() - 12h;
    auto randomGenerator = std::mt19937(42);
    auto features = db::Features{};
    for (int i = 0; i < 10; ++i) {
        features.push_back(
            sql_chemistry::GatewayAccess<db::Feature>::construct()
                .setUserId(std::to_string(i))
                .setTimestamp(TIME)
                .setDataset(db::Dataset::Rides)
                .setSourceId(std::to_string(i))
                .setMdsKey({std::to_string(i), std::to_string(i)})
                .setSize(6, 9));
    }
    {
        auto txn = testFixture_->pool().masterWriteableTransaction();
        std::shuffle(features.begin(), features.end(), randomGenerator);
        db::FeatureGateway{*txn}.insert(features);
        txn->commit();
    }
    std::shuffle(features.begin(), features.end(), randomGenerator);
    updateRidesByPhotoIds(testFixture_->pool(),
                          invokeForEach(&db::Feature::id, features));
    std::shuffle(features.begin(), features.end(), randomGenerator);
    auto hypothesis = makeHypothesis(features);
    auto txn = testFixture_->pool().slaveTransaction();
    auto rideHypothesis = tryMakeRideHypothesis(*txn, hypothesis.id());
    EXPECT_TRUE(rideHypothesis);
    auto ride = db::RideGateway{*txn}.loadById(rideHypothesis->rideId());
    auto feature = std::min_element(
        features.begin(), features.end(), [](const auto& lhs, const auto& rhs) {
            return lhs.id() < rhs.id();
        });
    EXPECT_EQ(ride.userId(), feature->userId());
}

TEST_F(RideInspectorTest, testRideDistance)
{
    auto features = db::Features{};
    auto nextTime = [seconds = 0s]() mutable {
        static const auto TIME =
            chrono::parseSqlDateTime("2022-05-11 12:27:00.000+03");
        return TIME + (++seconds);
    };
    auto nextPos = [meters = 0]() mutable {
        static const auto POS = geolib3::Point2(37.670128, 55.728180);
        return geolib3::fastGeoShift(POS, geolib3::Vector2(++meters, 0));
    };
    for (int i = 0; i < 3; ++i) {
        features.push_back(makePhoto(GOOD_QUALITY,
                                     ROAD_PROBABILITY,
                                     NOT_FORBIDDEN_CONTENT,
                                     nextTime())
                               .setGeodeticPos(nextPos())
                               .setHeading(geolib3::Heading{45})
                               .setSize({6, 9})
                               .setClientRideId(CLIENT_RIDE_ID));
    }
    auto ride1 = makeRide(features.begin(), features.end());
    features.front().setTimestamp(features.front().timestamp() -
                                  MIN_TIME_GAP_BETWEEN_RIDES);
    auto ride2 = makeRide(features.begin(), features.end());
    features.back().setTimestamp(features.back().timestamp() +
                                 MIN_TIME_GAP_BETWEEN_RIDES);
    auto ride3 = makeRide(features.begin(), features.end());
    EXPECT_GT(ride1.distanceInMeters(), ride2.distanceInMeters());
    EXPECT_GT(ride2.distanceInMeters(), ride3.distanceInMeters());
    EXPECT_EQ(ride3.distanceInMeters(), 0.);
}

}  // namespace maps::mrc::ride_inspector::tests
