#include "catch.hpp"

// test utils
#include "test_utils.hpp"

// mapnik
#include <mapnik/memory_datasource.hpp>
#include <mapnik/util/fs.hpp>
#include <mapnik/agg_renderer.hpp>
#include <mapnik/feature_factory.hpp>
#include <mapnik/load_map.hpp>
#include <mapnik/image_util.hpp>
#include <mapnik/vertex_adapters.hpp>
#include <mapnik/projection.hpp>
#include <mapnik/proj_transform.hpp>
#include <mapnik/util/file_io.hpp>
#include <mapnik/util/geometry_to_geojson.hpp>
#include <mapnik/version.hpp>
#if MAPNIK_VERSION >= 300100
#include <mapnik/geometry/is_empty.hpp>
#include <mapnik/geometry/reprojection.hpp>
#include <mapnik/geometry/transform.hpp>
#include <mapnik/geometry/strategy.hpp>
#else
#include <mapnik/geometry_is_empty.hpp>
#include <mapnik/geometry_reprojection.hpp>
#include <mapnik/geometry_transform.hpp>
#include <mapnik/geometry_strategy.hpp>
#endif
#include <mapnik/proj_strategy.hpp>
#include <mapnik/geometry.hpp>
#include <mapnik/datasource_cache.hpp>

// mapbox
#include <mapbox/geometry/geometry.hpp>

// boost
#include <boost/optional/optional_io.hpp>

// mapnik-vector-tile
#include "vector_tile_compression.hpp"
#include "vector_tile_processor.hpp"
#include "vector_tile_projection.hpp"
#include "vector_tile_geometry_decoder.hpp"
#include "vector_tile_geometry_encoder_pbf.hpp"
#include "vector_tile_datasource_pbf.hpp"
#include "vector_tile_layer.hpp"

//protozero
#include "protozero/pbf_reader.hpp"

//std
#include <string>
#include <fstream>
#include <streambuf>

// libprotobuf
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#include "vector_tile.pb.h"
#pragma GCC diagnostic pop

TEST_CASE("pbf vector tile input")
{
    unsigned tile_size = 4096;
    mapnik::Map map(256,256,"epsg:3857");
    mapnik::layer lyr("layer",map.srs());
    lyr.set_datasource(testing::build_ds(0,0));
    map.add_layer(lyr);
    mapnik::vector_tile_impl::processor ren(map);
    mapnik::vector_tile_impl::tile out_tile = ren.create_tile(0,0,0,tile_size);
    CHECK(out_tile.is_painted() == true);
    CHECK(out_tile.is_empty() == false);
    vector_tile::Tile tile;
    tile.ParseFromString(out_tile.get_buffer());
    // serialize to message
    std::string buffer;
    CHECK(tile.SerializeToString(&buffer));
    CHECK(147 == buffer.size());
    // now create new objects
    mapnik::Map map2(256,256,"epsg:3857");
    vector_tile::Tile tile2;
    CHECK(tile2.ParseFromString(buffer));
    CHECK(1 == tile2.layers_size());
    vector_tile::Tile_Layer const& layer2 = tile2.layers(0);
    CHECK(std::string("layer") == layer2.name());
    CHECK(1 == layer2.features_size());

    mapnik::layer lyr2("layer",map.srs());

    protozero::pbf_reader pbf_tile(buffer);
    pbf_tile.next();
    protozero::pbf_reader layer3 = pbf_tile.get_message();

    std::shared_ptr<mapnik::vector_tile_impl::tile_datasource_pbf> ds = std::make_shared<
                                    mapnik::vector_tile_impl::tile_datasource_pbf>(
                                        layer3,0,0,0);
    CHECK(ds->get_name() == "layer");
    mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789);
    ds->set_envelope(bbox);
    CHECK( ds->type() == mapnik::datasource::Vector );
    CHECK( ds->get_geometry_type() == mapnik::datasource_geometry_t::Collection );
    mapnik::layer_descriptor lay_desc = ds->get_descriptor();
    std::set<std::string> expected_names;
    expected_names.insert("bool");
    expected_names.insert("boolf");
    expected_names.insert("double");
    expected_names.insert("float");
    expected_names.insert("int");
    expected_names.insert("name");
    expected_names.insert("uint");
    std::size_t desc_count = 0;
    for (auto const& desc : lay_desc.get_descriptors())
    {
        ++desc_count;
        CHECK(expected_names.count(desc.get_name()) == 1);
    }
    CHECK(desc_count == expected_names.size());
    lyr2.set_datasource(ds);
    lyr2.add_style("style");
    map2.add_layer(lyr2);
    mapnik::load_map(map2,"test/data/style.xml");
    //std::clog << mapnik::save_map_to_string(map2) << "\n";
    map2.zoom_all();
    mapnik::image_rgba8 im(map2.width(),map2.height());
    mapnik::agg_renderer<mapnik::image_rgba8> ren2(map2,im);
    ren2.apply();
    if (!mapnik::util::exists("test/fixtures/expected-1.png"))
    {
        mapnik::save_to_file(im,"test/fixtures/expected-1.png","png32");
    }
    unsigned diff = testing::compare_images(im,"test/fixtures/expected-1.png");
    CHECK(0 == diff);
    if (diff > 0)
    {
        mapnik::save_to_file(im,"test/fixtures/actual-1.png","png32");
    }
}

