#include "editor/feature_matcher.hpp"

#include "base/logging.hpp"
#include "base/stl_helpers.hpp"
#include "base/stl_iterator.hpp"

#include <algorithm>
#include <functional>
#include <string>
#include <utility>

#include <boost/geometry.hpp>
#include <boost/geometry/geometries/adapted/boost_tuple.hpp>
#include <boost/geometry/geometries/point_xy.hpp>
#include <boost/geometry/geometries/polygon.hpp>

using namespace std;

using editor::XMLFeature;

namespace
{
namespace bg = boost::geometry;

// Use simple xy coordinates because spherical are not supported by boost::geometry algorithms.
using PointXY = bg::model::d2::point_xy<double>;
using Polygon = bg::model::polygon<PointXY>;
using MultiPolygon = bg::model::multi_polygon<Polygon>;
using Linestring = bg::model::linestring<PointXY>;
using MultiLinestring = bg::model::multi_linestring<Linestring>;
using AreaType = bg::default_area_result<Polygon>::type;

using ForEachRefFn = function<void(XMLFeature const & xmlFt)>;
using ForEachWayFn = function<void(pugi::xml_node const & way, string const & role)>;

double const kPointDiffEps = 1e-5;
double const kPenaltyScore = -1;

template <typename LGeometry, typename RGeometry>
AreaType IntersectionArea(LGeometry const & our, RGeometry const & their)
{
  ASSERT(bg::is_valid(our), ());
  ASSERT(bg::is_valid(their), ());

  MultiPolygon result;
  bg::intersection(our, their, result);
  return bg::area(result);
}

void AddInnerIfNeeded(pugi::xml_document const & osmResponse, pugi::xml_node const & way,
                      Polygon & dest)
{
  if (dest.inners().empty() || dest.inners().back().empty())
    return;

  auto const refs = way.select_nodes("nd/@ref");
  if (refs.empty())
    return;

  string const nodeRef = refs[0].attribute().value();
  auto const node = osmResponse.select_node(("osm/node[@id='" + nodeRef + "']").data()).node();
  ASSERT(node, ("OSM response have ref", nodeRef, "but have no node with such id.", osmResponse));
  XMLFeature xmlFt(node);

  auto const & pt = dest.inners().back().back();
  m2::PointD lastPoint(pt.x(), pt.y());

  if (lastPoint.EqualDxDy(xmlFt.GetMercatorCenter(), kPointDiffEps))
    return;

  dest.inners().emplace_back();
}

void MakeOuterRing(MultiLinestring & outerLines, Polygon & dest)
{
  bool const needReverse =
      outerLines.size() > 1 && bg::equals(outerLines[0].front(), outerLines[1].back());

  for (size_t i = 0; i < outerLines.size(); ++i)
  {
    if (needReverse)
      bg::reverse(outerLines[i]);

    bg::append(dest.outer(), outerLines[i]);
  }
}

template <typename LGeometry, typename RGeometry>
double MatchByGeometry(LGeometry const & lhs, RGeometry const & rhs)
{
  if (!bg::is_valid(lhs) || !bg::is_valid(rhs))
    return kPenaltyScore;

  auto const lhsArea = bg::area(lhs);
  auto const rhsArea = bg::area(rhs);
  auto const intersectionArea = IntersectionArea(lhs, rhs);
  auto const unionArea = lhsArea + rhsArea - intersectionArea;

  // Avoid infinity.
  if (base::AlmostEqualAbs(unionArea, 0.0, 1e-18))
    return kPenaltyScore;

  auto const score = intersectionArea / unionArea;

  // If area of the intersection is a half of the object area, penalty score will be returned.
  if (score <= 0.5)
    return kPenaltyScore;

  return score;
}

MultiPolygon TrianglesToPolygon(vector<m2::PointD> const & points)
{
  size_t const kTriangleSize = 3;
  if (points.size() % kTriangleSize != 0)
    MYTHROW(matcher::NotAPolygonException, ("Count of points must be multiple of", kTriangleSize));

  vector<MultiPolygon> polygons;
  for (size_t i = 0; i < points.size(); i += kTriangleSize)
  {
    MultiPolygon polygon;
    polygon.resize(1);
    auto & p = polygon[0];
    auto & outer = p.outer();
    for (size_t j = i; j < i + kTriangleSize; ++j)
      outer.push_back(PointXY(points[j].x, points[j].y));
    bg::correct(p);
    if (!bg::is_valid(polygon))
      MYTHROW(matcher::NotAPolygonException, ("The triangle is not valid"));
    polygons.push_back(polygon);
  }

  if (polygons.empty())
    return {};

  auto & result = polygons[0];
  for (size_t i = 1; i < polygons.size(); ++i)
  {
    MultiPolygon u;
    bg::union_(result, polygons[i], u);
    u.swap(result);
  }
  return result;
}

/// Returns value form (-Inf, 1]. Negative values are used as penalty, positive as score.
double ScoreLatLon(XMLFeature const & xmlFt, ms::LatLon const & latLon)
{
  auto const a = MercatorBounds::FromLatLon(xmlFt.GetCenter());
  auto const b = MercatorBounds::FromLatLon(latLon);
  return 1.0 - (a.Length(b) / kPointDiffEps);
}

void ForEachRefInWay(pugi::xml_document const & osmResponse, pugi::xml_node const & way,
                     ForEachRefFn const & fn)
{
  for (auto const & xNodeRef : way.select_nodes("nd/@ref"))
  {
    string const nodeRef = xNodeRef.attribute().value();
    auto const node = osmResponse.select_node(("osm/node[@id='" + nodeRef + "']").data()).node();
    ASSERT(node, ("OSM response have ref", nodeRef, "but have no node with such id.", osmResponse));
    XMLFeature xmlFt(node);
    fn(xmlFt);
  }
}

void ForEachWayInRelation(pugi::xml_document const & osmResponse, pugi::xml_node const & relation,
                          ForEachWayFn const & fn)
{
  auto const nodesSet = relation.select_nodes("member[@type='way']/@ref");
  for (auto const & xNodeRef : nodesSet)
  {
    string const wayRef = xNodeRef.attribute().value();
    auto const xpath = "osm/way[@id='" + wayRef + "']";
    auto const way = osmResponse.select_node(xpath.c_str()).node();

    auto const rolePath = "member[@ref='" + wayRef + "']/@role";
    pugi::xpath_node roleNode = relation.select_node(rolePath.c_str());

    // It is possible to have a wayRef that refers to a way not included in a given relation.
    // We can skip such ways.
    if (!way)
      continue;

    // If more than one way is given and there is one with no role specified,
    // it's an error. We skip this particular way but try to use others anyway.
    if (!roleNode && nodesSet.size() != 1)
      continue;

    string role = "outer";
    if (roleNode)
      role = roleNode.attribute().value();

    fn(way, role);
  }
}

template <typename Geometry>
void AppendWay(pugi::xml_document const & osmResponse, pugi::xml_node const & way, Geometry & dest)
{
  ForEachRefInWay(osmResponse, way, [&dest](XMLFeature const & xmlFt)
  {
    auto const & p = xmlFt.GetMercatorCenter();
    bg::append(dest, boost::make_tuple(p.x, p.y));
  });
}

Polygon GetWaysGeometry(pugi::xml_document const & osmResponse, pugi::xml_node const & way)
{
  Polygon result;
  AppendWay(osmResponse, way, result);

  bg::correct(result);

  return result;
}

Polygon GetRelationsGeometry(pugi::xml_document const & osmResponse,
                             pugi::xml_node const & relation)
{
  Polygon result;
  MultiLinestring outerLines;

  auto const fn = [&osmResponse, &result, &outerLines](pugi::xml_node const & way,
                                                       string const & role)
  {
    if (role == "outer")
    {
      outerLines.emplace_back();
      AppendWay(osmResponse, way, outerLines.back());
    }
    else if (role == "inner")
    {
      if (result.inners().empty())
        result.inners().emplace_back();

      // Support several inner rings.
      AddInnerIfNeeded(osmResponse, way, result);
      AppendWay(osmResponse, way, result.inners().back());
    }
  };

  ForEachWayInRelation(osmResponse, relation, fn);

  MakeOuterRing(outerLines, result);
  bg::correct(result);

  return result;
}

Polygon GetWaysOrRelationsGeometry(pugi::xml_document const & osmResponse,
                                   pugi::xml_node const & wayOrRelation)
{
  if (strcmp(wayOrRelation.name(), "way") == 0)
    return GetWaysGeometry(osmResponse, wayOrRelation);
  return GetRelationsGeometry(osmResponse, wayOrRelation);
}

/// Returns value form [-1, 1]. Negative values are used as penalty, positive as score.
/// |osmResponse| - nodes, ways and relations from osm;
/// |wayOrRelation| - either way or relation to be compared agains ourGeometry;
/// |ourGeometry| - geometry of a FeatureType;
double ScoreGeometry(pugi::xml_document const & osmResponse, pugi::xml_node const & wayOrRelation,
                     vector<m2::PointD> const & ourGeometry)
{
  ASSERT(!ourGeometry.empty(), ("Our geometry cannot be empty"));

  auto const their = GetWaysOrRelationsGeometry(osmResponse, wayOrRelation);

  if (bg::is_empty(their))
    return kPenaltyScore;

  auto const our = TrianglesToPolygon(ourGeometry);

  if (bg::is_empty(our))
    return kPenaltyScore;

  return MatchByGeometry(our, their);
}
}  // namespace

namespace matcher
{
pugi::xml_node GetBestOsmNode(pugi::xml_document const & osmResponse, ms::LatLon const & latLon)
{
  double bestScore = kPenaltyScore;
  pugi::xml_node bestMatchNode;

  for (auto const & xNode : osmResponse.select_nodes("osm/node"))
  {
    try
    {
      XMLFeature xmlFt(xNode.node());

      double const nodeScore = ScoreLatLon(xmlFt, latLon);
      if (nodeScore < 0)
        continue;

      if (bestScore < nodeScore)
      {
        bestScore = nodeScore;
        bestMatchNode = xNode.node();
      }
    }
    catch (editor::NoLatLon const & ex)
    {
      LOG(LWARNING, ("No lat/lon attribute in osm response node.", ex.Msg()));
      continue;
    }
  }

  // TODO(mgsergio): Add a properly defined threshold when more fields will be compared.
  // if (bestScore < kMiniScoreThreshold)
  //   return pugi::xml_node;

  return bestMatchNode;
}

pugi::xml_node GetBestOsmWayOrRelation(pugi::xml_document const & osmResponse,
                                       vector<m2::PointD> const & geometry)
{
  double bestScore = kPenaltyScore;
  pugi::xml_node bestMatchWay;

  auto const xpath = "osm/way|osm/relation[tag[@k='type' and @v='multipolygon']]";
  for (auto const & xWayOrRelation : osmResponse.select_nodes(xpath))
  {
    double const nodeScore = ScoreGeometry(osmResponse, xWayOrRelation.node(), geometry);

    if (nodeScore < 0)
      continue;

    if (bestScore < nodeScore)
    {
      bestScore = nodeScore;
      bestMatchWay = xWayOrRelation.node();
    }
  }

  return bestMatchWay;
}

double ScoreTriangulatedGeometries(vector<m2::PointD> const & lhs, vector<m2::PointD> const & rhs)
{
  auto const lhsPolygon = TrianglesToPolygon(lhs);
  if (bg::is_empty(lhsPolygon))
    return kPenaltyScore;

  auto const rhsPolygon = TrianglesToPolygon(rhs);
  if (bg::is_empty(rhsPolygon))
    return kPenaltyScore;

  return MatchByGeometry(lhsPolygon, rhsPolygon);
}

double ScoreTriangulatedGeometriesByPoints(vector<m2::PointD> const & lhs,
                                           vector<m2::PointD> const & rhs)
{
  // The default comparison operator used in sort above (cmp1) and one that is
  // used in set_itersection (cmp2) are compatible in that sence that
  // cmp2(a, b) :- cmp1(a, b) and
  // cmp1(a, b) :- cmp2(a, b) || a almost equal b.
  // You can think of cmp2 as !(a >= b).
  // But cmp2 is not transitive:
  // i.e. !cmp(a, b) && !cmp(b, c) does NOT implies !cmp(a, c),
  // |a, b| < eps, |b, c| < eps.
  // This could lead to unexpected results in set_itersection (with greedy implementation),
  // but we assume such situation is very unlikely.
  auto const matched = set_intersection(begin(lhs), end(lhs),
                                        begin(rhs), end(rhs),
                                        CounterIterator(),
                                        [](m2::PointD const & p1, m2::PointD const & p2)
                                        {
                                          return p1 < p2 && !p1.EqualDxDy(p2, 1e-7);
                                        }).GetCount();

  return static_cast<double>(matched) / lhs.size();
}
}  // namespace matcher


/*
	Copyright 2014 Kristina Simpson <sweet.kristas@gmail.com>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
*/

#include <boost/graph/dijkstra_shortest_paths.hpp>
#include <boost/graph/graph_traits.hpp>
#include <boost/property_map/property_map.hpp>

#include "hex_logical_tiles.hpp"
#include "hex_pathfinding.hpp"
#include "profile_timer.hpp"
#include "units.hpp"

