#include <library/cpp/testing/gtest/gtest.h>
#include <maps/libs/common/include/file_utils.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/opencv.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/feature_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 <yandex/maps/mrc/unittest/database_fixture.h>
#include <yandex/maps/mrc/unittest/local_server.h>

#include <boost/preprocessor/stringize.hpp>

namespace maps::mrc::feature_publisher::tests {

const auto EMPTY_FEATURE_ID = db::TId{5};

auto& playground()
{
    static auto result =
        unittest::WithUnittestConfig<unittest::DatabaseFixture,
                                     unittest::MdsStubFixture>{};
    return result;
}

pgpool3::TransactionHandle transaction()
{
    return playground().pool().masterWriteableTransaction();
}

privacy::RegionPrivacyPtr makeRegionPrivacy()
{
    auto loader = object::MockLoader{};
    return privacy::makeCachingRegionPrivacy(
        loader, BinaryPath("maps/data/test/geoid/geoid.mms.1"));
}

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

struct ContextFixture : testing::Test, Context {
    ContextFixture()
        : Context{playground().config(),
                  BinaryPath("maps/data/test/graph3"),
                  BinaryPath("maps/data/test/graph3"),
                  makeRegionPrivacy(),
                  std::make_unique<MockCameraDeviationClassifier>()}
    {
        auto mds = playground().config().makeMdsClient();
        for (const auto& feature : db::FeatureGateway{*transaction()}.load()) {
            mds.del(feature.mdsKey());
        }
        playground().postgres().truncateTables();
        auto txn = transaction();
        txn->exec(maps::common::readFileToString(SRC_("data.sql")));
        auto features = db::FeatureGateway{*txn}.load();
        for (auto& feature : features) {
            auto img = feature.id() == EMPTY_FEATURE_ID
                           ? std::string{}
                           : common::getTestImage<std::string>();
            feature.setMdsKey(
                mds.post(std::to_string(feature.id()), img).key());
        }
        db::FeatureGateway{*txn}.update(features, db::UpdateFeatureTxn::No);
        txn->commit();
    }
};

std::ostream& operator<<(std::ostream& os,
                         const db::CameraDeviation& cameraDeviation)
{
    return os << db::toIntegral(cameraDeviation);
}

template <class T>
void print(std::ostream& os, std::string_view key, const T& val)
{
    os << "." << key << "=" << val << ", ";
}

template <class T>
void print(std::ostream& os, std::string_view key, const std::optional<T>& val)
{
    if (val) {
        print(os, key, *val);
    }
}

#define PRINT(os, val) print(os, BOOST_PP_STRINGIZE(val), val)

struct FeatureDesc {
    db::TId id;
    std::optional<bool> showAuthorship;
    std::optional<bool> gdprDeleted;
    std::optional<bool> deletedByUser;
    bool hasPos;
    std::optional<db::CameraDeviation> cameraDeviation;
    bool hasSize;
    std::optional<db::FeaturePrivacy> privacy;
    bool automaticShouldBePublished;
    bool hasProcessedAt;

    static FeatureDesc from(const db::Feature& feature)
    {
        return FeatureDesc{
            .id = feature.id(),
            .showAuthorship = feature.showAuthorship(),
            .gdprDeleted = feature.gdprDeleted(),
            .deletedByUser = feature.deletedByUser(),
            .hasPos = feature.hasPos(),
            .cameraDeviation = feature.hasCameraDeviation()
                                   ? std::optional{feature.cameraDeviation()}
                                   : std::nullopt,
            .hasSize = feature.hasSize(),
            .privacy = feature.hasPrivacy() ? std::optional{feature.privacy()}
                                            : std::nullopt,
            .automaticShouldBePublished =
                feature.automaticShouldBePublished().value_or(false),
            .hasProcessedAt = feature.processedAt().has_value(),
        };
    }