TEST_CASE("pbf vector tile datasource")
{
    unsigned tile_size = 4096;
    mapnik::Map map(tile_size,tile_size,"epsg:3857");
    mapnik::layer lyr("layer",map.srs());
    lyr.set_datasource(testing::build_ds(0,0));
    map.add_layer(lyr);

    mapnik::vector_tile_impl::processor ren(map);
    mapnik::vector_tile_impl::tile out_tile = ren.create_tile(0,0,0);

    // serialize to message
    std::string buffer;
    out_tile.serialize_to_string(buffer);
    CHECK(out_tile.is_painted() == true);
    CHECK(out_tile.is_empty() == false);

    // check that vector tile contains proper information
    vector_tile::Tile tile;
    tile.ParseFromString(out_tile.get_buffer());

    REQUIRE(1 == tile.layers_size());
    vector_tile::Tile_Layer const& layer = tile.layers(0);
    CHECK(std::string("layer") == layer.name());
    REQUIRE(1 == layer.features_size());
    vector_tile::Tile_Feature const& f = layer.features(0);
    CHECK(static_cast<mapnik::value_integer>(1) == static_cast<mapnik::value_integer>(f.id()));
    CHECK(3 == f.geometry_size());
    CHECK(9 == f.geometry(0));
    CHECK(4096 == f.geometry(1));
    CHECK(4096 == f.geometry(2));

    protozero::pbf_reader pbf_tile(buffer);
    pbf_tile.next();
    protozero::pbf_reader layer2 = pbf_tile.get_message();

    // now actually start the meat of the test
    mapnik::vector_tile_impl::tile_datasource_pbf ds(layer2,0,0,0);
    mapnik::featureset_ptr fs;

    // ensure we can query single feature
    mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789);
    fs = ds.features(mapnik::query(bbox));
    mapnik::feature_ptr feat = fs->next();
    REQUIRE(feat != mapnik::feature_ptr());
    CHECK(feat->size() == 0);
    CHECK(fs->next() == mapnik::feature_ptr());
    mapnik::query qq = mapnik::query(mapnik::box2d<double>(-1,-1,1,1));
    qq.add_property_name("name");
    fs = ds.features(qq);
    feat = fs->next();
    REQUIRE(feat != mapnik::feature_ptr());
    CHECK(feat->size() == 1);
    CHECK(feat->get("name") == mapnik::value_unicode_string("null island"));

    // now check that datasource api throws out feature which is outside extent
    fs = ds.features(mapnik::query(mapnik::box2d<double>(-10,-10,-10,-10)));
    CHECK(fs->next() == mapnik::feature_ptr());

    // ensure same behavior for feature_at_point
    fs = ds.features_at_point(mapnik::coord2d(0.0,0.0),0.0001);
    REQUIRE(fs->next() != mapnik::feature_ptr());

    fs = ds.features_at_point(mapnik::coord2d(1.0,1.0),1.0001);
    REQUIRE(fs->next() != mapnik::feature_ptr());

    fs = ds.features_at_point(mapnik::coord2d(-10,-10),0);
    CHECK(fs->next() == mapnik::feature_ptr());

    // finally, make sure attributes are also filtered
    mapnik::feature_ptr f_ptr;
    fs = ds.features(mapnik::query(bbox));
    f_ptr = fs->next();
    REQUIRE(f_ptr != mapnik::feature_ptr());
    // no attributes
    CHECK(f_ptr->context()->size() == 0);

    mapnik::query q(bbox);
    q.add_property_name("name");
    fs = ds.features(q);
    f_ptr = fs->next();
    REQUIRE(f_ptr != mapnik::feature_ptr());
    // one attribute
    CHECK(f_ptr->context()->size() == 1);
}