namespace hex
{
	// XXX Modify these to work with hex::logical::map
	hex_graph_ptr create_graph(const game::state& gs, int x, int y, int w, int h)
	{
		//profile::manager pman("create_graph");

		std::vector<point> vertices;
		std::vector<edge> edges;
		std::vector<cost> weights;

		auto& map = gs.get_map();

		if(w == 0) {
			w = map->width();
		}
		if(h == 0) {
			h = map->height();
		}

		// Scan through enemy entities add to dictionary keyed on position.
		// For each valid surrounding position of the enemy entity add this to a set of positions.
		// enemies on a tile make that tile unavailable as a desitination.
		// surrounding positions have no edges from them to other nodes. other nodes may
		// have edges to them.
		// XXX todo.

		//std::set<point> friendly_units;
		std::map<point, game::unit_ptr> enemy_units;
		std::set<point> surrounding_positions;
		auto cp = gs.get_entities().front()->get_owner();
		for(auto& u : gs.get_entities()) {
			auto owner = u->get_owner();
			auto& pos = u->get_position();
			if(cp->team() != owner->team()) {
				enemy_units[pos] = u;
				auto surrounds = map->get_surrounding_positions(pos.x, pos.y);
				for(auto& t : surrounds) {
					surrounding_positions.emplace(t);
				}
			} else {
				//friendly_units.emplace(pos);
			}
		}
		// remove enemy entities from surrounding positions
		auto team_current = gs.get_entities().front()->get_owner()->team();
		for(auto& e : gs.get_entities()) {
			auto e_team = e->get_owner()->team();
			if(e_team != team_current) {
				auto it = surrounding_positions.find(e->get_position());
				if(it != surrounding_positions.end()) {
					surrounding_positions.erase(it);
				}
			}
		}

		std::map<point, int> reverse_map;

		// find vertices and edges to construct the graph.
		vertices.reserve(w*h);
		for(int m = y; m != y+h; ++m) {
			for(int n = x; n != x+w; ++n) {
				point n1(n, m);
				auto surrounds = map->get_surrounding_positions(n1);
				// scan through entities for units at t
				auto it = enemy_units.find(n1);
				if(it == enemy_units.end()) {
					vertices.emplace_back(n1);
					reverse_map[n1] = vertices.size()-1;
					for(auto& n2 : surrounds) {
						if(n2.x >= x && n2.x < x+w
							&& n2.y >= y && n2.y < y+h
							&& enemy_units.find(n2) == enemy_units.end()) {
							const bool src_node_zoc = surrounding_positions.find(n1) != surrounding_positions.end();
							const bool dst_node_zoc = surrounding_positions.find(n2) != surrounding_positions.end();
							if(!src_node_zoc || !dst_node_zoc) {
								//std::cerr << "Adding edge from " << n1 << " to " << n2 << "\n";
								edges.emplace_back(n1, n2);
								weights.emplace_back(map->get_tile_at(n2)->get_cost());
							}
						}
					}
				}
			}
		}

		hex_graph_ptr graph = std::make_shared<graph_t>(vertices.size());
		graph->reverse_map.swap(reverse_map);
		graph->vertices.swap(vertices);
		WeightMap weightmap = boost::get(boost::edge_weight, graph->graph);
		size_t n = 0;
		for(auto& ep : edges) {
			edge_descriptor e;
			bool inserted;
			boost::tie(e, inserted) = boost::add_edge(graph->reverse_map[ep.first], graph->reverse_map[ep.second], graph->graph);
			weightmap[e] = map->get_tile_at(graph->vertices[e.m_target])->get_cost();//weights[n++];
		}

		return graph;
	}

	hex_graph_ptr create_cost_graph(const game::state& gs, const point& src, float max_cost)
	{
		auto& map = gs.get_map();
		int max_area = static_cast<int>(max_cost*4.0f+1.0f);
		int x = src.x - max_area/2;
		int w = max_area;
		if(x < 0) {
			w = w + x;
			x = 0;
		} else if(x >= static_cast<int>(map->width())) {
			w = w - (x - map->width() - 1);
			x = map->width() - 1;
		}
		int y = src.y - max_area/2;
		int h = max_area;
		if(y < 0) {
			h = h + y;
			y = 0;
		} else if(y >= static_cast<int>(map->height())) {
			h = h - (y - map->height() - 1);
			y = map->height() - 1;
		}

		return create_graph(gs, x, y, w, h);
	}

	result_list find_available_moves(hex_graph_ptr graph, const point& src, float max_cost)
	{
		//profile::manager pman("find_available_moves");

		result_list res;

		std::vector<cost> d(boost::num_vertices(graph->graph));
		std::vector<vertex> p(boost::num_vertices(graph->graph));
		boost::dijkstra_shortest_paths(graph->graph, graph->reverse_map[src],
			boost::predecessor_map(boost::make_iterator_property_map(p.begin(), boost::get(boost::vertex_index, graph->graph)))
				.distance_map(boost::make_iterator_property_map(d.begin(), boost::get(boost::vertex_index, graph->graph))));

		boost::graph_traits<hex_graph>::vertex_iterator vi, vend;
		for (boost::tie(vi, vend) = boost::vertices(graph->graph); vi != vend; ++vi) {
			if(d[*vi] < max_cost) {
				res.emplace_back(graph->vertices[*vi], d[*vi]);
			}
		}

		// remove tiles that have friendly entities on them, from the results.
		/*res.erase(std::remove_if(res.begin(), res.end(), [&friendly_units](const hex_object* t){
			return friendly_units.find(point(t->x(), t->y())) != friendly_units.end();
		}), res.end());*/

		return res;
	}

	struct found_goal {}; // exception for termination

	// visitor that terminates when we find the goal
	template<typename Vertex>
	class astar_goal_visitor : public boost::default_astar_visitor
	{
	public:
		astar_goal_visitor(Vertex goal) : goal_(goal) {}
		template<typename Graph> void examine_vertex(Vertex u, Graph& g) {
			if(u == goal_) {
				throw found_goal();
			}
		}
	private:
		Vertex goal_;
	};

	template <class Graph, class CostType>
	class astar_heuristic : public std::unary_function<typename boost::graph_traits<Graph>::vertex_descriptor, CostType>
	{
	public:
		typedef typename boost::graph_traits<Graph>::vertex_descriptor Vertex;
		astar_heuristic(Vertex goal, const std::vector<point>& vertices)
			: goal_x_(vertices[goal].x),
			  goal_y_(vertices[goal].y),
			  vertices_(vertices) {}
		CostType operator()(Vertex u)
		{
			const auto u_x = vertices_[u].x;
			const auto u_y = vertices_[u].y;
			return static_cast<CostType>((abs(u_x - goal_x_) + abs(u_y - goal_y_) + abs(u_x + u_y - goal_x_ - goal_y_)) / 2.0f);
		}
	private:
		int goal_x_;
		int goal_y_;
		const std::vector<point>& vertices_;
	};

	result_path find_path(hex_graph_ptr graph, const point& src, const point& dst)
	{
		//profile::manager pman("find_path");

		auto src_it = graph->reverse_map.find(src);
		ASSERT_LOG(src_it != graph->reverse_map.end(), "source node not in graph.");
		auto dst_it = graph->reverse_map.find(dst);
		ASSERT_LOG(dst_it != graph->reverse_map.end(), "destination node not in graph.");

		std::vector<vertex> p(boost::num_vertices(graph->graph));
		std::vector<cost> d(boost::num_vertices(graph->graph));
		try {
			boost::astar_search_tree(graph->graph, src_it->second, astar_heuristic<hex_graph, cost>(dst_it->second, graph->vertices),
				boost::predecessor_map(boost::make_iterator_property_map(p.begin(), boost::get(boost::vertex_index, graph->graph))).
				distance_map(boost::make_iterator_property_map(d.begin(), boost::get(boost::vertex_index, graph->graph))).
				visitor(astar_goal_visitor<vertex>(dst_it->second)));
		} catch(found_goal /*fg*/) {
			result_path shortest_path;
			for(vertex v = dst_it->second;; v = p[v]) {
				shortest_path.emplace_back(graph->vertices[v]);
				if(p[v] == v) {
					std::reverse(shortest_path.begin(), shortest_path.end());
					return shortest_path;
				}
			}
		}
		return result_path();
	}
}


#include <fstream>
#include <iostream>
#include <limits>
#include <list>
#include <string>
#include <vector>
#include <Eigen/Dense>

#include "asset.h"
#include "popup.h"
#include "popup_2d.h"
#include "popup_3d.h"

using namespace Eigen;
using namespace std;

namespace {

bool RasterArchitectureFloorplanToArchitectureMesh(
    const Floorplan& floorplan,
    const ArchitectureFloorplan& architecture_floorplan,
    const RasterArchitectureFloorplan& raster_architecture_floorplan,
    const Options& options,
    ArchitectureMesh& architecture_mesh) {
  if (!FloorToFloorMesh(raster_architecture_floorplan.width,
                        raster_architecture_floorplan.height,
                        raster_architecture_floorplan.floor,
                        options.wall_radius,
                        options.wall_uv_scale,
                        options.floor_uv_scale,
                        options.use_floorplan_image,
                        floorplan.image_width,
                        floorplan.image_height,
                        architecture_mesh))
    return false;

  if (!FloorToWallDoorWindowMesh(architecture_floorplan,
                                 raster_architecture_floorplan,
                                 options,
                                 architecture_mesh))
    return false;

  return true;
}

bool WriteMesh(const Mesh& mesh,
               const std::string& prefix,
               const std::string& mtl_filename) {
  if (mesh.triangles.empty())
    return true;

  ofstream ofstr;
  ofstr.open(prefix + mesh.name + string(".obj"));

  ofstr << "mtllib " << mtl_filename << endl;

  for (const auto& triangle : mesh.triangles) {
    for (int i = 0; i < 3; ++i) {
      ofstr << "v "
            << triangle.vertices[i][0] << ' '
            << triangle.vertices[i][1] << ' '
            << triangle.vertices[i][2] << endl;
    }
  }

  if (mesh.material == "") {
    int vertex_index = 1;
    for (const auto& triangle : mesh.triangles) {
      ofstr << "f "
            << vertex_index << ' '
            << vertex_index + 1 << ' '
            << vertex_index + 2 << endl;

      vertex_index += 3;
    }
  } else {
    ofstr << "usemtl " << mesh.material << endl;

    int vertex_index = 1;
    for (const auto& triangle : mesh.triangles) {
      for (int i = 0; i < 3; ++i) {
        ofstr << "vt "
              << triangle.uvs[i][0] << ' '
              << triangle.uvs[i][1] << endl;
      }

      ofstr << "f "
            << vertex_index << '/' << vertex_index << ' '
            << vertex_index + 1 << '/' << vertex_index + 1 << ' '
            << vertex_index + 2 << '/' << vertex_index + 2 << endl;

      vertex_index += 3;
    }
  }

  ofstr.close();
  return true;
}

int RangeDistance(const Vector2i& range0, const Vector2i& range1) {
  if (range0[1] < range1[0])
    return range1[0] - range0[1];
  else if (range1[1] < range0[0])
    return range0[0] - range1[1];
  else
    return 0;
}

}  // namespace

bool ReadFloorplan(const string& filename, Floorplan& floorplan) {
  ifstream ifstr;
  ifstr.open(filename.c_str());
  if (!ifstr.is_open()) {
    cerr << "Failed in reading the file: " << filename << endl;
    return false;
  }

  int num_walls;
  ifstr >> floorplan.image_width >> floorplan.image_height >> num_walls;

  for (int w = 0; w < num_walls; ++w) {
    Annotation annotation;
    ifstr >> annotation.xs[0] >> annotation.ys[0]
          >> annotation.xs[1] >> annotation.ys[1]
          >> annotation.attributes[0]
          >> annotation.attributes[1];
    annotation.label = "wall";

    const int newy0 = floorplan.image_height - annotation.ys[1];
    const int newy1 = floorplan.image_height - annotation.ys[0];

    annotation.ys[0] = newy0;
    annotation.ys[1] = newy1;

    floorplan.annotations.push_back(annotation);
  }

  while (true) {
    Annotation annotation;
    ifstr >> annotation.xs[0] >> annotation.ys[0]
          >> annotation.xs[1] >> annotation.ys[1]
          >> annotation.label
          >> annotation.attributes[0]
          >> annotation.attributes[1];
    if (ifstr.eof())
      break;

    const int newy0 = floorplan.image_height - annotation.ys[1];
    const int newy1 = floorplan.image_height - annotation.ys[0];

    annotation.ys[0] = newy0;
    annotation.ys[1] = newy1;

    floorplan.annotations.push_back(annotation);
  }
  ifstr.close();
  cerr << "Read " << floorplan.annotations.size() << " annotations." << endl;

  floorplan.floorplan_txt_path = filename;
  floorplan.floorplan_jpg_filename = filename;
  {
    string& stmp = floorplan.floorplan_jpg_filename;
    const int length = stmp.length();
    stmp[length - 3] = 'p';
    stmp[length - 2] = 'n';
    stmp[length - 1] = 'g';
    stmp = stmp.substr(stmp.find_last_of('/') + 1, length);
  }

  floorplan.mtl_path = filename;
  {
    string& stmp = floorplan.mtl_path;
    const int length = stmp.length();
    stmp[length - 3] = 'm';
    stmp[length - 2] = 't';
    stmp[length - 1] = 'l';

    floorplan.mtl_filename = stmp.substr(stmp.find_last_of('/') + 1, length);
  }

  return true;

  /*
  ifstream ifstr;
  ifstr.open(filename.c_str());
  if (!ifstr.is_open()) {
    cerr << "Failed in reading the file: " << filename << endl;
    return false;
  }

  ifstr >> floorplan.image_width >> floorplan.image_height;
  while (true) {
    Annotation annotation;
    ifstr >> annotation.xs[0] >> annotation.ys[0]
          >> annotation.xs[1] >> annotation.ys[1]
          >> annotation.label
          >> annotation.attributes[0]
          >> annotation.attributes[1];
    if (ifstr.eof())
      break;

    const int newy0 = floorplan.image_height - annotation.ys[1];
    const int newy1 = floorplan.image_height - annotation.ys[0];

    annotation.ys[0] = newy0;
    annotation.ys[1] = newy1;

    floorplan.annotations.push_back(annotation);
  }
  ifstr.close();
  cerr << "Read " << floorplan.annotations.size() << " annotations." << endl;

  floorplan.floorplan_txt_path = filename;
  floorplan.floorplan_jpg_filename = filename;
  {
    string& stmp = floorplan.floorplan_jpg_filename;
    const int length = stmp.length();
    stmp[length - 3] = 'p';
    stmp[length - 2] = 'n';
    stmp[length - 1] = 'g';
    stmp = stmp.substr(stmp.find_last_of('/') + 1, length);
  }

  floorplan.mtl_path = filename;
  {
    string& stmp = floorplan.mtl_path;
    const int length = stmp.length();
    stmp[length - 3] = 'm';
    stmp[length - 2] = 't';
    stmp[length - 1] = 'l';

    floorplan.mtl_filename = stmp.substr(stmp.find_last_of('/') + 1, length);
  }

  return true;
  */
}

