#include <library/cpp/testing/common/env.h>
#include <library/cpp/testing/gtest/gtest.h>
#include <maps/libs/common/include/file_utils.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/panorama_utils.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/types.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/tests/panorama_mocks.h>
#include <yandex/maps/mrc/unittest/utils.h>

#include <array>
#include <sstream>

namespace maps::mrc::common::tests {

namespace {

const std::string S3MDS{"http://s3mds.com"};
const std::string MDS_KEY{"key"};

const auto ENCODED_TILE =
    maps::common::readFileToString(SRC_("images/arrows_320x240.png"));

constexpr std::uint32_t TILE_WIDTH = 320;
constexpr std::uint32_t TILE_HEIGHT = 240;
constexpr std::uint32_t COLS = 3;
constexpr std::uint32_t ROWS = 2;
constexpr std::uint16_t ZOOM_LEVEL = 0;
constexpr float SCALE = 1.0f;

const PanoCutParams CUT_PARAMS{
    TILE_WIDTH * COLS * 2, // total width is twice bigger than test tiles
    TILE_HEIGHT* ROWS * 2, // total height is twice bigger than test tiles
    TILE_WIDTH,            // tiles width
    TILE_HEIGHT,           // tiles height
    ZOOM_LEVEL             // zoom level
};

const PanoCropOut CROP_OUT{0, 0,              // start point
                           TILE_WIDTH* COLS,  // total crop out width
                           TILE_HEIGHT* ROWS, // total crop out height
                           SCALE};

PanoTiles testTiles()
{
    const auto tile = decodeImage(ENCODED_TILE);

    PanoTiles tiles{ROWS, std::vector<cv::Mat>{COLS}};
    for (std::uint32_t col = 0; col < COLS; ++col) {
        for (std::uint32_t row = 0; row < ROWS; ++row) {
            tiles[row][col] = tile.clone();
        }
    }

    return tiles;
}

PanoTiles reduceTiles(
    PanoTiles panoTiles,
    std::size_t colIdx,
    bool reduceBottomTilesHeight = true)
{
    // Reduce width of specified tiles column in half.
    for (auto& panoTilesLine : panoTiles) {
        if (panoTilesLine.size() <= colIdx) {
            continue;
        }
        cv::Mat tile = panoTilesLine[colIdx].clone();
        panoTilesLine[colIdx] =
            cv::Mat{tile, cv::Rect{0, 0, tile.cols / 2, tile.rows}};
    }

    if (!reduceBottomTilesHeight) {
        return panoTiles;
    }

    // Reduce height of the bottom tiles in half.
    for (auto& tile: panoTiles.back()) {
        tile = cv::Mat{tile, cv::Rect{0, 0, tile.cols, tile.rows / 2}};
    }

    return panoTiles;
}

} // namespace

TEST(panorama_utils_should, test_concat_tiles_full)
{
    const auto actual =
        concatAndCropTiles(CUT_PARAMS, CROP_OUT, testTiles());

    auto expected = decodeImage(maps::common::readFileToString(
        SRC_("images/concat_tiles_full.png")));

    EXPECT_TRUE(unittest::areImagesEq(actual, expected));
}

TEST(panorama_utils_should, test_concat_tiles_halved_width)
{
    auto cropOut = CROP_OUT;
    cropOut.width -= TILE_WIDTH / 2;
    const auto actual = concatAndCropTiles(
        CUT_PARAMS, cropOut,
        reduceTiles(testTiles(), COLS / 2,
                    false /* don't reduce bottom line tiles in height */));

    auto expected = decodeImage(maps::common::readFileToString(
        SRC_("images/concat_tiles_halved_width.png")));

    EXPECT_TRUE(unittest::areImagesEq(actual, expected));
}

TEST(panorama_utils_should, test_concat_tiles_halved_height)
{
    auto cropOut = CROP_OUT;
    cropOut.height -= TILE_HEIGHT / 2;
    const auto actual = concatAndCropTiles(
        CUT_PARAMS, cropOut,
        reduceTiles(testTiles(), COLS, /* don't reduce tiles in width */
                    true /* reduce bottom line tiles in height */));

    auto expected = decodeImage(maps::common::readFileToString(
        SRC_("images/concat_tiles_halved_height.png")));

    EXPECT_TRUE(unittest::areImagesEq(actual, expected));
}

TEST(panorama_utils_should, test_concat_tiles_halved_width_and_height)
{
    auto cropOut = CROP_OUT;
    cropOut.width -= TILE_WIDTH / 2;
    cropOut.height -= TILE_HEIGHT / 2;
    const auto actual = concatAndCropTiles(
        CUT_PARAMS, cropOut,
        reduceTiles(testTiles(), COLS - 1 /* reduce tiles in width */,
                    true /* reduce bottom line tiles in height */));

    auto expected = decodeImage(maps::common::readFileToString(
        SRC_("images/concat_tiles_halved_width_and_height.png")));

    EXPECT_TRUE(unittest::areImagesEq(actual, expected));
}

TEST(panorama_utils_should, test_round) {
    EXPECT_EQ(2, round<std::int8_t>(1.5f));
    EXPECT_EQ(2, round<std::int16_t>(1.5f));
    EXPECT_EQ(2, round<std::int32_t>(1.5f));
    EXPECT_EQ(2, round<std::int64_t>(1.5f));
    EXPECT_EQ(2u, round<std::uint8_t>(1.5f));
    EXPECT_EQ(2u, round<std::uint16_t>(1.5f));
    EXPECT_EQ(2u, round<std::uint32_t>(1.5f));
    EXPECT_EQ(2u, round<std::uint64_t>(1.5f));

    EXPECT_EQ(1, round<std::int8_t>(1.4f));
    EXPECT_EQ(1, round<std::int16_t>(1.4f));
    EXPECT_EQ(1, round<std::int32_t>(1.4f));
    EXPECT_EQ(1, round<std::int64_t>(1.4f));
    EXPECT_EQ(1u, round<std::uint8_t>(1.4f));
    EXPECT_EQ(1u, round<std::uint16_t>(1.4f));
    EXPECT_EQ(1u, round<std::uint32_t>(1.4f));
    EXPECT_EQ(1u, round<std::uint64_t>(1.4f));
}

TEST(panorama_utils_should, test_cut_params_output_operator) {
  std::stringstream out;
  out << CUT_PARAMS;
  EXPECT_EQ(out.str(), std::to_string(TILE_WIDTH * COLS * 2) + "," +
                           std::to_string(TILE_HEIGHT * ROWS * 2) + "," +
                           std::to_string(TILE_WIDTH) + "," +
                           std::to_string(TILE_HEIGHT) + "," +
                           std::to_string(ZOOM_LEVEL));
}

TEST(panorama_utils_should, test_crop_out_output_operator) {
  std::stringstream out;
  out << CROP_OUT;
  EXPECT_EQ(out.str(), "0,0," + std::to_string(TILE_WIDTH * COLS) + "," +
                           std::to_string(TILE_HEIGHT * ROWS) + ",1");
}

TEST(panorama_utils_should, test_tiles_count) {
    EXPECT_EQ(1u, tilesCount(1, 1));
    EXPECT_EQ(2u, tilesCount(2, 1));
    EXPECT_EQ(2u, tilesCount(3, 2));
    EXPECT_THROW(tilesCount(1, 0), maps::RuntimeError);
}

TEST(panorama_utils_should, test_make_pano_tile_url) {
    EXPECT_EQ(
        makeS3MdsPanoTileUrl(S3MDS, MDS_KEY, 0, 1, 2).toString(),
        S3MDS + "/pano/" + MDS_KEY + "/0.1.2");
}

TEST(panorama_utils_should, pano_tiles_downloading) {
    PanoTilesDownloaderMock testPanoDownloader{testTiles()};

    const auto actual = testPanoDownloader.download(CUT_PARAMS, CROP_OUT);

    EXPECT_EQ(actual.size(), ROWS);
    for(const auto& line : actual) {
        EXPECT_EQ(line.size(), COLS);
    }

    const auto expected = testTiles();
    for (std::size_t row = 0; row < ROWS; ++row) {
        for (std::size_t col = 0; col < COLS; ++col) {
            EXPECT_TRUE(actual[row][col].size() == expected[row][col].size());
            EXPECT_TRUE(
                unittest::areImagesEq(actual[row][col], expected[row][col]));
        }
    }
}

TEST(panorama_utils_should, test_pano_tiles_downloading_exception) {
    class ExceptPanoDonwloaderOne : public PanoTilesDownloaderMock {
    public:
        void requestTile(const RequestContext&) override
        {
            throw maps::RuntimeError{};
        };
    };

    class ExceptPanoDonwloaderTwo : public PanoTilesDownloaderMock {
    public:
        PanoTiles getTiles(uint32_t, uint32_t) override
        {
            throw maps::RuntimeError{};
        }
    };

    EXPECT_THROW(
        ExceptPanoDonwloaderOne{}.download(CUT_PARAMS, CROP_OUT),
        maps::RuntimeError);

    EXPECT_THROW(
        ExceptPanoDonwloaderTwo{}.download(CUT_PARAMS, CROP_OUT),
        maps::RuntimeError);
}

} // namespace maps::mrc::common::tests