TEST_CASE("pbf encoding multi line")
{
    mapbox::geometry::multi_line_string<std::int64_t> geom;
    {
        mapbox::geometry::line_string<std::int64_t> ring;
        ring.emplace_back(0,0);
        ring.emplace_back(2,2);
        geom.emplace_back(std::move(ring));
    }
    {
        mapbox::geometry::line_string<std::int64_t> ring;
        ring.emplace_back(1,1);
        ring.emplace_back(2,2);
        geom.emplace_back(std::move(ring));
    }
    vector_tile::Tile tile;
    vector_tile::Tile_Layer * t_layer = tile.add_layers();
    t_layer->set_name("layer");
    t_layer->set_version(2);
    t_layer->set_extent(4096);
    vector_tile::Tile_Feature * t_feature = t_layer->add_features();
    std::int32_t x = 0;
    std::int32_t y = 0;

    std::string feature_str;
    protozero::pbf_writer feature_writer(feature_str);
    CHECK(mapnik::vector_tile_impl::encode_geometry_pbf(geom, feature_writer, x, y));
    t_feature->ParseFromString(feature_str);

    std::string buffer;
    tile.SerializeToString(&buffer);
    REQUIRE(1 == tile.layers_size());
    vector_tile::Tile_Layer const& layer = tile.layers(0);
    REQUIRE(1 == layer.features_size());
    vector_tile::Tile_Feature const& f = layer.features(0);
    CHECK(12 == f.geometry_size());
    CHECK(9 == f.geometry(0)); // 1 move_to
    CHECK(0 == f.geometry(1)); // x:0
    CHECK(0 == f.geometry(2)); // y:0
    CHECK(10 == f.geometry(3)); // 1 line_to
    CHECK(4 == f.geometry(4)); // x:2
    CHECK(4 == f.geometry(5)); // y:2
    CHECK(9 == f.geometry(6)); // 1 move_to
    CHECK(1 == f.geometry(7)); // x:1
    CHECK(1 == f.geometry(8)); // y:1
    CHECK(10 == f.geometry(9)); // 1 line_to
    CHECK(2 == f.geometry(10)); // x:2
    CHECK(2 == f.geometry(11)); // y:2

    mapnik::featureset_ptr fs;
    mapnik::feature_ptr f_ptr;

    protozero::pbf_reader pbf_tile(buffer);
    pbf_tile.next();
    protozero::pbf_reader layer2 = pbf_tile.get_message();

    unsigned tile_size = 4096;
    mapnik::vector_tile_impl::tile_datasource_pbf ds(layer2,0,0,0);
    mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789);
    fs = ds.features(mapnik::query(bbox));
    f_ptr = fs->next();
    REQUIRE(f_ptr != mapnik::feature_ptr());
    // no attributes
    CHECK(f_ptr->context()->size() == 0);

    CHECK(f_ptr->get_geometry().is<mapnik::geometry::multi_line_string<double> >());
}