bool ChangeDoorsToWindows(const Options& options, Floorplan& floorplan) {
  const int kExterior = 11;

  vector<Vector4i> entrance_xs_ys;
  for (auto& entrance : floorplan.annotations) {
    if (entrance.label != "entrance")
      continue;
    entrance_xs_ys.push_back(Vector4i(entrance.xs[0], entrance.xs[1], entrance.ys[0], entrance.ys[1]));
  }

  for (auto& door : floorplan.annotations) {
    if (door.label != "door")
      continue;

    // Close from entrance?
    int distance_from_entrance = numeric_limits<int>::max();
    for (const auto& xs_ys : entrance_xs_ys) {
      const int distance =
        RangeDistance(Vector2i(door.xs[0], door.xs[1]), Vector2i(xs_ys[0], xs_ys[1])) +
        RangeDistance(Vector2i(door.ys[0], door.ys[1]), Vector2i(xs_ys[2], xs_ys[3]));
      distance_from_entrance = min(distance, distance_from_entrance);
    }

    if (distance_from_entrance < options.entrance_distance)
        continue;

    // If it is on a wall which is next to the exterior.
    for (const auto& wall : floorplan.annotations) {
      // Wall.
      if (wall.label != "wall")
        continue;
      // Exterior.
      if (wall.attributes[0] != kExterior && wall.attributes[1] != kExterior)
        continue;

      // If wall contains a door.
      if (wall.xs[0] <= door.xs[0] && door.xs[1] <= wall.xs[1] &&
          wall.ys[0] <= door.ys[0] && door.ys[1] <= wall.ys[1]) {
        door.label = "window";
      }
    }
  }

  return true;
}

bool ExtractArchitectureFloorplan(const Floorplan& floorplan, ArchitectureFloorplan& architecture_floorplan) {
  const string kWall   = "wall";
  const string kDoor   = "door";
  const string kWindow = "window";
  for (const auto& annotation : floorplan.annotations) {
    if (annotation.label == kWall)
      architecture_floorplan.walls.push_back(annotation);
    else if (annotation.label == kDoor)
      architecture_floorplan.doors.push_back(annotation);
    else if (annotation.label == kWindow)
      architecture_floorplan.windows.push_back(annotation);
  }

  return true;
}

bool GenerateArchitectureMesh(const Floorplan& floorplan,
                              const ArchitectureFloorplan& architecture_floorplan,
                              const Options& options,
                              RasterArchitectureFloorplan& raster_architecture_floorplan,
                              ArchitectureMesh& architecture_mesh) {
  int min_x, max_x, min_y, max_y;
  if (!GetMinMaxXY(architecture_floorplan, min_x, max_x, min_y, max_y))
    return false;

  if (min_x <= options.wall_radius || min_y <= options.wall_radius) {
    cerr << "Walls are too close to the image boundary." << endl;
    return false;
  }

  const int kMargin = 2;
  const int width  = max_x + options.wall_radius + kMargin;
  const int height = max_y + options.wall_radius + kMargin;

  if (!SetRasterArchitectureFloorplan(architecture_floorplan,
                                      options, width, height, raster_architecture_floorplan))
    return false;

  if (!RasterArchitectureFloorplanToArchitectureMesh(
          floorplan,
          architecture_floorplan,
          raster_architecture_floorplan,
          options,
          architecture_mesh))
    return false;

  return true;
}

bool WriteArchitectureMesh(const ArchitectureMesh& architecture_mesh,
                           const std::string& prefix,
                           const std::string& mtl_filename) {
  if (!WriteMesh(architecture_mesh.floor, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.bathroom_floor, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.kitchen_floor, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.dining_floor, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.bedroom_floor, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.floor_bottom, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.wall, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.bathroom_wall, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.kitchen_wall, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.dining_wall, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.bedroom_wall, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.door0, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.door1, prefix, mtl_filename))
    return false;

  if (!WriteMesh(architecture_mesh.window, prefix, mtl_filename))
    return false;

  return true;
}

bool WriteObjects(const Floorplan& floorplan,
                  const std::vector<Vector4d>& free_space_ratios,
                  const double scale,
                  const std::string& prefix) {
  int bathtub_id = 0;
  int washing_basin_id0 = 0;
  int washing_basin_id1 = 0;
  int cooking_counter_id = 0;
  int toilet_id = 0;
  int stairs_id = 0;
  //----------------------------------------------------------------------
  AssetDatabase database;
  if (!InitializeAssetDatabase(database))
    return false;

  for (int obj = 0; obj < floorplan.annotations.size(); ++obj) {
    const Annotation& annotation = floorplan.annotations[obj];
    const Vector4d free_space_ratio = free_space_ratios[obj];

    Asset asset;
    char filename[1024];
    if (annotation.label == "bathtub") {
      asset = database.bathtub;
      sprintf(filename, "%sbathtub%02d.obj", prefix.c_str(), bathtub_id++);
    } else if (annotation.label == "washing_basin") {
      if (annotation.attributes[0] == 1) {
        asset = database.washing_basin0;
        sprintf(filename, "%swashing_basin0%02d.obj", prefix.c_str(), washing_basin_id0++);
      } else {
        asset = database.washing_basin1;
        sprintf(filename, "%swashing_basin1%02d.obj", prefix.c_str(), washing_basin_id1++);
      }
    } else if (annotation.label == "cooking_counter") {
      asset = database.cooking_counter;
      sprintf(filename, "%scooking_counter%02d.obj", prefix.c_str(), cooking_counter_id++);
    } else if (annotation.label == "toilet") {
      asset = database.toilet;
      sprintf(filename, "%stoilet%02d.obj", prefix.c_str(), toilet_id++);
    } else if (annotation.label == "stairs") {
      asset = database.stairs;
      sprintf(filename, "%sstairs%02d.obj", prefix.c_str(), stairs_id++);
    } else {
      continue;
    }

    const double kZ = 0;
    Vector3d annotation_min_xyz(annotation.xs[0], annotation.ys[0], kZ);
    Vector3d annotation_max_xyz(annotation.xs[1], annotation.ys[1], kZ);
    Vector3d center = (annotation_min_xyz + annotation_max_xyz) / 2.0;
    annotation_min_xyz = center + (annotation_min_xyz - center) * scale;
    annotation_max_xyz = center + (annotation_max_xyz - center) * scale;

    if (!AlignAsset(annotation_min_xyz, annotation_max_xyz, free_space_ratio, asset))
      return false;

    if (!WriteAssetObj(filename, asset))
      return false;
  }

  return true;
}

bool WriteMtl(const Options& options,
              const std::string& mtl_path,
              const std::string& floorplan_jpg_filename) {
  ofstream ofstr;
  ofstr.open(mtl_path.c_str());
  if (!ofstr.is_open())
    return false;

  ofstr << "newmtl floor" << endl;
  if (options.use_floorplan_image) {
    ofstr << "  map_Ka " << floorplan_jpg_filename << endl
          << "  map_Kd " << floorplan_jpg_filename << endl;
  } else {
    ofstr << "  map_Ka floor.jpg" << endl
          << "  map_Kd floor.jpg" << endl;
  }
  ofstr << "newmtl door0" << endl
        << "  map_Ka door.jpg" << endl
        << "  map_Kd door.jpg" << endl
        << "newmtl door1" << endl
        << "  map_Ka door2.jpg" << endl
        << "  map_Kd door2.jpg" << endl
        << "newmtl wall" << endl
        << "  map_Ka wall.jpg" << endl
        << "  map_Kd wall.jpg" << endl
        << "newmtl bathroom_wall" << endl
        << "  map_Ka bathroom_wall.jpg" << endl
        << "  map_Kd bathroom_wall.jpg" << endl
        << "newmtl kitchen_wall" << endl
        << "  map_Ka kitchen_wall.jpg" << endl
        << "  map_Kd kitchen_wall.jpg" << endl
        << "newmtl dining_wall" << endl
        << "  map_Ka dining_wall.jpg" << endl
        << "  map_Kd dining_wall.jpg" << endl
        << "newmtl bedroom_wall" << endl
        << "  map_Ka bedroom_wall.jpg" << endl
        << "  map_Kd bedroom_wall.jpg" << endl
        << "newmtl window" << endl
        << "  map_Ka window.jpg" << endl
        << "  map_Kd window.jpg" << endl
        << "newmtl white" << endl;

  return true;
}


//
// Copyright (c) 2017-2020 Project PAI Foundation
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
//

#include "wallet/test/wallet_stake_test_fixture.h"

#include <boost/test/unit_test.hpp>

#include "stake/stakepoolfee.h"
#include "validation.h"
#include "policy/policy.h"
#include "wallet/coincontrol.h"
#include "net.h"
#include "rpc/server.h"
#include "miner.h"
#include "consensus/tx_verify.h"

BOOST_FIXTURE_TEST_SUITE(ticket_tests, WalletStakeTestingSetup)

// test correctness of stake pool fee percent validation
BOOST_AUTO_TEST_CASE(stake_pool_fee_percent_test)
{
    // tests with values as doubles

    struct TestDataDouble {
        double percent;
        bool expected;
    };

    std::vector<TestDataDouble> doubleTests {
        {0.01, true},
        {0.011, true},
        {0.011, true},
        {.05, true},
        {0.1, true},
        {0.1234567890, true},
        {1.0, true},
        {10.0, true},
        {25.0, true},
        {25.78, true},
        {50.0, true},
        {99.0, true},
        {99.99, true},
        {100.0, true},
        {0.0, false},
        {100.000001, false},
        {1000.0, false}
    };

    for (auto& test: doubleTests)
        BOOST_CHECK_EQUAL(IsValidPoolFeePercent(test.percent), test.expected);

    // tests with values as integers

    struct TestDataInteger {
        int percent;
        bool expected;
    };

    std::vector<TestDataInteger> integerTests {
        {1, true},
        {2, true},
        {10, true},
        {50, true},
        {100, true},
        {0, false},
        {101, false},
        {1000, false}
    };

    for (auto& test: integerTests)
        BOOST_CHECK_EQUAL(IsValidPoolFeePercent(test.percent), test.expected);
}

// test stake pool ticket fee calculation
BOOST_AUTO_TEST_CASE(stake_pool_ticket_fee_test)
{
    struct TestData {
        CAmount stakeDiff;
        CAmount fee;
        int height;
        double poolFee;
        CAmount expected;
    };

    // test with good data

    // TODO: Update these if still failing after finalizing all the implementations in the voter subsidy
    std::vector<TestData> goodTests {
        {10 * COIN, static_cast<CAmount>(0.01 * COIN), 25000, 1.00, static_cast<CAmount>(0.01500463 * COIN)},
        {20 * COIN, static_cast<CAmount>(0.01 * COIN), 25000, 1.00, static_cast<CAmount>(0.01621221 * COIN)},
        { 5 * COIN, static_cast<CAmount>(0.05 * COIN), 50000, 2.59, static_cast<CAmount>(0.03310616 * COIN)},
        {15 * COIN, static_cast<CAmount>(0.05 * COIN), 50000, 2.59, static_cast<CAmount>(0.03956376 * COIN)}
    };

    //for (auto& test: goodTests)
    //    BOOST_CHECK_EQUAL(StakePoolTicketFee(test.stakeDiff, test.fee, test.height, test.poolFee), test.expected);

    // test with bad data

    std::vector<TestData> badTests {
        {10 * COIN, static_cast<CAmount>(0.01 * COIN), 25000, 1.00, static_cast<CAmount>(0.12345678 * COIN)},
        {20 * COIN, static_cast<CAmount>(0.01 * COIN), 25000, 1.00, static_cast<CAmount>(0.01621222 * COIN)},
        { 5 * COIN, static_cast<CAmount>(0.05 * COIN), 50000, 2.59, static_cast<CAmount>(0.03313616 * COIN)},
        {15 * COIN, static_cast<CAmount>(0.05 * COIN), 50000, 2.59, static_cast<CAmount>(0.04956376 * COIN)},
        {11 * COIN, static_cast<CAmount>(0.01 * COIN), 25000, 1.00, static_cast<CAmount>(0.01500463 * COIN)},
        {20 * COIN, static_cast<CAmount>(0.02 * COIN), 25000, 1.00, static_cast<CAmount>(0.01621221 * COIN)},
        { 5 * COIN, static_cast<CAmount>(0.05 * COIN), 51000, 2.59, static_cast<CAmount>(0.03310616 * COIN)},
        {15 * COIN, static_cast<CAmount>(0.05 * COIN), 50000, 2.71, static_cast<CAmount>(0.03956376 * COIN)}
    };

    //for (auto& test: badTests)
    //    BOOST_CHECK(StakePoolTicketFee(test.stakeDiff, test.fee, test.height, test.poolFee) != test.expected);
}