    std::string toString() const
    {
        std::ostringstream os;
        PRINT(os, id);
        PRINT(os, showAuthorship);
        PRINT(os, gdprDeleted);
        PRINT(os, deletedByUser);
        PRINT(os, hasPos);
        PRINT(os, cameraDeviation);
        PRINT(os, hasSize);
        PRINT(os, privacy);
        PRINT(os, automaticShouldBePublished);
        PRINT(os, hasProcessedAt);
        return os.str();
    }

    friend std::ostream& operator<<(std::ostream& os, const FeatureDesc& that)
    {
        return os << that.toString();
    }

    friend bool operator==(const FeatureDesc&, const FeatureDesc&) = default;
};

const auto EXPECTED = std::vector{
    FeatureDesc{.id = 1,
                .hasPos = true,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 2,
                .hasPos = true,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 3,
                .hasPos = true,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 4,
                .hasPos = true,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .hasProcessedAt = true},
    FeatureDesc{.id = 5,
                .hasPos = true,
                .privacy = db::FeaturePrivacy::Public,
                .hasProcessedAt = true},
    FeatureDesc{.id = 6},
    FeatureDesc{.id = 7,
                .hasPos = true,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Secret,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 8,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Right,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 9,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Right,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 10,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 11,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .hasProcessedAt = true},
    FeatureDesc{.id = 12,
                .showAuthorship = true,
                .gdprDeleted = true,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 13,
                .showAuthorship = true,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 14,
                .showAuthorship = true,
                .deletedByUser = true,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 15,
                .showAuthorship = true,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 16,
                .showAuthorship = true,
                .deletedByUser = true,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 17,
                .showAuthorship = true,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 18,
                .hasPos = true,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Restricted,
                .automaticShouldBePublished = true,
                .hasProcessedAt = true},
    FeatureDesc{.id = 19,
                .hasPos = true,
                .cameraDeviation = db::CameraDeviation::Front,
                .hasSize = true,
                .privacy = db::FeaturePrivacy::Public,
                .hasProcessedAt = true},
};

TEST_F(ContextFixture, processClassifiedPhotos)
{
    using namespace testing;
    auto cameraDeviationClassifier =
        std::make_unique<MockCameraDeviationClassifier>();

    static const auto FEATURE_ID_TO_CAMERA_DEVIATION_MAP =
        std::map<db::TId, db::CameraDeviation>{
            {1, db::CameraDeviation::Right},
            {7, db::CameraDeviation::Right},
            {8, db::CameraDeviation::Right},
            {10, db::CameraDeviation::Front},
            {12, db::CameraDeviation::Front},
            {17, db::CameraDeviation::Front},
            {19, db::CameraDeviation::Front},
            };

    EXPECT_CALL(*cameraDeviationClassifier, evalForPassage(_, _, _))
        .WillRepeatedly(Invoke(WithArg<0>([](auto first) {
            return FEATURE_ID_TO_CAMERA_DEVIATION_MAP.at(first->id());
        })));

    setCameraDeviationClassifier(std::move(cameraDeviationClassifier));
    EXPECT_EQ(processClassifiedPhotos(), EXPECTED.size() - 1);
    auto features = db::FeatureGateway{*transaction()}.load(
        orderBy(db::table::Feature::id));
    EXPECT_EQ(features.size(), EXPECTED.size());
    for (size_t i = 0; i < features.size(); ++i) {
        EXPECT_EQ(FeatureDesc::from(features[i]), EXPECTED[i]);
    }
    EXPECT_EQ(processClassifiedPhotos(), 0u);
}

TEST_F(ContextFixture, evalSize)
{
    auto features = db::FeatureGateway{*transaction()}.load();
    for (const auto& feature : features) {
        EXPECT_FALSE(feature.hasSize());
    }
    evalSize(features.begin(), features.end());
    for (const auto& feature : features) {
        if (feature.id() == EMPTY_FEATURE_ID) {
            EXPECT_FALSE(feature.hasSize());
        }
        else {
            EXPECT_TRUE(feature.hasSize());
        }
    }
}

TEST_F(ContextFixture, evalUserSetingsForPassage)
{
    auto features = db::FeatureGateway{*transaction()}.load(
        db::table::Feature::id.between(12, 17),
        orderBy(db::table::Feature::sourceId)
            .orderBy(db::table::Feature::date));
    EXPECT_EQ(features.size(), 6u);
    for (const auto& feature : features) {
        EXPECT_FALSE(feature.showAuthorship().has_value());
        EXPECT_FALSE(feature.gdprDeleted().has_value());
        EXPECT_FALSE(feature.deletedByUser().has_value());
    }
    evalUserSetingsForPassage(features.begin(), features.end());
    for (const auto& feature : features) {
        EXPECT_TRUE(feature.showAuthorship().value());
        if (feature.id() == 12) {
            EXPECT_TRUE(feature.gdprDeleted().value());
        }
        else {
            EXPECT_FALSE(feature.gdprDeleted().has_value());
        }
        switch (feature.id()) {
            case 14:
            case 16:
                EXPECT_TRUE(feature.deletedByUser().value());
                break;
            default:
                EXPECT_FALSE(feature.deletedByUser().has_value());
        }
    }
}

TEST_F(ContextFixture, evalPositionForPassage)
{
    auto features = db::FeatureGateway{*transaction()}.load(
        db::table::Feature::id.between(1, 5),
        orderBy(db::table::Feature::sourceId)
            .orderBy(db::table::Feature::date));
    EXPECT_EQ(features.size(), 5u);
    for (const auto& feature : features) {
        EXPECT_FALSE(feature.hasPos());
    }
    evalPositionForPassage(features.begin(), features.end());
    for (const auto& feature : features) {
        EXPECT_TRUE(feature.hasPos());
    }
}

TEST_F(ContextFixture, evalCameraDeviationForPassage)
{
    using namespace testing;
    auto cameraDeviationClassifier =
        std::make_unique<MockCameraDeviationClassifier>();
    EXPECT_CALL(*cameraDeviationClassifier, evalForPassage(_, _, _))
        .WillOnce(Return(db::CameraDeviation::Right));
    setCameraDeviationClassifier(std::move(cameraDeviationClassifier));
    auto features = db::FeatureGateway{*transaction()}.load(
        db::table::Feature::id.between(10, 11),
        orderBy(db::table::Feature::sourceId)
            .orderBy(db::table::Feature::date));
    EXPECT_EQ(features.size(), 2u);
    for (const auto& feature : features) {
        EXPECT_FALSE(feature.hasCameraDeviation());
    }
    evalSize(features.begin(), features.end());
    evalCameraDeviationForPassage(features.begin(), features.end());
    for (const auto& feature : features) {
        EXPECT_EQ(feature.cameraDeviation(), db::CameraDeviation::Right);
    }
}

TEST_F(ContextFixture, emptyImageNoException)
{
    using namespace testing;

    auto cameraDeviationClassifier =
        std::make_unique<MockCameraDeviationClassifier>();
    EXPECT_CALL(*cameraDeviationClassifier, evalForPassage(_, _, _))
        .WillRepeatedly(Throw(LogicError()));
    setCameraDeviationClassifier(std::move(cameraDeviationClassifier));

    {
        auto txn = transaction();
        auto features = db::FeatureGateway{*txn}.load(db::table::Feature::id !=
                                                      EMPTY_FEATURE_ID);
        for (auto& feature : features) {
            feature.setProcessedAt(chrono::TimePoint::clock::now());
        }
        db::FeatureGateway{*txn}.update(features, db::UpdateFeatureTxn::No);
        txn->commit();
    }

    EXPECT_FALSE(db::FeatureGateway{*transaction()}
                     .loadOne(db::table::Feature::id == EMPTY_FEATURE_ID)
                     .processedAt()
                     .has_value());
    EXPECT_EQ(processClassifiedPhotos(), 1u);
    EXPECT_TRUE(db::FeatureGateway{*transaction()}
                    .loadOne(db::table::Feature::id == EMPTY_FEATURE_ID)
                    .processedAt()
                    .has_value());
}

}  // namespace maps::mrc::feature_publisher::tests