// NOTE: encoding multiple lines as one path is technically incorrect
// because in Mapnik the protocol is to split geometry parts into separate paths.
// However this case should still be supported because keeping a single flat array is an
// important optimization in the case that lines do not need to be labeled in custom ways
// or represented as GeoJSON

TEST_CASE( "pbf decoding empty buffer", "should throw exception" ) {
    std::string buffer;
    protozero::pbf_reader pbf_tile(buffer);
    pbf_tile.next();
    protozero::pbf_reader layer2;
#if defined(DNDEBUG)
    // throws in release mode
    REQUIRE_THROWS(layer2 = pbf_tile.get_message());
#else
    // aborts in protozero in debug mode
#endif
}

TEST_CASE( "pbf decoding garbage buffer", "should throw exception" ) {
    std::string buffer("daufyglwi3h7fseuhfas8w3h,dksufasdf");
    protozero::pbf_reader pbf_tile(buffer);
    REQUIRE_THROWS_AS(pbf_tile.next(), protozero::unknown_pbf_wire_type_exception const&);
    protozero::pbf_reader layer2;
#if defined(DNDEBUG)
    // throws in release mode
    REQUIRE_THROWS(layer2 = pbf_tile.get_message());
#else
    // aborts in protozero in debug mode
#endif
}


TEST_CASE("pbf decoding some truncated buffers")
{
    unsigned tile_size = 4096;
    mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789);
    mapnik::Map map(tile_size,tile_size,"epsg:3857");
    mapnik::layer lyr("layer",map.srs());
    lyr.set_datasource(testing::build_ds(0,0));
    map.add_layer(lyr);

    // Create processor
    mapnik::vector_tile_impl::processor ren(map);

    // Request Tile
    mapnik::vector_tile_impl::tile out_tile = ren.create_tile(0,0,0);
    CHECK(out_tile.is_painted() == true);
    CHECK(out_tile.is_empty() == false);

    // Now check that the tile is correct.
    vector_tile::Tile tile;
    tile.ParseFromString(out_tile.get_buffer());
    REQUIRE(1 == tile.layers_size());
    vector_tile::Tile_Layer const& layer = tile.layers(0);
    CHECK(std::string("layer") == layer.name());
    REQUIRE(1 == layer.features_size());
    vector_tile::Tile_Feature const& f = layer.features(0);
    CHECK(static_cast<mapnik::value_integer>(1) == static_cast<mapnik::value_integer>(f.id()));
    CHECK(3 == f.geometry_size());
    CHECK(9 == f.geometry(0));
    CHECK(4096 == f.geometry(1));
    CHECK(4096 == f.geometry(2));

    std::string buffer;
    out_tile.serialize_to_string(buffer);
    // We will test truncating the generated protobuf at every increment.
    //  Most cases should fail, except for the lucky bites where we chop
    // it off at a point that would be valid anyway.
    for (std::size_t i=1; i< buffer.size(); ++i)
    {
      CHECK_THROWS(
      {
          protozero::pbf_reader pbf_tile(buffer.c_str(), i);
          pbf_tile.next();
          protozero::pbf_reader layer2 = pbf_tile.get_message();
          mapnik::vector_tile_impl::tile_datasource_pbf ds(layer2,0,0,0);
          mapnik::featureset_ptr fs;
          mapnik::feature_ptr f_ptr;
          fs = ds.features(mapnik::query(bbox));
          f_ptr = fs->next();
          while (f_ptr != mapnik::feature_ptr())
          {
              f_ptr = fs->next();
          }
      }
      );
    }
}