// test fee limits for ticket (vote or revoke)
BOOST_FIXTURE_TEST_CASE(ticket_vote_or_revoke_fee_limits, WalletStakeTestingSetup)
{
    struct TestData {
        CAmount voteFeeLimit;
        CAmount revocationFeeLimit;

        CAmount expectedVoteFeeLimit;
        CAmount expectedRevocationFeeLimit;
    };

    std::vector<TestData> tests {
        {                    0,                    0,         0,          0},
        {                    1,                    1,         1,          1},
        {                    2,                    2,         2,          2},
        {                   10,                   11,        10,         11},
        {                 1000,                 1001,      1000,       1001},
        {              10*COIN,               5*COIN,   10*COIN,     5*COIN},
        {            1000*COIN,            2000*COIN, 1000*COIN,  2000*COIN},
        {            MAX_MONEY,            MAX_MONEY, MAX_MONEY,  MAX_MONEY},
        // values below should be capped at valid money amounts
        {                   -1,                   -1,         0,          0},
        {                -1000,                -1000,         0,          0},
        {             -10*COIN,             -10*COIN,         0,          0},
        {           -1000*COIN,           -1000*COIN,         0,          0},
        {           -MAX_MONEY,           -MAX_MONEY,         0,          0},
        { -MAX_MONEY-1000*COIN, -MAX_MONEY-1000*COIN,         0,          0},
        {          MAX_MONEY+1,          MAX_MONEY+1, MAX_MONEY,  MAX_MONEY},
        {  MAX_MONEY+1000*COIN,  MAX_MONEY+2000*COIN, MAX_MONEY,  MAX_MONEY}
    };

    // default fee

    BOOST_CHECK_EQUAL(TicketContribData::DefaultFeeLimit, 1LL<<20);

    // fee limit accessors

    for (auto& test: tests) {
        // constructor

        TicketContribData tcd1{1, CKeyID(), 0LL, test.voteFeeLimit, test.revocationFeeLimit};

        BOOST_CHECK_EQUAL(tcd1.voteFeeLimit(), test.expectedVoteFeeLimit);
        BOOST_CHECK_EQUAL(tcd1.revocationFeeLimit(), test.expectedRevocationFeeLimit);

        // setters

        TicketContribData tcd2;

        tcd2.setVoteFeeLimit(test.voteFeeLimit);
        BOOST_CHECK_EQUAL(tcd2.voteFeeLimit(), test.expectedVoteFeeLimit);

        tcd2.setRevocationFeeLimit(test.revocationFeeLimit);
        BOOST_CHECK_EQUAL(tcd2.revocationFeeLimit(), test.expectedRevocationFeeLimit);
    }

    // input validation

    LOCK2(cs_main, wallet->cs_wallet);

    BOOST_CHECK(GetSomeCoins());

    CPubKey pubKey;
    BOOST_CHECK(wallet->GetKeyFromPool(pubKey));
    const CTxDestination addr = pubKey.GetID();

    const CAmount& ticketPrice = CalculateNextRequiredStakeDifficulty(chainActive.Tip(), consensus);
    BOOST_CHECK_GT(ticketPrice, 0LL);

    uint256 splitTxtHash = SendMoney({std::make_pair(addr, ticketPrice)});
    BOOST_CHECK(splitTxtHash != emptyData.hash);

    CCoinsView viewDummy;
    CCoinsViewCache view(&viewDummy);
    CCoinsViewMemPool viewMempool(pcoinsTip, mempool);
    view.SetBackend(viewMempool);

    CMutableTransaction mtx;

    mtx.vin.push_back(CTxIn(splitTxtHash, 0));

    mtx.vout.push_back(CTxOut(0, GetScriptForBuyTicketDecl(BuyTicketData{1})));
    mtx.vout.push_back(CTxOut(ticketPrice, GetScriptForDestination(addr)));
    mtx.vout.push_back(CTxOut(0, GetScriptForTicketContrib(TicketContribData(1, addr, ticketPrice, 0, TicketContribData::DefaultFeeLimit))));
    mtx.vout.push_back(CTxOut(0, GetScriptForDestination(addr)));

    CAmount nFees{0};
    CValidationState state;

    BOOST_CHECK(Consensus::CheckTxInputs(mtx, state, view, GetSpendHeight(view), nFees, params));

    TicketContribData contrib;
    ParseTicketContrib(mtx, ticketContribOutputIndex, contrib);

    if (consensus.nMinimumTotalVoteFeeLimit > 0) {
        contrib.setVoteFeeLimit(consensus.nMinimumTotalVoteFeeLimit-1);
        mtx.vout[ticketContribOutputIndex].scriptPubKey = GetScriptForTicketContrib(contrib);
        BOOST_CHECK(!Consensus::CheckTxInputs(mtx, state, view, GetSpendHeight(view), nFees, params) && state.GetRejectReason() == "total-vote-fee-limit-too-low");
    }

    if (consensus.nMinimumTotalRevocationFeeLimit > 0) {
        contrib.setVoteFeeLimit(consensus.nMinimumTotalVoteFeeLimit);
        contrib.setRevocationFeeLimit(consensus.nMinimumTotalRevocationFeeLimit-1);
        mtx.vout[ticketContribOutputIndex].scriptPubKey = GetScriptForTicketContrib(contrib);
        BOOST_CHECK(!Consensus::CheckTxInputs(mtx, state, view, GetSpendHeight(view), nFees, params) && state.GetRejectReason() == "total-revocation-fee-limit-too-low");
    }

    view.SetBackend(viewDummy);
}

// test the serialization and deserialization of the ticket contribution data
BOOST_AUTO_TEST_CASE(ticket_contrib_data_serialization)
{
    struct TestData {
        TicketContribData tcd;

        CScript expectedScript;
        TicketContribData expectedTcd;
    };

    CPubKey pubKey;
    BOOST_CHECK(wallet->GetKeyFromPool(pubKey));
    const CTxDestination addr = pubKey.GetID();

    auto scriptBuilder = [](TicketContribData tcd) -> CScript {
        return CScript() << OP_RETURN << OP_STRUCT << 1 << CLASS_Staking << STAKE_TicketContribution
                         << tcd.nVersion
                         << ToByteVector(tcd.rewardAddr) << tcd.whichAddr
                         << tcd.contributedAmount
                         << tcd.voteFeeLimit()
                         << tcd.revocationFeeLimit();
    };

    std::vector<TestData> tests {
        {
            TicketContribData(1, addr, 5*COIN, 10, 0),
            scriptBuilder(TicketContribData(1, addr, 5*COIN, 10, 0)),
            TicketContribData(1, addr, 5*COIN, 10, 0)
        },
        {
            TicketContribData(2, addr, 10*COIN, 0, 10),
            scriptBuilder(TicketContribData(2, addr, 10*COIN, 0, 10)),
            TicketContribData(2, addr, 10*COIN, 0, 10)
        },
        {
            TicketContribData(3, addr, 20*COIN, 20, 0),
            scriptBuilder(TicketContribData(3, addr, 20*COIN, 20, 0)),
            TicketContribData(3, addr, 20*COIN, 20, 0)
        },
        {
            TicketContribData(4, addr, 30*COIN, 0, 30),
            scriptBuilder(TicketContribData(4, addr, 30*COIN, 0, 30)),
            TicketContribData(4, addr, 30*COIN, 0, 30)
        },
        {
            TicketContribData(5, addr, 40*COIN, 15, 25),
            scriptBuilder(TicketContribData(5, addr, 40*COIN, 15, 25)),
            TicketContribData(5, addr, 40*COIN, 15, 25)
        },
        {
            TicketContribData(6, addr, 100*COIN, 40, 30),
            scriptBuilder(TicketContribData(6, addr, 100*COIN, 40, 30)),
            TicketContribData(6, addr, 100*COIN, 40, 30)
        },
        {
            TicketContribData(4, addr, 30*COIN, 0, TicketContribData::DefaultFeeLimit),
            scriptBuilder(TicketContribData(4, addr, 30*COIN, 0, TicketContribData::DefaultFeeLimit)),
            TicketContribData(4, addr, 30*COIN, 0, TicketContribData::DefaultFeeLimit)
        },
        {
            TicketContribData(4, addr, 30*COIN, TicketContribData::DefaultFeeLimit, 0),
            scriptBuilder(TicketContribData(4, addr, 30*COIN, TicketContribData::DefaultFeeLimit, 0)),
            TicketContribData(4, addr, 30*COIN, TicketContribData::DefaultFeeLimit, 0)
        },
        {
            TicketContribData(5, addr, 40*COIN, 1000*COIN, 25),
            scriptBuilder(TicketContribData(5, addr, 40*COIN, 1000*COIN, 25)),
            TicketContribData(5, addr, 40*COIN, 1000*COIN, 25)
        },
        {
            TicketContribData(6, addr, 100*COIN, 40, 1000*COIN),
            scriptBuilder(TicketContribData(6, addr, 100*COIN, 40, 1000*COIN)),
            TicketContribData(6, addr, 100*COIN, 40, 1000*COIN)
        },
        {
            TicketContribData(5, addr, 40*COIN, 10000000*COIN, 25),
            scriptBuilder(TicketContribData(5, addr, 40*COIN, 10000000*COIN, 25)),
            TicketContribData(5, addr, 40*COIN, 10000000*COIN, 25)
        },
        {
            TicketContribData(6, addr, 100*COIN, 40, 10000000*COIN),
            scriptBuilder(TicketContribData(6, addr, 100*COIN, 40, 10000000*COIN)),
            TicketContribData(6, addr, 100*COIN, 40, 10000000*COIN)
        },
        {
            TicketContribData(5, addr, 40*COIN, 10000000*COIN, 25),
            scriptBuilder(TicketContribData(5, addr, 40*COIN, 10000000*COIN, 25)),
            TicketContribData(5, addr, 40*COIN, 10000000*COIN, 25)
        },
        {
            TicketContribData(6, addr, 100*COIN, 40, 10000000*COIN),
            scriptBuilder(TicketContribData(6, addr, 100*COIN, 40, 10000000*COIN)),
            TicketContribData(6, addr, 100*COIN, 40, 10000000*COIN)
        },
        {
            TicketContribData(5, addr, 40*COIN, -1, MAX_MONEY+1),
            scriptBuilder(TicketContribData(5, addr, 40*COIN, 0, MAX_MONEY)),
            TicketContribData(5, addr, 40*COIN, 0, MAX_MONEY)
        },
        {
            TicketContribData(6, addr, 100*COIN, MAX_MONEY+1, -1),
            scriptBuilder(TicketContribData(6, addr, 100*COIN, MAX_MONEY, 0)),
            TicketContribData(6, addr, 100*COIN, MAX_MONEY, 0)
        }
    };

    CMutableTransaction tx;
    std::vector<unsigned char> serializedTxOut;
    TicketContribData deserializedTcd;

    for (auto& test: tests) {
        BOOST_CHECK(test.tcd == test.expectedTcd);

        CScript script = GetScriptForTicketContrib(test.tcd);
        BOOST_CHECK(script == test.expectedScript);

        tx.vout.clear();

        CTxOut txOut{0, script};
        tx.vout.push_back(txOut);
        BOOST_CHECK(ParseTicketContrib(tx, 0, deserializedTcd));

        BOOST_CHECK(deserializedTcd == test.expectedTcd);
    }

    // validated deserialization

    {
        const uint160 dummyAddr{};

        auto flScriptBuilder = [&dummyAddr](const CAmount& vfl, const CAmount& rfl) -> CScript {
            return CScript() << OP_RETURN << OP_STRUCT << 1 << CLASS_Staking << STAKE_TicketContribution
                             << 1
                             << ToByteVector(dummyAddr) << 1
                             << 1000*COIN
                             << vfl
                             << rfl;
        };

        CScript script = flScriptBuilder(-1, MAX_MONEY+1);
        tx.vout.clear(); tx.vout.push_back(CTxOut{0, script});
        BOOST_CHECK(!ParseTicketContrib(tx, 0, deserializedTcd));

        script = flScriptBuilder(MAX_MONEY+1, -1);
        tx.vout.clear(); tx.vout.push_back(CTxOut{0, script});
        BOOST_CHECK(!ParseTicketContrib(tx, 0, deserializedTcd));
    }
}