TEST_CASE("pbf vector tile from simplified geojson")
{
    unsigned tile_size = 256 * 100;
    mapnik::Map map(tile_size,tile_size,"epsg:3857");
    mapnik::layer lyr("layer","epsg:4326");
    std::shared_ptr<mapnik::memory_datasource> ds = testing::build_geojson_ds("./test/data/poly.geojson");
    ds->set_envelope(mapnik::box2d<double>(160.147311,11.047284,160.662858,11.423830));
    lyr.set_datasource(ds);
    map.add_layer(lyr);
    mapnik::vector_tile_impl::processor ren(map);
    mapnik::vector_tile_impl::tile out_tile = ren.create_tile(0,0,0,tile_size);
    CHECK(out_tile.is_painted() == true);
    CHECK(out_tile.is_empty() == false);

    vector_tile::Tile tile;
    tile.ParseFromString(out_tile.get_buffer());
    REQUIRE(1 == tile.layers_size());
    vector_tile::Tile_Layer const& layer = tile.layers(0);
    CHECK(std::string("layer") == layer.name());
    REQUIRE(1 == layer.features_size());
    vector_tile::Tile_Feature const& f = layer.features(0);
    unsigned z = 0;
    unsigned x = 0;
    unsigned y = 0;
    double resolution = mapnik::EARTH_CIRCUMFERENCE/(1 << z);
    double tile_x = -0.5 * mapnik::EARTH_CIRCUMFERENCE + x * resolution;
    double tile_y =  0.5 * mapnik::EARTH_CIRCUMFERENCE - y * resolution;
    double scale = (static_cast<double>(layer.extent()) / tile_size) * tile_size/resolution;

    std::string buffer;
    tile.SerializeToString(&buffer);
    protozero::pbf_reader pbf_tile(buffer);
    pbf_tile.next();
    protozero::pbf_reader pbf_layer = pbf_tile.get_message();
    // Need to loop because they could be encoded in any order
    bool found = false;
    while (!found && pbf_layer.next(2))
    {
      // But there will be only one in our tile, so we'll break out of loop once we find it
      protozero::pbf_reader pbf_feature = pbf_layer.get_message();
      while (!found && pbf_feature.next(4))
      {
          found = true;
          mapnik::vector_tile_impl::GeometryPBF::pbf_itr geom_itr = pbf_feature.get_packed_uint32();
          mapnik::vector_tile_impl::GeometryPBF geoms(geom_itr);
          auto geom = mapnik::vector_tile_impl::decode_geometry<double>(geoms, f.type(), 2, tile_x, tile_y, scale, -1.0 * scale);
          unsigned int n_err = 0;
          mapnik::projection wgs84("epsg:4326",true);
          mapnik::projection merc("epsg:3857",true);
          mapnik::proj_transform prj_trans(merc,wgs84);
          mapnik::geometry::geometry<double> projected_geom = mapnik::geometry::reproject_copy(geom,prj_trans,n_err);
          CHECK( n_err == 0 );
          std::string geojson_string;
          CHECK( mapnik::util::to_geojson(geojson_string,projected_geom) );
          std::string geojson_file = "./test/data/simplified_geometry_pbf.geojson";
          mapnik::util::file input(geojson_file);
          if (!mapnik::util::exists(geojson_file) || (std::getenv("UPDATE") != nullptr))
          {
              std::ofstream out(geojson_file);
              out << geojson_string;
          } else if (input.is_open())
          {
              std::string json_string(input.data().get(), input.size());
              CHECK (geojson_string == json_string);
          }
          break;
      }
    }
    REQUIRE( found );
}

TEST_CASE("pbf raster tile output -- should be able to overzoom raster")
{
    unsigned tile_size = 4096;
    int buffer_size = 1024;
    mapnik::vector_tile_impl::merc_tile out_tile(0, 0, 0, tile_size, buffer_size);
    {
        mapnik::box2d<double> const& bbox = out_tile.extent();
        std::ostringstream s;
        s << std::fixed << std::setprecision(16)
          << bbox.minx() << ',' << bbox.miny() << ','
          << bbox.maxx() << ',' << bbox.maxy();

        // build map
        mapnik::Map map(tile_size,tile_size,"epsg:3857");
        map.set_buffer_size(buffer_size);
        mapnik::layer lyr("layer",map.srs());
        mapnik::parameters params;
        params["type"] = "gdal";
        params["extent"] = s.str();
        params["file"] = "test/data/256x256.png";
        std::shared_ptr<mapnik::datasource> ds = mapnik::datasource_cache::instance().create(params);
        lyr.set_datasource(ds);
        map.add_layer(lyr);

        // build processor
        mapnik::vector_tile_impl::processor ren(map);
        ren.set_image_format("jpeg");
        ren.set_scaling_method(mapnik::SCALING_BILINEAR);

        // Update the tile
        ren.update_tile(out_tile);
    }
    vector_tile::Tile tile;
    tile.ParseFromString(out_tile.get_buffer());
    // Done creating test data, now test created tile
    REQUIRE(1 == tile.layers_size());
    vector_tile::Tile_Layer const& layer = tile.layers(0);
    CHECK(std::string("layer") == layer.name());
    REQUIRE(1 == layer.features_size());
    vector_tile::Tile_Feature const& f = layer.features(0);
    CHECK(static_cast<mapnik::value_integer>(1) == static_cast<mapnik::value_integer>(f.id()));
    CHECK(0 == f.geometry_size());
    CHECK(f.has_raster());
    std::string const& ras_buffer = f.raster();
    CHECK(!ras_buffer.empty());

    // confirm tile looks correct as encoded
    std::string buffer;
    CHECK(tile.SerializeToString(&buffer));

    protozero::pbf_reader pbf_tile(buffer);
    pbf_tile.next();
    protozero::pbf_reader layer2 = pbf_tile.get_message();

    // now read back and render image at larger size
    // and zoomed in
    // 2/0/1.png
    mapnik::box2d<double> bbox = mapnik::vector_tile_impl::tile_mercator_bbox(0,1,2);
    mapnik::Map map2(256,256,"epsg:3857");
    map2.set_buffer_size(1024);
    mapnik::layer lyr2("layer",map2.srs());
    std::shared_ptr<mapnik::vector_tile_impl::tile_datasource_pbf> ds2 = std::make_shared<
                                    mapnik::vector_tile_impl::tile_datasource_pbf>(
                                        layer2,0,0,0);
    lyr2.set_datasource(ds2);
    lyr2.add_style("style");
    map2.add_layer(lyr2);
    mapnik::load_map(map2,"test/data/raster_style.xml");
    map2.zoom_to_box(bbox);
    mapnik::image_rgba8 im(map2.width(),map2.height());
    mapnik::agg_renderer<mapnik::image_rgba8> ren2(map2,im);
    ren2.apply();
    if (!mapnik::util::exists("test/fixtures/expected-3.png"))
    {
        mapnik::save_to_file(im,"test/fixtures/expected-3.png","png32");
    }
    unsigned diff = testing::compare_images(im,"test/fixtures/expected-3.png");
    CHECK(0 == diff);
    if (diff > 0)
    {
        mapnik::save_to_file(im,"test/fixtures/actual-3.png","png32");
    }
}

TEST_CASE("Check that we throw on various valid-but-we-don't-handle PBF encoded files")
{
    std::vector<std::string> filenames = {"test/data/tile_with_extra_feature_field.mvt",
                                          "test/data/tile_with_extra_layer_fields.mvt",
                                          "test/data/tile_with_invalid_layer_value_type.mvt",
                                          "test/data/tile_with_unexpected_geomtype.mvt"};

    for (auto const& f : filenames)
    {
        std::ifstream t(f);
        REQUIRE(t.is_open());
        std::string buffer((std::istreambuf_iterator<char>(t)),
                        std::istreambuf_iterator<char>());
        CHECK_THROWS({
            mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789);
            unsigned tile_size = 4096;
              protozero::pbf_reader pbf_tile(buffer);
              pbf_tile.next();
              protozero::pbf_reader layer2 = pbf_tile.get_message();
              mapnik::vector_tile_impl::tile_datasource_pbf ds(layer2,0,0,0);
              mapnik::featureset_ptr fs;
              mapnik::feature_ptr f_ptr;
              fs = ds.features(mapnik::query(bbox));
              f_ptr = fs->next();
              while (f_ptr != mapnik::feature_ptr()) {
                  f_ptr = fs->next();
              }
        });
   }

}