// test the serialization and deserialization of the ticket multiple contribution data
BOOST_AUTO_TEST_CASE(ticket_contribs_data_serialization)
{
    struct TestData {
        TicketContribData tcd;

        CScript expectedScript;
        TicketContribData expectedTcd;
    };

    CPubKey pubKey;
    BOOST_CHECK(wallet->GetKeyFromPool(pubKey));
    const CTxDestination addr = pubKey.GetID();

    auto scriptBuilder = [](TicketContribData tcd) -> CScript {
        return CScript() << OP_RETURN << OP_STRUCT << 1 << CLASS_Staking << STAKE_TicketContribution
                         << tcd.nVersion
                         << ToByteVector(tcd.rewardAddr) << tcd.whichAddr
                         << tcd.contributedAmount
                         << tcd.voteFeeLimit()
                         << tcd.revocationFeeLimit();
    };

    std::vector<TestData> tests {
        {
            TicketContribData(1, addr, 5*COIN, 10, 0),
            scriptBuilder(TicketContribData(1, addr, 5*COIN, 10, 0)),
            TicketContribData(1, addr, 5*COIN, 10, 0)
        },
        {
            TicketContribData(2, addr, 10*COIN, 0, 10),
            scriptBuilder(TicketContribData(2, addr, 10*COIN, 0, 10)),
            TicketContribData(2, addr, 10*COIN, 0, 10)
        },
        {
            TicketContribData(3, addr, 20*COIN, 20, 0),
            scriptBuilder(TicketContribData(3, addr, 20*COIN, 20, 0)),
            TicketContribData(3, addr, 20*COIN, 20, 0)
        },
        {
            TicketContribData(4, addr, 30*COIN, 0, 30),
            scriptBuilder(TicketContribData(4, addr, 30*COIN, 0, 30)),
            TicketContribData(4, addr, 30*COIN, 0, 30)
        },
        {
            TicketContribData(5, addr, 40*COIN, 15, 25),
            scriptBuilder(TicketContribData(5, addr, 40*COIN, 15, 25)),
            TicketContribData(5, addr, 40*COIN, 15, 25)
        },
        {
            TicketContribData(6, addr, 100*COIN, 40, 30),
            scriptBuilder(TicketContribData(6, addr, 100*COIN, 40, 30)),
            TicketContribData(6, addr, 100*COIN, 40, 30)
        },
        {
            TicketContribData(7, addr, 40*COIN, -1, MAX_MONEY+1),
            scriptBuilder(TicketContribData(7, addr, 40*COIN, 0, MAX_MONEY)),
            TicketContribData(7, addr, 40*COIN, 0, MAX_MONEY)
        },
        {
            TicketContribData(8, addr, 100*COIN, MAX_MONEY+1, -1),
            scriptBuilder(TicketContribData(8, addr, 100*COIN, MAX_MONEY, 0)),
            TicketContribData(8, addr, 100*COIN, MAX_MONEY, 0)
        }
    };

    CMutableTransaction mtx;
    for (unsigned i = 0; i < ticketContribOutputIndex; ++i)
        mtx.vout.push_back(CTxOut{1, coinbaseScriptPubKey}); // dummy output

    for (unsigned i = 0; i < tests.size(); ++i)  {
        BOOST_CHECK(GetScriptForTicketContrib(tests[i].tcd) == tests[i].expectedScript);

        mtx.vout.push_back(CTxOut{0, GetScriptForTicketContrib(tests[i].tcd)});

        mtx.vout.push_back(CTxOut{1, coinbaseScriptPubKey}); // dummy change
    }

    std::vector<TicketContribData> deserializedTcds;
    CAmount totalContribution;
    CAmount totalVoteFeeLimit;
    CAmount totalRevocationFeeLimit;

    BOOST_CHECK(ParseTicketContribs(mtx, deserializedTcds, totalContribution, totalVoteFeeLimit, totalRevocationFeeLimit));

    BOOST_CHECK(deserializedTcds.size() == tests.size());

    CAmount expectedTotalContribution{0};
    CAmount expectedTotalVoteFeeLimit{0};
    CAmount expectedTotalRevocationFeeLimit{0};
    for (unsigned i = 0; i < tests.size(); ++i) {
        BOOST_CHECK(deserializedTcds[i] == tests[i].expectedTcd);

        expectedTotalContribution += tests[i].expectedTcd.contributedAmount;
        expectedTotalVoteFeeLimit += tests[i].expectedTcd.voteFeeLimit();
        expectedTotalRevocationFeeLimit += tests[i].expectedTcd.revocationFeeLimit();
    }

    BOOST_CHECK(totalContribution == expectedTotalContribution);
    BOOST_CHECK(totalVoteFeeLimit == expectedTotalVoteFeeLimit);
    BOOST_CHECK(totalRevocationFeeLimit == expectedTotalRevocationFeeLimit);
}

// test the ticket purchase estimated sizes of inputs and outputs
BOOST_AUTO_TEST_CASE(ticket_purchase_estimated_sizes)
{
    BOOST_CHECK_EQUAL(GetEstimatedP2PKHTxInSize(), 148U);
    BOOST_CHECK_EQUAL(GetEstimatedP2PKHTxInSize(false), 180U);

    BOOST_CHECK_EQUAL(GetEstimatedP2PKHTxOutSize(), 34U);

    BOOST_CHECK_EQUAL(GetEstimatedBuyTicketDeclTxOutSize(), 16U);

    BOOST_CHECK_EQUAL(GetEstimatedTicketContribTxOutSize(), 64U);

    BOOST_CHECK_EQUAL(GetEstimatedSizeOfBuyTicketTx(true), 552U);
    BOOST_CHECK_EQUAL(GetEstimatedSizeOfBuyTicketTx(false), 306U);
}

// test the ticket ownership verification
BOOST_FIXTURE_TEST_CASE(is_my_ticket, WalletStakeTestingSetup)
{
    GetSomeCoins();

    LOCK2(cs_main, wallet->cs_wallet);

    // dummy transaction

    {
        CMutableTransaction mtx;
        mtx.vin.push_back(CTxIn(emptyData.hash, 0));
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx), false);
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx.GetHash()), false);
        mtx.vout.push_back(CTxOut(1000, CScript()));
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx), false);
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx.GetHash()), false);
    }

    // valid transaction (not owned output)

    {
        CKey key;
        key.MakeNewKey(true);

        CMutableTransaction mtx;
        mtx.vout.push_back(CTxOut(1 * COIN, GetScriptForDestination(key.GetPubKey().GetID())));
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx), false);
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx.GetHash()), false);

        CAmount feeRet;
        int changePos{-1};
        std::string reason;
        BOOST_CHECK_MESSAGE(wallet->FundTransaction(mtx, feeRet, changePos, reason, false, {}, CCoinControl()), ((reason.size() > 0) ? reason : "FundTransaction"));
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx), false);
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx.GetHash()), false);
    }

    // valid transaction (owned output)

    {
        CPubKey pubKey;
        wallet->GetKeyFromPool(pubKey);

        CMutableTransaction mtx;
        mtx.vout.push_back(CTxOut(1 * COIN, GetScriptForDestination(pubKey.GetID())));
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx), false);
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx.GetHash()), false);

        CAmount feeRet;
        int changePos{-1};
        std::string reason;
        BOOST_CHECK_MESSAGE(wallet->FundTransaction(mtx, feeRet, changePos, reason, false, {}, CCoinControl()), ((reason.size() > 0) ? reason : "FundTransaction"));
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx), false);
        BOOST_CHECK_EQUAL(wallet->IsMyTicket(mtx.GetHash()), false);
    }

    // valid ticket (not owned stake output)

    {
        CKey key;
        key.MakeNewKey(true);

        std::vector<std::string> txHashes;
        CWalletError we;

        std::tie(txHashes, we) = wallet->PurchaseTicket("", MAX_MONEY, 1, key.GetPubKey().GetID(), CNoDestination(), 1, CNoDestination(), 0.0, 0);
        BOOST_CHECK(we.code == CWalletError::SUCCESSFUL);
        BOOST_CHECK(txHashes.size() > 0);

        BOOST_CHECK_EQUAL(wallet->IsMyTicket(uint256S(txHashes[0])), false);
    }

    // valid ticket (generated reward address)

    {
        CPubKey pubKey;
        wallet->GetKeyFromPool(pubKey);

        std::vector<std::string> txHashes;
        CWalletError we;

        std::tie(txHashes, we) = wallet->PurchaseTicket("", MAX_MONEY, 1, pubKey.GetID(), CNoDestination(), 1, CNoDestination(), 0.0, 0);
        BOOST_CHECK(we.code == CWalletError::SUCCESSFUL);
        BOOST_CHECK(txHashes.size() > 0);

        BOOST_CHECK_EQUAL(wallet->IsMyTicket(uint256S(txHashes[0])), true);
    }

    // valid ticket (explicit reward address)

    {
        CPubKey pubKey;
        wallet->GetKeyFromPool(pubKey);

        CPubKey rewardPubKey;
        wallet->GetKeyFromPool(rewardPubKey);
        CKeyID rewardAddr = rewardPubKey.GetID();

        std::vector<std::string> txHashes;
        CWalletError we;

        std::tie(txHashes, we) = wallet->PurchaseTicket("", MAX_MONEY, 1, pubKey.GetID(), rewardAddr, 1, CNoDestination(), 0.0, 0);
        BOOST_CHECK(we.code == CWalletError::SUCCESSFUL);
        BOOST_CHECK(txHashes.size() > 0);

        BOOST_CHECK_EQUAL(wallet->IsMyTicket(uint256S(txHashes[0])), true);

        TicketContribData contrib;
        BOOST_CHECK(ParseTicketContrib(*wallet->GetWalletTx(uint256S(txHashes[0]))->tx, ticketContribOutputIndex, contrib));
        BOOST_CHECK(contrib.rewardAddr == rewardAddr);
        BOOST_CHECK(contrib.whichAddr == 1);
    }
}

// test the split transaction for funding a ticket purchase
BOOST_FIXTURE_TEST_CASE(ticket_purchase_split_transaction, WalletStakeTestingSetup)
{
    uint256 splitTxHash;
    CWalletError we;
    CAmount neededPerTicket{0};
    const CAmount ticketFee{10000};

    GetSomeCoins();

    LOCK2(cs_main, wallet->cs_wallet);

    // Coin availability

    std::tie(splitTxHash, we) = wallet.get()->CreateTicketPurchaseSplitTx("", 1000000000 * COIN, ticketFee);
    BOOST_CHECK_EQUAL(we.code, CWalletError::WALLET_INSUFFICIENT_FUNDS);

    std::tie(splitTxHash, we) = wallet.get()->CreateTicketPurchaseSplitTx("", 100000000 * COIN, ticketFee);
    BOOST_CHECK_EQUAL(we.code, CWalletError::WALLET_INSUFFICIENT_FUNDS);

    std::tie(splitTxHash, we) = wallet.get()->CreateTicketPurchaseSplitTx("", 10000000 * COIN, ticketFee);
    BOOST_CHECK_EQUAL(we.code, CWalletError::WALLET_INSUFFICIENT_FUNDS);

    // Single ticket

    std::tie(splitTxHash, we) = wallet.get()->CreateTicketPurchaseSplitTx("", 100 * COIN, ticketFee);
    BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

    const CWalletTx* splitTxSingle = wallet.get()->GetWalletTx(splitTxHash);
    BOOST_CHECK(splitTxSingle != nullptr);

    BOOST_CHECK_EQUAL(splitTxSingle->tx->vin.size(), 1U);

    BOOST_CHECK_EQUAL(splitTxSingle->tx->vout.size(), 2U); // ticket + change

    neededPerTicket = 100 * COIN + ticketFee; // ticket price + ticket fee

    BOOST_CHECK_EQUAL(splitTxSingle->tx->vout[0].nValue, neededPerTicket);

    // Multiple tickets

    std::tie(splitTxHash, we) = wallet.get()->CreateTicketPurchaseSplitTx("", 10 * COIN, ticketFee, 0, 10);
    BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

    const CWalletTx* splitTxMultiple = wallet.get()->GetWalletTx(splitTxHash);
    BOOST_CHECK(splitTxMultiple != nullptr);

    BOOST_CHECK_EQUAL(splitTxMultiple->tx->vout.size(), 10U + 1U); // tickets + change

    neededPerTicket = 10 * COIN + ticketFee; // ticket price + ticket fee

    for (size_t i = 0; i < 10; ++i)
        BOOST_CHECK_EQUAL(splitTxMultiple->tx->vout[i].nValue, neededPerTicket);

    // Use VSP (single ticket)

    std::tie(splitTxHash, we) = wallet.get()->CreateTicketPurchaseSplitTx("", 100 * COIN, ticketFee, 20 * COIN);
    BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

    const CWalletTx* splitTxVspSingle = wallet.get()->GetWalletTx(splitTxHash);
    BOOST_CHECK(splitTxVspSingle != nullptr);

    BOOST_CHECK_EQUAL(splitTxVspSingle->tx->vout.size(), 2U + 1U); // user + VSP + change

    neededPerTicket = 100 * COIN + ticketFee; // ticket price + ticket fee

    BOOST_CHECK_EQUAL(splitTxVspSingle->tx->vout[0].nValue, 20 * COIN); // VSP fee
    BOOST_CHECK_EQUAL(splitTxVspSingle->tx->vout[1].nValue, neededPerTicket - 20 * COIN); // user (needed per ticket - VSP)

    // Use VSP (multiple tickets)

    std::tie(splitTxHash, we) = wallet.get()->CreateTicketPurchaseSplitTx("", 10 * COIN, ticketFee, 5 * COIN, 10);
    BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

    const CWalletTx* splitTxVspMultiple = wallet.get()->GetWalletTx(splitTxHash);
    BOOST_CHECK(splitTxVspMultiple != nullptr);

    BOOST_CHECK_EQUAL(splitTxVspMultiple->tx->vout.size(), 10U * 2U + 1U); // users + VSPs + change

    neededPerTicket = 10 * COIN + ticketFee; // ticket price + ticket fee

    for (size_t i = 0; i < 10; ++i) {
        BOOST_CHECK_EQUAL(splitTxVspMultiple->tx->vout[2*i].nValue, 5 * COIN); // VSP fee
        BOOST_CHECK_EQUAL(splitTxVspMultiple->tx->vout[2*i+1].nValue, neededPerTicket - 5 * COIN); // user (needed per ticket - VSP)
    }

    // Fee rate

    std::tie(splitTxHash, we) = wallet.get()->CreateTicketPurchaseSplitTx("", 10 * COIN, ticketFee, 5 * COIN, 10, ticketFee);
    BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

    const CWalletTx* splitTxFeeRate = wallet.get()->GetWalletTx(splitTxHash);
    BOOST_CHECK(splitTxFeeRate != nullptr);

    CFeeRate feeRate{ticketFee};

    CAmount fee;
    std::string account;
    std::list<COutputEntry> received;
    std::list<COutputEntry> sent;
    splitTxFeeRate->GetAmounts(received, sent, fee, account, ISMINE_ALL);

    BOOST_CHECK_LE(feeRate.GetFee(static_cast<size_t>(GetVirtualTransactionSize(*(splitTxFeeRate->tx)))), fee);
}