TEST_CASE("pbf vector tile from linestring geojson")
{
    unsigned tile_size = 4096;
    mapnik::Map map(tile_size,tile_size,"epsg:3857");
    mapnik::layer lyr("layer","epsg:4326");
    auto ds = testing::build_geojson_fs_ds("./test/data/linestrings_and_point.geojson");
    lyr.set_datasource(ds);
    map.add_layer(lyr);
    mapnik::vector_tile_impl::processor ren(map);
    // Request Tile
    mapnik::vector_tile_impl::tile out_tile = ren.create_tile(0,0,0);
    CHECK(out_tile.is_painted() == true);
    CHECK(out_tile.is_empty() == false);

    // Now check that the tile is correct.
    vector_tile::Tile tile;
    tile.ParseFromString(out_tile.get_buffer());
    REQUIRE(1 == tile.layers_size());
    vector_tile::Tile_Layer const& layer = tile.layers(0);
    CHECK(std::string("layer") == layer.name());
    REQUIRE(3 == layer.features_size());
    std::string buffer;
    tile.SerializeToString(&buffer);
    protozero::pbf_reader pbf_tile(buffer);
    pbf_tile.next();
    protozero::pbf_reader layer3 = pbf_tile.get_message();
    std::shared_ptr<mapnik::vector_tile_impl::tile_datasource_pbf> ds2 = std::make_shared<
                                    mapnik::vector_tile_impl::tile_datasource_pbf>(
                                        layer3,0,0,0);
    CHECK(ds2->get_name() == "layer");

    mapnik::box2d<double> bbox(-20037508.342789,-20037508.342789,20037508.342789,20037508.342789);
    mapnik::query q(bbox);
    q.add_property_name("x");
    q.add_property_name("y");
    q.add_property_name("pbool");

    std::size_t expected_num_attr_returned = q.property_names().size();
    auto fs = ds2->features(q);
    auto f_ptr = fs->next();
    REQUIRE(f_ptr != mapnik::feature_ptr());
    // no attributes
    CHECK(f_ptr->context()->size() == expected_num_attr_returned);
    CHECK(f_ptr->get_geometry().is<mapnik::geometry::line_string<double> >());

    // second feature
    f_ptr = fs->next();
    REQUIRE(f_ptr != mapnik::feature_ptr());
    CHECK(f_ptr->context()->size() == expected_num_attr_returned);
    CHECK(f_ptr->get_geometry().is<mapnik::geometry::line_string<double> >());

    // third feature (x : 0, y : 10, pbool : false)
    f_ptr = fs->next();
    REQUIRE(f_ptr != mapnik::feature_ptr());
    CHECK(f_ptr->context()->size() == expected_num_attr_returned);
    CHECK(f_ptr->get_geometry().is<mapnik::geometry::point<double> >());

    CHECK(f_ptr->has_key("x") == true);
    CHECK_NOTHROW(f_ptr->get("x").get<mapnik::value_integer>());
    CHECK(f_ptr->get("x") == 0);

    CHECK(f_ptr->has_key("y") == true);
    CHECK_NOTHROW(f_ptr->get("y").get<mapnik::value_integer>());
    CHECK(f_ptr->get("y") == 10);

    CHECK(f_ptr->has_key("pbool") == true);
    CHECK_NOTHROW(f_ptr->get("pbool").get<mapnik::value_bool>());
    CHECK(f_ptr->get("pbool") == false);

    // only three features
    f_ptr = fs->next();
    CHECK(f_ptr == mapnik::feature_ptr());
}