// test the reordering of split transactions in a block
BOOST_FIXTURE_TEST_CASE(split_transaction_reordering, WalletStakeTestingSetup)
{
    const int count = 5;
    const CAmount ticketFee{10000};

    LOCK2(cs_main, wallet->cs_wallet);

    CPubKey splitTxPubKeys[count];
    CTxDestination splitTxKeyIds[count];
    CScript splitTxScriptPubKey[count];
    for (size_t i = 0; i < count; ++i) {
        BOOST_CHECK(wallet->GetKeyFromPool(splitTxPubKeys[i]));
        splitTxKeyIds[i] = splitTxPubKeys[i].GetID();
        splitTxScriptPubKey[i] = GetScriptForDestination(splitTxKeyIds[i]);
    }

    CPubKey ticketPubKeys[count];
    CTxDestination ticketKeyIds[count];
    CScript ticketScriptPubKey[count];
    for (size_t i = 0; i < count; ++i) {
        BOOST_CHECK(wallet->GetKeyFromPool(ticketPubKeys[i]));
        ticketKeyIds[i] = ticketPubKeys[i].GetID();
        ticketScriptPubKey[i] = GetScriptForDestination(ticketKeyIds[i]);
    }

    ExtendChain(consensus.nStakeEnabledHeight + 1 - chainActive.Height());

    CAmount ticketPrice = CalculateNextRequiredStakeDifficulty(chainActive.Tip(), consensus);
    CAmount neededPerTicket = ticketPrice + ticketFee;

    // create some interdependent split transactions
    // the first one has a large change, in order to cover the subsequent ones

    CMutableTransaction splitTxs[count];
    CWalletError we;
    for (int i = count-1; i >= 0 ; --i) {
        if (i == count-1) {
            CAmount feeRet{0};
            int changePos{1};
            std::string reason;

            splitTxs[i].vout.push_back(CTxOut(neededPerTicket + 5 * neededPerTicket, splitTxScriptPubKey[i]));

            BOOST_CHECK(wallet->FundTransaction(splitTxs[i], feeRet, changePos, reason, false, {}, CCoinControl()));

            splitTxs[i].vout[0].nValue = neededPerTicket;

            splitTxs[i].vout[1].nValue += 5 * neededPerTicket;
        } else {
            splitTxs[i].vin.push_back(CTxIn(splitTxs[i+1].GetHash(), 1));

            splitTxs[i].vout.push_back(CTxOut(neededPerTicket, splitTxScriptPubKey[i]));

            splitTxs[i].vout.push_back(CTxOut(splitTxs[i+1].vout[1].nValue - neededPerTicket, splitTxScriptPubKey[i]));

            BOOST_CHECK(wallet->SignTransaction(splitTxs[i]));

            CAmount fee = CFeeRate(10000).GetFee(GetVirtualTransactionSize(splitTxs[i]));
            splitTxs[i].vout[1].nValue -= fee;
        }

        BOOST_CHECK(wallet->SignTransaction(splitTxs[i]));

        CWalletTx wtx;
        CValidationState state;
        wtx.fTimeReceivedIsTxTime = true;
        wtx.BindWallet(wallet.get());
        wtx.SetTx(MakeTransactionRef(splitTxs[i]));
        CReserveKey reservekey{wallet.get()};
        BOOST_CHECK(wallet->CommitTransaction(wtx, reservekey, g_connman.get(), state));
    }

    // create the tickets

    CMutableTransaction tickets[count];
    for (size_t i = 0; i < count ; ++i) {
        tickets[i].vin.push_back(CTxIn(splitTxs[i].GetHash(), 0));

        tickets[i].vout.push_back(CTxOut(0, GetScriptForBuyTicketDecl(BuyTicketData{1})));

        tickets[i].vout.push_back(CTxOut(ticketPrice, GetScriptForDestination(ticketKeyIds[i])));

        tickets[i].vout.push_back(CTxOut(0, GetScriptForTicketContrib(TicketContribData{1, ticketKeyIds[i], neededPerTicket, 0, TicketContribData::DefaultFeeLimit})));

        tickets[i].vout.push_back(CTxOut(splitTxs[i].vout[0].nValue - neededPerTicket, GetScriptForDestination(ticketKeyIds[i])));

        BOOST_CHECK(wallet->SignTransaction(tickets[i]));

        CWalletTx wtx;
        CValidationState state;
        wtx.fTimeReceivedIsTxTime = true;
        wtx.BindWallet(wallet.get());
        wtx.SetTx(MakeTransactionRef(tickets[i]));
        CReserveKey reservekey{wallet.get()};
        BOOST_CHECK(wallet->CommitTransaction(wtx, reservekey, g_connman.get(), state));
    }

    // create a block template and mine it

    std::unique_ptr<CBlockTemplate> pblocktemplate;
    BOOST_CHECK_NO_THROW(pblocktemplate = BlockAssembler(params).CreateNewBlock(coinbaseScriptPubKey));
    BOOST_CHECK(pblocktemplate.get());

    CBlock& block = pblocktemplate->block;

    unsigned int extraNonce = 0;
    IncrementExtraNonce(&block, chainActive.Tip(), extraNonce);

    while (!CheckProofOfWork(block.GetHash(), block.nBits, block.nVersion, consensus)) ++block.nNonce;

    std::shared_ptr<const CBlock> shared_pblock = std::make_shared<const CBlock>(block);
    BOOST_CHECK(ProcessNewBlock(params, shared_pblock, true, nullptr));
}

// test the transaction for purchasing tickets
BOOST_FIXTURE_TEST_CASE(ticket_purchase_transaction, WalletStakeTestingSetup)
{
    std::vector<std::string> txHashes;
    CWalletError we;

    LOCK2(cs_main, wallet->cs_wallet);

    CPubKey ticketPubKey;
    BOOST_CHECK(wallet->GetKeyFromPool(ticketPubKey));
    CTxDestination ticketKeyId = ticketPubKey.GetID();

    CPubKey vspPubKey;
    BOOST_CHECK(wallet->GetKeyFromPool(vspPubKey));
    CTxDestination vspKeyId = vspPubKey.GetID();

    const CAmount spendLimit{100000 * COIN};
    const CAmount feeRate{10000};
    const double vspFeePercent{5.0};

    // validate a single ticket, with or without VSP fee
    auto checkTicket = [&](uint256 txHash, bool useVsp) {
        // Transaction

        BOOST_CHECK(txHash != emptyData.hash);

        const CWalletTx* wtx = wallet->GetWalletTx(txHash);
        BOOST_CHECK(wtx != nullptr);

        BOOST_CHECK(wtx->tx != nullptr);
        const CTransaction& tx = *(wtx->tx);

        // Inputs

        BOOST_CHECK_EQUAL(tx.vin.size(), useVsp ? 2U : 1U); // (vsp) + user

        // Outputs

        BOOST_CHECK_EQUAL(tx.vout.size(), useVsp ? 6U : 4U); // declaration + stake + (vsp commitment + vsp change +) user commitment + user change

        // check ticket declaration
        BOOST_CHECK_EQUAL(ParseTxClass(tx), TX_BuyTicket);
        BOOST_CHECK_EQUAL(tx.vout[0].nValue, 0);

        // make sure that the stake address and value are correct
        int64_t ticketPrice = CalculateNextRequiredStakeDifficulty(chainActive.Tip(), consensus);
        BOOST_CHECK(tx.vout[1].scriptPubKey == GetScriptForDestination(ticketKeyId));
        BOOST_CHECK_EQUAL(tx.vout[1].nValue, ticketPrice);

        // precalculate the necessary values
        size_t txSize = GetEstimatedSizeOfBuyTicketTx(useVsp);
        CFeeRate txFeeRate{feeRate};
        CAmount ticketFee{std::max(txFeeRate.GetFee(txSize), wallet->minTxFee.GetFee(txSize))};
        CAmount neededPerTicket = ticketPrice + ticketFee;
        CAmount vspFee = (useVsp ? StakePoolTicketFee(ticketPrice, ticketFee, chainActive.Height() + 1, vspFeePercent) : 0);
        uint32_t userOutput = 2 + (useVsp ? 2 : 0);

        // check VSP outputs, if present
        // make sure that the contributor address matches the VSP address, the contributed amount is the VSP fee
        // and that the change is zero
        if (useVsp) {
            TicketContribData tcd;
            BOOST_CHECK(ParseTicketContrib(tx, 2, tcd));
            BOOST_CHECK_EQUAL(tcd.nVersion, 1);
            BOOST_CHECK_EQUAL(tcd.contributedAmount, vspFee);
            BOOST_CHECK_EQUAL(tcd.voteFeeLimit(), 0);
            BOOST_CHECK_EQUAL(tcd.revocationFeeLimit(), TicketContribData::DefaultFeeLimit);
            BOOST_CHECK_EQUAL(tcd.whichAddr, 1);
            BOOST_CHECK(tcd.rewardAddr == boost::get<CKeyID>(vspKeyId));
            BOOST_CHECK_EQUAL(tx.vout[2].nValue, 0);

            BOOST_CHECK(tx.vout[3].scriptPubKey == GetScriptForDestination(vspKeyId));
            BOOST_CHECK_EQUAL(tx.vout[3].nValue, 0);
        }

        // check user outputs
        // make sure that the contributor address is owned by the wallet, the contributed amount reflects the correct value,
        // also considering the VSP fee if present and that the change is zero
        TicketContribData tcd;
        BOOST_CHECK(ParseTicketContrib(tx, userOutput, tcd));
        BOOST_CHECK_EQUAL(tcd.nVersion, 1);
        BOOST_CHECK_EQUAL(tcd.contributedAmount, neededPerTicket - vspFee);
        BOOST_CHECK_EQUAL(tcd.voteFeeLimit(), 0);
        BOOST_CHECK_EQUAL(tcd.revocationFeeLimit(), TicketContribData::DefaultFeeLimit);
        BOOST_CHECK_EQUAL(tcd.whichAddr, 1);
        BOOST_CHECK(wallet->IsMine(CTxOut(0, GetScriptForDestination(CKeyID(uint160(tcd.rewardAddr))))));
        BOOST_CHECK_EQUAL(tx.vout[userOutput].nValue, 0);

        BOOST_CHECK(wallet->IsMine(tx.vout[userOutput+1]));
        BOOST_CHECK_EQUAL(tx.vout[userOutput+1].nValue, 0);
    };

    // Coin availability
    {
        txHashes.clear();

        std::tie(txHashes, we) = wallet->PurchaseTicket("", 100000 * COIN, 1, ticketKeyId, CNoDestination(), 10, CNoDestination(), 0.0, 0, 1000 * COIN);
        BOOST_CHECK_EQUAL(we.code, CWalletError::WALLET_INSUFFICIENT_FUNDS);

        GetSomeCoins();

        std::tie(txHashes, we) = wallet->PurchaseTicket("", 100 * COIN, 1, ticketKeyId, CNoDestination(), 10000, CNoDestination(), 0.0, 0, feeRate);
        BOOST_CHECK_EQUAL(we.code, CWalletError::WALLET_ERROR);
    }

    ExtendChain(consensus.nStakeValidationHeight + 1 - chainActive.Height());

    // Single purchase, no VSP
    {
        txHashes.clear();

        std::tie(txHashes, we) = wallet->PurchaseTicket("", spendLimit, 1, ticketKeyId, CNoDestination(), 1, CNoDestination(), 0.0, 0, feeRate);
        BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

        BOOST_CHECK_EQUAL(txHashes.size(), 1U);

        uint256 txHash = uint256S(txHashes[0]);

        checkTicket(txHash, false);
    }

    // Single purchase, with VSP
    {
        txHashes.clear();

        std::tie(txHashes, we) = wallet->PurchaseTicket("", spendLimit, 1, ticketKeyId, CNoDestination(), 1, vspKeyId, vspFeePercent, 0, feeRate);
        BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

        BOOST_CHECK_EQUAL(txHashes.size(), 1U);

        uint256 txHash = uint256S(txHashes[0]);

        checkTicket(txHash, true);
    }

    // Multiple purchase, no VSP
    {
        txHashes.clear();

        unsigned int numTickets{10};

        std::tie(txHashes, we) = wallet->PurchaseTicket("", spendLimit, 1, ticketKeyId, CNoDestination(), numTickets, CNoDestination(), 0.0, 0, feeRate);
        BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

        BOOST_CHECK_EQUAL(txHashes.size(), static_cast<size_t>(numTickets));

        for (unsigned int i = 0; i < numTickets; ++i) {
            uint256 txHash = uint256S(txHashes[i]);
            checkTicket(txHash, false);
        }
    }

    // Multiple purchase, with VSP
    {
        txHashes.clear();

        unsigned int numTickets{10};

        std::tie(txHashes, we) = wallet->PurchaseTicket("", spendLimit, 1, ticketKeyId, CNoDestination(), numTickets, vspKeyId, vspFeePercent, 0, feeRate);
        BOOST_CHECK_EQUAL(we.code, CWalletError::SUCCESSFUL);

        BOOST_CHECK_EQUAL(txHashes.size(), static_cast<size_t>(numTickets));

        for (unsigned int i = 0; i < numTickets; ++i) {
            uint256 txHash = uint256S(txHashes[i]);
            checkTicket(txHash, true);
        }
    }
}

// test the ticket buyer
BOOST_FIXTURE_TEST_CASE(ticket_buyer, WalletStakeTestingSetup)
{
    CPubKey ticketPubKey;
    CPubKey vspPubKey;

    {
        LOCK(wallet->cs_wallet);
        BOOST_CHECK(wallet->GetKeyFromPool(ticketPubKey));
        BOOST_CHECK(wallet->GetKeyFromPool(vspPubKey));
    }

    CTxDestination ticketKeyId = ticketPubKey.GetID();

    CTxDestination vspKeyId = vspPubKey.GetID();

    ExtendChain(consensus.nStakeEnabledHeight + 1 - chainActive.Height());

    BOOST_CHECK_GE(chainActive.Tip()->nTx, 1U);
    BOOST_CHECK_GE(chainActive.Tip()->nFreshStake, 0U);

    CTicketBuyer* tb = wallet->GetTicketBuyer();
    BOOST_CHECK(tb != nullptr);

    BOOST_CHECK(!tb->isStarted());

    CTicketBuyerConfig& cfg = tb->GetConfig();

    BOOST_CHECK_EQUAL(cfg.buyTickets, false);

    cfg.account = "";
    cfg.votingAccount = "";
    cfg.votingAddress = ticketKeyId;
    cfg.poolFeeAddress = CNoDestination();
    cfg.poolFees = 0.0;
    cfg.limit = 1;
    cfg.passphrase = "";

    // Coin availability

    cfg.maintain = wallet->GetBalance() - 1000;

    tb->start();

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));

    ExtendChain(1);

    BOOST_CHECK_GE(chainActive.Tip()->nTx, 1U);
    BOOST_CHECK_GE(chainActive.Tip()->nFreshStake, 0U);

    // Single ticket, no VSP

    tb->stop();

    cfg.maintain = 0;

    tb->start();

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);

    BOOST_CHECK_GE(chainActive.Tip()->nTx, 1U + 1U + 1U);
    BOOST_CHECK_GE(chainActive.Tip()->nFreshStake, 1U);

    BOOST_CHECK_EQUAL(txsInMempoolBeforeLastBlock.size(), 2U);

    BOOST_CHECK_EQUAL(ParseTxClass(*txsInMempoolBeforeLastBlock[0].get()), TX_Regular);
    BOOST_CHECK_EQUAL(txsInMempoolBeforeLastBlock[0]->vout.size(), 1U + 1U);

    // TODO: Add amount validation
    CheckTicket(*txsInMempoolBeforeLastBlock[1].get(), {TicketContribData(1, ticketKeyId, 0, 0, TicketContribData::DefaultFeeLimit)});

    // Single ticket, VSP

    tb->stop();

    ExtendChain(consensus.nStakeValidationHeight + 1 - chainActive.Height());
    ExtendChain(5); // since no purchases are made prior to the interval switch,
                    // make sure that the tests do not overlap with this switch.

    cfg.poolFeeAddress = vspKeyId;
    cfg.poolFees = 5.0;

    tb->start();

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);

    BOOST_CHECK_GE(chainActive.Tip()->nTx, 1U + 1U + 1U);
    BOOST_CHECK_GE(chainActive.Tip()->nFreshStake, 1U);

    BOOST_CHECK_EQUAL(ParseTxClass(*txsInMempoolBeforeLastBlock[0].get()), TX_Regular);
    BOOST_CHECK_EQUAL(txsInMempoolBeforeLastBlock[0]->vout.size(), 1U + 1U + 1U);

    // TODO: Add amount validation
    CheckTicket(*txsInMempoolBeforeLastBlock[1].get(), {TicketContribData(1, vspKeyId, 0, 0, TicketContribData::DefaultFeeLimit), TicketContribData(1, ticketKeyId, 0, 0, TicketContribData::DefaultFeeLimit)});

    // Multiple tickets, no VSP

    tb->stop();

    cfg.poolFeeAddress = CNoDestination();
    cfg.poolFees = 0.0;
    cfg.limit = 5;

    tb->start();

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);

    BOOST_CHECK_GE(chainActive.Tip()->nTx, static_cast<unsigned int>(1 + 1 + cfg.limit));
    BOOST_CHECK_GE(chainActive.Tip()->nFreshStake, static_cast<uint8_t>(cfg.limit));

    BOOST_CHECK_EQUAL(ParseTxClass(*txsInMempoolBeforeLastBlock[0].get()), TX_Regular);
    BOOST_CHECK_EQUAL(txsInMempoolBeforeLastBlock[0]->vout.size(), static_cast<size_t>(cfg.limit + 1));

    // TODO: Add amount validation
    for (size_t i = 0; static_cast<int>(i) < cfg.limit; ++i)
        CheckTicket(*txsInMempoolBeforeLastBlock[1 + i].get(), {TicketContribData(1, ticketKeyId, 0, 0, TicketContribData::DefaultFeeLimit)});

    // Multiple ticket, VSP

    tb->stop();

    cfg.poolFeeAddress = vspKeyId;
    cfg.poolFees = 5.0;

    tb->start();

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);

    BOOST_CHECK_GE(chainActive.Tip()->nTx, static_cast<unsigned int>(1 + 1 + cfg.limit));
    BOOST_CHECK_GE(chainActive.Tip()->nFreshStake, static_cast<uint8_t>(cfg.limit));

    BOOST_CHECK_EQUAL(ParseTxClass(*txsInMempoolBeforeLastBlock[0].get()), TX_Regular);
    BOOST_CHECK_EQUAL(txsInMempoolBeforeLastBlock[0]->vout.size(), static_cast<size_t>(2 * cfg.limit + 1));

    // TODO: Add amount validation
    for (size_t i = 0; static_cast<int>(i) < cfg.limit; ++i)
        CheckTicket(*txsInMempoolBeforeLastBlock[1 + i].get(), {TicketContribData(1, vspKeyId, 0, 0, TicketContribData::DefaultFeeLimit), TicketContribData(1, ticketKeyId, 0, 0, TicketContribData::DefaultFeeLimit)});

    tb->stop();
}

// test the ticket buyer on encrypted wallet
BOOST_FIXTURE_TEST_CASE(ticket_buyer_encrypted, WalletStakeTestingSetup)
{
    CPubKey ticketPubKey;
    CPubKey vspPubKey;

    {
        LOCK(wallet->cs_wallet);
        BOOST_CHECK(wallet->GetKeyFromPool(ticketPubKey));
        BOOST_CHECK(wallet->GetKeyFromPool(vspPubKey));
        wallet->EncryptWallet(passphrase);
    }

    CTxDestination ticketKeyId = ticketPubKey.GetID();

    CTxDestination vspKeyId = vspPubKey.GetID();

    ExtendChain(consensus.nStakeValidationHeight + 1 - chainActive.Height() + 7); // since no purchases are made right before the interval switch,
                                                                                // make sure that the tests do not overlap with this switch.

    CTicketBuyer* tb = wallet->GetTicketBuyer();
    BOOST_CHECK(tb != nullptr);

    BOOST_CHECK(!tb->isStarted());

    CTicketBuyerConfig& cfg = tb->GetConfig();

    BOOST_CHECK_EQUAL(cfg.buyTickets, false);

    cfg.account = "";
    cfg.votingAccount = "";
    cfg.votingAddress = ticketKeyId;
    cfg.poolFeeAddress = vspKeyId;
    cfg.poolFees = 5.0;
    cfg.limit = 5;
    cfg.passphrase = passphrase;

    BOOST_CHECK(wallet->IsLocked());

    tb->start();

    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    ExtendChain(1);

    BOOST_CHECK_GE(chainActive.Tip()->nTx, static_cast<unsigned int>(1 + 1 + cfg.limit));
    BOOST_CHECK_GE(chainActive.Tip()->nFreshStake, static_cast<uint8_t>(cfg.limit));

    BOOST_CHECK_EQUAL(ParseTxClass(*txsInMempoolBeforeLastBlock[0].get()), TX_Regular);
    BOOST_CHECK_EQUAL(txsInMempoolBeforeLastBlock[0]->vout.size(), static_cast<size_t>(2 * cfg.limit + 1));

    // TODO: Add amount validation
    for (size_t i = 0; static_cast<int>(i) < cfg.limit; ++i)
        CheckTicket(*txsInMempoolBeforeLastBlock[1 + i].get(), {TicketContribData(1, vspKeyId, 0, 0, TicketContribData::DefaultFeeLimit), TicketContribData(1, ticketKeyId, 0, 0, TicketContribData::DefaultFeeLimit)});

    tb->stop();
}

// test the password validation from optional value
BOOST_FIXTURE_TEST_CASE(password_validation_from_optional_value, WalletStakeTestingSetup)
{
    LOCK2(cs_main, wallet->cs_wallet);

    // unencrypted wallet

    BOOST_CHECK_NO_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue()));
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(10)), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(1.001)), UniValue);
    BOOST_CHECK_NO_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue("")));
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue("aWr0ngP4$$word")), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(passphrase.c_str())), UniValue);
    BOOST_CHECK_NO_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(emptyData.string)));
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(std::string("aWr0ngP4$$word"))), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(std::string(passphrase.c_str()))), UniValue);

    // encrypted wallet (wrong password)

    wallet->EncryptWallet(passphrase);
    BOOST_CHECK(wallet->IsLocked());

    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue()), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(10)), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(1.001)), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue("")), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue("aWr0ngP4$$word")), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(emptyData.string)), UniValue);
    BOOST_CHECK_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(std::string("aWr0ngP4$$word"))), UniValue);

    // encrypted wallet (good password)

    BOOST_CHECK_NO_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(passphrase.c_str())));
    BOOST_CHECK_NO_THROW(ValidatedPasswordFromOptionalValue(wallet.get(), UniValue(std::string(passphrase.c_str()))));
}

// test the ticket buyer RPCs
BOOST_FIXTURE_TEST_CASE(ticket_buyer_rpc, WalletStakeTestingSetup)
{
    vpwallets.insert(vpwallets.begin(), wallet.get());

    RegisterWalletRPCCommands(tableRPC);

    CPubKey ticketPubKey;
    BOOST_CHECK(wallet->GetKeyFromPool(ticketPubKey));
    CTxDestination ticketKeyId = ticketPubKey.GetID();
    std::string ticketAddress{EncodeDestination(ticketKeyId)};

    CPubKey rewardPubKey;
    BOOST_CHECK(wallet->GetKeyFromPool(rewardPubKey));
    CTxDestination rewardKeyId = rewardPubKey.GetID();
    std::string rewardAddress{EncodeDestination(rewardKeyId)};

    CPubKey vspPubKey;
    BOOST_CHECK(wallet->GetKeyFromPool(vspPubKey));
    CTxDestination vspKeyId = vspPubKey.GetID();
    std::string vspAddress{EncodeDestination(vspKeyId)};

    CTicketBuyer* tb = wallet->GetTicketBuyer();
    BOOST_CHECK(tb != nullptr);

    BOOST_CHECK(!tb->isStarted());

    CTicketBuyerConfig& cfg = tb->GetConfig();

    // Settings (write)

    BOOST_CHECK_THROW(CallRpc("setticketbuyeraccount"), std::runtime_error);
    BOOST_CHECK_NO_THROW(CallRpc("setticketbuyeraccount abc"));

    BOOST_CHECK_THROW(CallRpc("setticketbuyerbalancetomaintain"), std::runtime_error);
    BOOST_CHECK_NO_THROW(CallRpc("setticketbuyerbalancetomaintain 123"));

    BOOST_CHECK_THROW(CallRpc("setticketbuyervotingaddress"), std::runtime_error);
    BOOST_CHECK_NO_THROW(CallRpc("setticketbuyervotingaddress " + ticketAddress));

    BOOST_CHECK_THROW(CallRpc("setticketbuyerrewardaddress"), std::runtime_error);
    BOOST_CHECK_NO_THROW(CallRpc("setticketbuyerrewardaddress " + rewardAddress));

    BOOST_CHECK_THROW(CallRpc("setticketbuyerpooladdress"), std::runtime_error);
    BOOST_CHECK_NO_THROW(CallRpc("setticketbuyerpooladdress " + vspAddress));

    BOOST_CHECK_THROW(CallRpc("setticketbuyerpoolfees"), std::runtime_error);
    BOOST_CHECK_NO_THROW(CallRpc("setticketbuyerpoolfees 5.0"));

    BOOST_CHECK_THROW(CallRpc("setticketbuyermaxperblock"), std::runtime_error);
    BOOST_CHECK_NO_THROW(CallRpc("setticketbuyermaxperblock 5"));

    BOOST_CHECK_EQUAL(cfg.buyTickets, false);
    BOOST_CHECK_EQUAL(cfg.account, "abc");
    BOOST_CHECK_EQUAL(cfg.maintain, 12300000000);
    BOOST_CHECK_EQUAL(cfg.votingAccount, "");
    BOOST_CHECK(cfg.votingAddress == ticketKeyId);
    BOOST_CHECK(cfg.poolFeeAddress == vspKeyId);
    BOOST_CHECK_EQUAL(cfg.poolFees, 5.0);
    BOOST_CHECK_EQUAL(cfg.limit, 5);

    // Settings (read)

    UniValue r;
    BOOST_CHECK_NO_THROW(r = CallRpc("ticketbuyerconfig"));
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "buytickets").get_bool(), false);
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "account").get_str(), "abc");
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "maintain").get_int64(), 12300000000);
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "votingAccount").get_str(), "");
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "votingAddress").get_str(), ticketAddress);
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "rewardAddress").get_str(), rewardAddress);
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "poolFeeAddress").get_str(), vspAddress);
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "poolFees").get_real(), 5.0);
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "limit").get_int(), 5);
    BOOST_CHECK_EQUAL(find_value(r.get_obj(), "minConf").get_int(), 1);

    // Start (with minimal settings)

    BOOST_CHECK_THROW(CallRpc("startticketbuyer"), std::runtime_error);
    BOOST_CHECK_THROW(CallRpc("startticketbuyer \"\""), std::runtime_error);
    BOOST_CHECK_NO_THROW(CallRpc("startticketbuyer def 124"));

    BOOST_CHECK_EQUAL(cfg.buyTickets, true);
    BOOST_CHECK_EQUAL(cfg.account, "def");
    BOOST_CHECK_EQUAL(cfg.maintain, 12400000000);
    BOOST_CHECK_EQUAL(cfg.votingAccount, "");
    BOOST_CHECK(cfg.votingAddress.which() == 0);
    BOOST_CHECK(cfg.poolFeeAddress.which() == 0);
    BOOST_CHECK_EQUAL(cfg.poolFees, 0.0);
    BOOST_CHECK_EQUAL(cfg.limit, 1);

    // Start (with full settings)

    tb->stop();

    BOOST_CHECK_EQUAL(cfg.buyTickets, false);

    BOOST_CHECK_NO_THROW(CallRpc(std::string("startticketbuyer fromaccount 125 \"\" votingaccount ") + ticketAddress + " " + rewardAddress + " " + vspAddress + " 10.0 8"));

    BOOST_CHECK_EQUAL(cfg.buyTickets, true);
    BOOST_CHECK_EQUAL(cfg.account, "fromaccount");
    BOOST_CHECK_EQUAL(cfg.votingAccount, "votingaccount");
    BOOST_CHECK_EQUAL(cfg.maintain, 12500000000);
    BOOST_CHECK(cfg.votingAddress == ticketKeyId);
    BOOST_CHECK(cfg.poolFeeAddress == vspKeyId);
    BOOST_CHECK_EQUAL(cfg.poolFees, 10.0);
    BOOST_CHECK_EQUAL(cfg.limit, 8);

    // Stop

    BOOST_CHECK_NO_THROW(CallRpc("stopticketbuyer"));

    BOOST_CHECK_EQUAL(cfg.buyTickets, false);

    // Start with passphrase

    tb->stop();

    BOOST_CHECK_EQUAL(cfg.buyTickets, false);

    wallet->EncryptWallet(passphrase);
    BOOST_CHECK(wallet->IsLocked());

    BOOST_CHECK_THROW(CallRpc("startticketbuyer def 124"), std::runtime_error);
    BOOST_CHECK_THROW(CallRpc("startticketbuyer def 124 wrongP4ssword!"), std::runtime_error);
    BOOST_CHECK_THROW(CallRpc("startticketbuyer def 124 wrongP4ssword votingaccount " + ticketAddress + " " + rewardAddress + " " + vspAddress + " 10.0 8"), std::runtime_error);

    BOOST_CHECK_NO_THROW(CallRpc(std::string("startticketbuyer fromaccount 125 ") + std::string(passphrase.c_str()) + " votingaccount " + ticketAddress + " " + rewardAddress + " " + vspAddress + " 10.0 8"));

    BOOST_CHECK_EQUAL(cfg.buyTickets, true);
    BOOST_CHECK_EQUAL(cfg.account, "fromaccount");
    BOOST_CHECK_EQUAL(cfg.votingAccount, "votingaccount");
    BOOST_CHECK_EQUAL(cfg.maintain, 12500000000);
    BOOST_CHECK(cfg.votingAddress == ticketKeyId);
    BOOST_CHECK(cfg.poolFeeAddress == vspKeyId);
    BOOST_CHECK_EQUAL(cfg.poolFees, 10.0);
    BOOST_CHECK_EQUAL(cfg.limit, 8);
    BOOST_CHECK_EQUAL(cfg.passphrase, passphrase);

    tb->stop();

    vpwallets.erase(std::remove(vpwallets.begin(), vpwallets.end(), wallet.get()), vpwallets.end());
}

BOOST_AUTO_TEST_SUITE_END()


//---------------------------------------------------------------------------//
//!
//! \file   tstBarnUnit.cpp
//! \author Alex Robinson
//! \brief  The barn unit unit tests
//!
//---------------------------------------------------------------------------//

// Std Lib Includes
#include <iostream>

// Boost Includes
#define BOOST_TEST_MAIN
#include <boost/test/unit_test.hpp>
#include <boost/test/tools/floating_point_comparison.hpp>
#include <boost/units/quantity.hpp>
#include <boost/units/systems/cgs/area.hpp>
#include <boost/units/systems/si/area.hpp>

// FRENSIE Includes
#include "Utility_BarnUnit.hpp"

using namespace Utility::Units;
namespace si = boost::units::si;
namespace cgs = boost::units::cgs;

//---------------------------------------------------------------------------//
// Tests
//---------------------------------------------------------------------------//
// Check that the barn unit can be initialized
BOOST_AUTO_TEST_CASE( initialize )
{
  boost::units::quantity<Utility::Units::Barn> barn_area( 1.0*barn );

  BOOST_CHECK_EQUAL( barn_area.value(), 1.0 );

  barn_area = 2.0*barn;

  BOOST_CHECK_EQUAL( barn_area.value(), 2.0 );
}

//---------------------------------------------------------------------------//
// Check that the barn unit can be initialized from cgs and si units
BOOST_AUTO_TEST_CASE( initialize_from_cgs_si )
{
  boost::units::quantity<Utility::Units::Barn> barn_area_cgs( 1.0*cgs::square_centimeter );

  boost::units::quantity<Utility::Units::Barn> barn_area_si( 1.0*si::square_meter );

  BOOST_CHECK_EQUAL( barn_area_cgs.value(), 1e24 );
  BOOST_CHECK_EQUAL( barn_area_si.value(), 1e28 );
}

//---------------------------------------------------------------------------//
// Check that the cgs and si units can be initialized from the barn unit
BOOST_AUTO_TEST_CASE( initialize_cgs_si )
{
  boost::units::quantity<cgs::area> cgs_area( 1.0*barn );

  boost::units::quantity<si::area> si_area( 1.0*barn );

  BOOST_CHECK_EQUAL( cgs_area.value(), 1e-24 );
  BOOST_CHECK_EQUAL( si_area.value(), 1e-28 );
}

//---------------------------------------------------------------------------//
// end tstBarnUnit.cpp
//---------------------------------------------------------------------------//


// Boost.Geometry (aka GGL, Generic Geometry Library)
// Unit Test

// Copyright (c) 2007-2011 Barend Gehrels, Amsterdam, the Netherlands.
// Use, modification and distribution is subject to the Boost Software License,
// Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

#ifndef BOOST_GEOMETRY_TEST_EXPAND_HPP
#define BOOST_GEOMETRY_TEST_EXPAND_HPP

#include <geometry_test_common.hpp>

#include <boost/geometry/algorithms/expand.hpp>
#include <boost/geometry/strategies/strategies.hpp>
#include <boost/geometry/domains/gis/io/wkt/read_wkt.hpp>
#include <boost/geometry/algorithms/assign.hpp>
#include <boost/geometry/util/write_dsv.hpp>

template <typename Geometry, typename Box>
void test_expand(Box& box,
                  std::string const& wkt,
                  std::string const& expected)
{
    Geometry geometry;
    bg::read_wkt(wkt, geometry);

    bg::expand(box, geometry);

    std::ostringstream out;
    out << bg::dsv(box, ",", "(", ")", ",", "", "");

    BOOST_CHECK_EQUAL(out.str(), expected);
}

template <typename Geometry, typename Box>
void test_expand_other_strategy(Box& box,
                  std::string const& wkt,
                  std::string const& expected)
{
    Geometry geometry;
    bg::read_wkt(wkt, geometry);

    bg::expand(box, geometry);

    std::ostringstream out;
    out << bg::dsv(box, ",", "(", ")", ",", "", "");

    BOOST_CHECK_EQUAL(out.str(), expected);
}

#endif


#include "Video.h"

#include "Utils/VideoUtils.h"

#include <cassert>
#include <boost/log/trivial.hpp>
#include "VideoFrameAccessor.h"

VideoFrameAccessor::VideoFrameAccessor() {

}

VideoFrameAccessor::VideoFrameAccessor(Video& video) : video(video) {

}

void VideoFrameAccessor::SetVideoSource(Video& video) {
	this->video = video;
}

mv::FrameAccessor::Key VideoFrameAccessor::GetImage(int clip,
	int frame, // Frame idx
	InputMode input_mode,
	int downscale,               // Downscale by 2^downscale.
	const mv::Region* region,    // Get full image if NULL.
	const Transform* transform,  // May be NULL.
	mv::FloatImage* destination) {

	// TODO: Check the image coordinates of FloatImage
	// opencv mat it is top left corner is 0/0
	// floatimage is probably (assumption) bottom left

	// According to Natron LibMv only uses image mono input
	if (input_mode != InputMode::MONO) {
		BOOST_LOG_TRIVIAL(warning) << "input mode is not mono";
	}

	// When region is nullptr then use the whole image else crop image

	cv::Mat cropImg;
	if (region) {
		cv::Rect2d roi;
		roi.x      = region->min(0);
		roi.y      = region->min(1);
		roi.width  = region->max(0) - region->min(0);
		roi.height = region->max(1) - region->min(1);

		cv::Mat frameMat = this->video.GetFrameByIdx(frame);
		cropImg = frameMat(roi); // Crop image by given region

	} else {
		// Get full image
		cropImg = this->video.GetFrameByIdx(frame);
	}

	std::vector<float> floatRect;
	floatRect.reserve(cropImg.rows * cropImg.cols);

	for (int y = 0; y < cropImg.rows; y++)
	{
		for (int x = 0; x < cropImg.cols; x++)
		{
			cv::Vec3b pixel = cropImg.at<cv::Vec3b>(x, y);

			// Convert to float value
			float r = static_cast<float>(pixel.val[0])  * (1.0f / 255.0f);
			float g = static_cast<float>(pixel.val[1])  * (1.0f / 255.0f);
			float b = static_cast<float>(pixel.val[2])  * (1.0f / 255.0f);

			// Convert to gray scale and add to float vector
			floatRect.push_back(
				0.2126f * r +
				0.7152f * g +
				0.0722f * b);

		}
	}

	mv::FloatImage mvFinalImage(
		floatRect.data(),
		cropImg.size().width,
		cropImg.size().height);

	destination->CopyFrom(mvFinalImage);

	return (mv::FrameAccessor::Key)&mvFinalImage;

	// ----------------------------------

	// Convert every pixel to grayscale
	cv::Mat grayCroppedImg;
	{
		cv::cvtColor(cropImg, grayCroppedImg, cv::COLOR_RGB2GRAY);
		grayCroppedImg.convertTo(grayCroppedImg, CV_32FC1);

		if (downscale > 0) {
			cv::Size newSize(
				grayCroppedImg.size().width  / (1 << downscale),
				grayCroppedImg.size().height / (1 << downscale)
			);
			cv::resize(grayCroppedImg,
				grayCroppedImg,
				newSize,
				0,
				0,
				cv::INTER_AREA
			);
		}
	}

	mv::FloatImage mvImage(
		grayCroppedImg.size().width,
		grayCroppedImg.size().height);

	float* mvImageDataPtr = mvImage.Data();

	for (int y = 0; y < grayCroppedImg.cols; y++) {
		for (int x = 0; x < grayCroppedImg.rows; x++) {
			mvImage.Fill(grayCroppedImg.at<float>(x, y));
		}
	}

	//*destination = mvImage;
	destination->CopyFrom(mvImage);

	return (mv::FrameAccessor::Key)&mvImage;
};

void VideoFrameAccessor::ReleaseImage(Key) {

};

mv::FrameAccessor::Key VideoFrameAccessor::GetMaskForTrack(int clip,
	int frame,
	int track,
	const mv::Region* region,
	mv::FloatImage* destination) {

	return nullptr;
};

void VideoFrameAccessor::ReleaseMask(Key key) {

};

bool VideoFrameAccessor::GetClipDimensions(int clip, int* width, int* height) {

	*width = this->video.GetVideoDim().width;
	*height = this->video.GetVideoDim().height;

	return this->video.HasFrames();
};

int VideoFrameAccessor::NumClips() {
	return 1;
};

int VideoFrameAccessor::NumFrames(int clip) {
	return this->video.GetFrameCount();
};


//==============================================================================
//         Copyright 2009 - 2013 LRI    UMR 8623 CNRS/Univ Paris Sud XI
//
//          Distributed under the Boost Software License, Version 1.0.
//                 See accompanying file LICENSE.txt or copy at
//                     http://www.boost.org/LICENSE_1_0.txt
//==============================================================================
#ifndef BOOST_SIMD_SWAR_FUNCTIONS_SIMD_SSE_AVX_SPLIT_HIGH_HPP_INCLUDED
#define BOOST_SIMD_SWAR_FUNCTIONS_SIMD_SSE_AVX_SPLIT_HIGH_HPP_INCLUDED
#ifdef BOOST_SIMD_HAS_AVX_SUPPORT

#include <boost/simd/swar/functions/split_high.hpp>
#include <boost/simd/include/functions/simd/combine.hpp>
#include <boost/simd/include/functions/simd/split.hpp>
#include <boost/dispatch/meta/upgrade.hpp>

namespace boost { namespace simd { namespace ext
{
  BOOST_DISPATCH_IMPLEMENT          ( split_high_
                                    , boost::simd::tag::avx_
                                    , (A0)
                                    , ((simd_<single_<A0>,boost::simd::tag::avx_>))
                                    )
  {
    typedef typename dispatch::meta::upgrade<A0>::type  result_type;

    BOOST_FORCEINLINE result_type operator()(const A0& a0) const
    {
      return _mm256_cvtps_pd(_mm256_extractf128_ps(a0, 1));
    }
  };

  BOOST_DISPATCH_IMPLEMENT          ( split_high_
                                    , boost::simd::tag::avx_
                                    , (A0)
                                    , ((simd_<integer_<A0>,boost::simd::tag::avx_>))
                                    )
  {
    typedef typename dispatch::meta::upgrade<A0>::type  result_type;

    BOOST_FORCEINLINE result_type operator()(const A0& a0) const
    {
      typedef typename meta::retarget<A0,tag::sse_>::type ext_t;
      typedef typename meta::retarget<result_type,tag::sse_>::type part_t;

      ext_t  e = _mm256_extractf128_si256(a0, 1);
      part_t ha1;
      part_t ha0 = split(e, ha1);

      return combine(ha0,ha1);
    }
  };
} } }

#endif
#endif