#ifndef TRACKER_HPP
#define TRACKER_HPP

#include <iostream>
#include <string>
#include <functional>

#include <ros/ros.h>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <cmath>
#include <numeric>// std::iota
#include <algorithm>// std::sort, std::stable_sort
#include <boost/range/combine.hpp>
#include <opencv2/opencv.hpp>

namespace mood_tracker {

template<typename T> std::vector<std::size_t> argsort(const std::vector<T>& v)
{
  std::vector<std::size_t> idx(v.size());
  iota(idx.begin(), idx.end(), 0);
  stable_sort(idx.begin(), idx.end(), [&v](std::size_t i1, std::size_t i2) {
    return v[i1] < v[i2];
  });
  return idx;
}

template<class T>
std::ostream& operator<<(std::ostream& stream, const std::pair<T, T>& pair)
{
  stream << " (" << pair.first << ", " << pair.second << ") ";
  return stream;
}

template<class T>
std::ostream& operator<<(std::ostream& stream, const std::vector<T>& values)
{
  stream << "[ ";
  for (const auto& element : values) { stream << element << " "; }
  stream << " ]";
  return stream;
}

/**
 * @brief This class is used for assigning ID's to given centroid estimates of detected
 * objects. The ID's are updated with respect to their mutual distance.
 * NOTE: Make sure that the centroid_t type has an appropriate operator<< overload.
 *
 * @tparam centroid_t Type of object position
 */
template<typename centroid_t> class DetectionTracker
{

public:
  using centroidMap_t = std::unordered_map<int /* Centroid ID */, centroid_t>;
  using distMatrix_t  = std::vector<std::vector<double>>;
  using distanceFcn_t = std::function<double(const centroid_t&, const centroid_t&)>;
  static constexpr auto INVALID_ID = -1;

  /**
   * @brief Construct a new Detection Tracker object.
   *
   * @param maxFrames Max frames for which the centroid is kept if object is not detected.
   * @param distance_function Distance function which computes distance between 2
   * centroid_t objects and returns distance of type double
   */
  DetectionTracker(int maxFrames, distanceFcn_t distance_function)
    : m_distance_function(std::move(distance_function)), m_maxDisappearedFrames(maxFrames)
  {}

  /**
   * @brief Get the Map object containing the tracking information.
   *
   * @return const centroidMap_t&
   */
  const centroidMap_t& getMap() { return m_centroidMap; }

  /**
   * @brief Check if given ID exists in the internal ID - centroid map.
   *
   * @param ID Given object ID.
   * @return true if the objects with given ID exists.
   * @return false if the object with given ID does not exist.
   */
  bool IDExists(const int ID) { return m_centroidMap.find(ID) != m_centroidMap.end(); }

  /**
   * @brief Return the number of frames the main update was called and object with given
   * ID was not found.
   *
   * @param id Given object ID.
   * @return int Number of disappeared frames
   */
  int getDisappearedCount(const int id)
  {
    const auto it = m_disappearedCentroidMap.find(id);
    if (it == m_disappearedCentroidMap.end()) { return -1; }
    return it->second;
  }

  std::tuple<bool, std::string> nextTrackingID()
  {
    if (m_centroidMap.size() < 2) { return { false, "Not enough centroid entries." }; }

    auto current_it = m_centroidMap.find(m_currentlyTrackedID);
    auto next_it    = std::next(current_it);

    if (next_it == m_centroidMap.end()) {
      m_currentlyTrackedID = m_centroidMap.begin()->first;
    } else {
      m_currentlyTrackedID = next_it->first;
    }

    return { true, "Tracking switched to ID: " + m_currentlyTrackedID };
  }

  /**
   * @brief This method should be called every time a new vector of detected object
   * centroids is obtained. It updates all internal structures and assigns appropriate
   * ID's to given objects.
   *
   * @param t_inputCentroids A vector of detected object centroids
   * @return std::tuple<bool, centroid_t> Returns the currently tracked centroid with the
   * success bool
   */
  std::tuple<bool, centroid_t> updateAllCentroids(
    const std::vector<centroid_t>& t_inputCentroids)
  {
    ROS_INFO_THROTTLE(THROTTLE_DURATION, "CentroidTracker - update called");
    ROS_DEBUG_STREAM("Input centroids: " << t_inputCentroids);
    ROS_DEBUG_STREAM_COND(!m_centroidMap.empty(), "Current centroids: ");
    for (auto& centroid : m_centroidMap) ROS_DEBUG_STREAM(centroid.second);

    if (t_inputCentroids.empty()) {
      // No new centroids need updating
      case_handle_empty_centroid_input();
      ROS_WARN_THROTTLE(THROTTLE_DURATION,
                        "CentroidTracker - input vector is empty, returning -1");
      return { false, centroid_t{} };
    }

    if (m_centroidMap.empty()) {
      // If there are no objects stored, store all new centroids
      ROS_WARN_THROTTLE(THROTTLE_DURATION,
                        "CentroidTracker - no objects currently stored");
      for (const auto& newCentroid : t_inputCentroids) { register_centroid(newCentroid); }
    } else {
      case_match_input_to_centroids(t_inputCentroids);
    }

    update_currently_tracked_ID();
    if (m_currentlyTrackedID == -1) { return { false, centroid_t{} }; }
    return { true, m_centroidMap[m_currentlyTrackedID] };
  }

private:
  void update_currently_tracked_ID()
  {
    if (m_centroidMap.empty()) {
      m_currentlyTrackedID = -1;
      return;
    }

    if (m_currentlyTrackedID == -1
        || m_centroidMap.find(m_currentlyTrackedID) == m_centroidMap.end()) {
      m_currentlyTrackedID = m_centroidMap.begin()->first;
    }
  }

  void case_match_input_to_centroids(const std::vector<centroid_t>& t_inputCentroids)
  {
    ROS_INFO_THROTTLE(THROTTLE_DURATION,
                      "CantroidTracker::case_match_input_to_centroids");
    std::vector<int /* IDs */> centroidIDs;
    std::vector<centroid_t>    centroidValues;
    for (auto& centroid : m_centroidMap) {
      centroidIDs.push_back(centroid.first);
      centroidValues.push_back(centroid.second);
    }

    // Compute distances between all pairs of existing and input centroids
    ROS_DEBUG("D: ");
    int          D_rowCount = centroidValues.size(), D_colCount = t_inputCentroids.size();
    distMatrix_t D(D_rowCount, std::vector<double>(D_colCount, 0.0));
    for (std::size_t i = 0; i < centroidValues.size(); i++) {
      for (std::size_t j = 0; j < t_inputCentroids.size(); j++) {
        D[i][j] = m_distance_function(centroidValues[i], t_inputCentroids[j]);
      }
      ROS_DEBUG_STREAM(D[i]);
    }

    // Do argsort on all rows
    std::vector<double> closestDistanceInRow;
    for (const auto& distanceRow : D) {
      closestDistanceInRow.push_back(
        *std::min_element(distanceRow.begin(), distanceRow.end()));
    }
    // Sorted current centroid indices wrt. the distance to the closes input centroid
    // e.g. the first element in this vector will have a closest input centroid
    std::vector<std::size_t> rows = argsort(closestDistanceInRow);
    ROS_DEBUG_STREAM("Min of each row: " << closestDistanceInRow);
    ROS_DEBUG_STREAM("^argsorted: " << rows);

    // Calculated list of indices of minimum values in distance matrix
    std::vector<std::size_t> minRowDistanceIndices;
    for (const auto& distanceRow : D) {
      minRowDistanceIndices.push_back(std::size_t(
        std::min_element(distanceRow.begin(), distanceRow.end()) - distanceRow.begin()));
    }
    ROS_DEBUG_STREAM("Argmin of each row: " << minRowDistanceIndices);

    // Sort minimum distance indices in each row according to the sorted distance indices
    // vector
    std::vector<std::size_t> cols;
    for (const auto& index : rows) { cols.push_back(minRowDistanceIndices[index]); }
    ROS_DEBUG_STREAM("Sorted wrt. the argsort(min of each row): " << cols);

    std::set<std::size_t /* index */> usedRowIndices, usedColumnIndices;
    for (auto item : boost::combine(rows, cols)) {
      std::size_t row, col;
      boost::tie(row, col) = item;

      if (usedRowIndices.find(row) != usedRowIndices.end()
          || usedColumnIndices.find(col) != usedColumnIndices.end()) {
        continue;
      }

      int centroidID = centroidIDs[row];
      ROS_WARN_STREAM_THROTTLE(
        THROTTLE_DURATION, m_centroidMap[centroidID] << " -> " << t_inputCentroids[col]);
      m_centroidMap[centroidID]            = t_inputCentroids[col];
      m_disappearedCentroidMap[centroidID] = 0;

      usedRowIndices.insert(row);
      usedColumnIndices.insert(col);
    }

    std::vector<std::size_t> rowIDs(D_rowCount), colIDs(D_colCount), unusedRowIDs,
      unusedColIDs;
    std::iota(rowIDs.begin(), rowIDs.end(), 0);
    std::iota(colIDs.begin(), colIDs.end(), 0);
    std::set_difference(rowIDs.begin(),
                        rowIDs.end(),
                        usedRowIndices.begin(),
                        usedRowIndices.end(),
                        std::inserter(unusedRowIDs, unusedRowIDs.begin()));
    std::set_difference(colIDs.begin(),
                        colIDs.end(),
                        usedColumnIndices.begin(),
                        usedColumnIndices.end(),
                        std::inserter(unusedColIDs, unusedColIDs.begin()));

    // if there is more inputs than centroids
    if (D_rowCount < D_colCount) {
      for (const auto& col : unusedColIDs) { register_centroid(t_inputCentroids[col]); }
    }

    // Go through all the unused IDs and remove increase the dissapearance map
    for (const auto& row : unusedRowIDs) {
      std::size_t centroidID = centroidIDs[row];
      m_disappearedCentroidMap[centroidID]++;
      ROS_WARN_STREAM_THROTTLE(
        THROTTLE_DURATION,
        "Centroid ID: " << centroidID << " - " << m_centroidMap[centroidID]
                        << "  unseen for " << m_disappearedCentroidMap[centroidID]
                        << " frames");

      if (m_disappearedCentroidMap[centroidID] > m_maxDisappearedFrames) {
        deregister_centroid_id(centroidID);
      }
    }
  }

  void case_handle_empty_centroid_input()
  {
    ROS_WARN_THROTTLE(THROTTLE_DURATION,
                      "CantroidTracker::case_handle_empty_centroid_input");
    // Add all the objects to disappearance map, if they're not there
    for (auto& objCentroid : m_centroidMap) {
      m_disappearedCentroidMap.emplace(objCentroid.first, 0);
    }

    // Make objects disappear
    std::vector<std::size_t> IDsToRemove;
    for (auto& map_entry : m_disappearedCentroidMap) {

      // Increase dissapearance count
      map_entry.second++;
      auto& centroid_index     = map_entry.first;
      auto& disappeared_frames = map_entry.second;
      ROS_WARN_STREAM_THROTTLE(THROTTLE_DURATION, m_centroidMap.size());
      ROS_WARN_STREAM_THROTTLE(THROTTLE_DURATION,
                               "Object: " << m_centroidMap[centroid_index]
                                          << " missing for " << disappeared_frames
                                          << " frames");

      // Check to remove the centroid
      if (disappeared_frames > m_maxDisappearedFrames) {
        IDsToRemove.push_back(centroid_index);
      }
    }

    for (const auto& id : IDsToRemove) { deregister_centroid_id(id); }
  }

  static const centroid_t get_closest_centroid(const centroid_t&              centroid,
                                               const std::vector<centroid_t>& centroids)
  {
    double      minDist      = 1e10;
    std::size_t closestIndex = -1;
    for (std::size_t i = 0; i < centroids.size(); i++) {
      double dist = distance(centroid, centroids.at(i));
      if (dist < minDist) {
        closestIndex = i;
        minDist      = dist;
      }
    }
    return centroids.at(closestIndex);
  }

  void register_centroid(const centroid_t& newCentroid)
  {
    m_centroidMap[m_nextObjectID]            = newCentroid;
    m_disappearedCentroidMap[m_nextObjectID] = 0;
    ROS_INFO_STREAM_THROTTLE(THROTTLE_DURATION, "New object added! - " << newCentroid);
    m_nextObjectID++;
  }

  void deregister_centroid_id(const int objectID)
  {
    ROS_FATAL_STREAM_THROTTLE(
      THROTTLE_DURATION,
      "Removing object with ID: " << objectID << " - " << m_centroidMap[objectID]);
    m_centroidMap.erase(objectID);
    m_disappearedCentroidMap.erase(objectID);
  }

  static constexpr double THROTTLE_DURATION    = 3.0;
  int                     m_nextObjectID       = 0;
  int                     m_currentlyTrackedID = -1;
  const int               m_maxDisappearedFrames;
  centroidMap_t           m_centroidMap;
  std::unordered_map<int /* Centroid ID */, int /* Disappeared frame count */>
                m_disappearedCentroidMap;
  distanceFcn_t m_distance_function;
};
}// namespace mood_tracker

#endif /* TRACKER_HPP */


/* Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *  * Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *  * Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *  * Neither the name of NVIDIA CORPORATION nor the names of its
 *    contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include <boost/multiprecision/cpp_int.hpp>

#include "numeric.hpp"

using namespace boost::multiprecision;

//------------------------------------
//           Miscellaneous
//------------------------------------

// Returns the smallest factor of an integer and the quotient after
// division with the smallest factor.
void SmallestFactor(uint64_t n, uint64_t& factor, uint64_t& residue)
{
  for (uint64_t i = 2; i < n; i++)
  {
    if (n % i == 0)
    {
      factor = i;
      residue = n / i;
      return;
    }
  }
  factor = n;
  residue = 1;
}

// Helper function to get close-to-square layouts of arrays
// containing a given number of nodes.
void GetTiling(uint64_t num_elems, uint64_t& height, uint64_t& width)
{
  std::vector<uint64_t> factors;
  uint64_t residue = num_elems;
  uint64_t cur_factor;
  while (residue > 1)
  {
    SmallestFactor(residue, cur_factor, residue);
    factors.push_back(cur_factor);
  }

  height = 1;
  width = 1;
  for (uint64_t i = 0; i < factors.size(); i++)
  {
    if (i % 2 == 0)
      height *= factors[i];
    else
      width *= factors[i];
  }

  if (height > width)
  {
    uint64_t temp = height;
    height = width;
    width = temp;
  }
}

double LinearInterpolate(double x,
                         double x0, double x1,
                         double q0, double q1)
{
  double slope = (x0 == x1) ? 0 : (q1 - q0) / double(x1 - x0);
  return q0 + slope * (x - x0);
}

double BilinearInterpolate(double x, double y,
                           double x0, double x1,
                           double y0, double y1,
                           double q00, double q01, double q10, double q11)
{
  // Linear interpolate along x dimension.
  double qx0 = LinearInterpolate(x, x0, x1, q00, q10);
  double qx1 = LinearInterpolate(x, x0, x1, q01, q11);

  // Linear interpolate along y dimension.
  return LinearInterpolate(y, y0, y1, qx0, qx1);
}


/**TODO:  Add copyright*/

#define BOOST_TEST_MODULE TensorOperation1 test suite
#include <boost/test/included/unit_test.hpp>
#include <TensorBase/ml/TensorSelect.h>
#include <TensorBase/ml/TensorOperationDefaultDevice.h>
#include <TensorBase/ml/TensorCollectionDefaultDevice.h>

using namespace TensorBase;
using namespace std;

BOOST_AUTO_TEST_SUITE(TensorOperation1)

/// The select Functor for table 1
struct SelectTable1 {
  template<typename DeviceT>
  void operator() (std::shared_ptr<TensorCollection<DeviceT>>& tensor_collection, DeviceT& device) {
    // Set up the SelectClauses for table 1:  all values in dim 0 for labels = 0 in dims 1 and 2
    std::shared_ptr<TensorDataDefaultDevice<int, 1>> select_labels_t1a1 = std::make_shared<TensorDataDefaultDevice<int, 1>>(Eigen::array<Eigen::Index, 1>({ 2 }));
    Eigen::Tensor<int, 1> labels_values_t1a1(2);
    labels_values_t1a1.setValues({ 0, 1 });
    select_labels_t1a1->setData(labels_values_t1a1);
    SelectClause<int, DeviceT> select_clause1("1", "1", "x", select_labels_t1a1);
    std::shared_ptr<TensorDataDefaultDevice<int, 1>> select_labels_t1a2 = std::make_shared<TensorDataDefaultDevice<int, 1>>(Eigen::array<Eigen::Index, 1>({ 1 }));
    Eigen::Tensor<int, 1> labels_values_t1a2(1);
    labels_values_t1a2.setValues({ 0 });
    select_labels_t1a2->setData(labels_values_t1a2);
    SelectClause<int, DeviceT> select_clause2("1", "2", "y", select_labels_t1a2);
    std::shared_ptr<TensorDataDefaultDevice<int, 1>> select_labels_t1a3 = std::make_shared<TensorDataDefaultDevice<int, 1>>(Eigen::array<Eigen::Index, 1>({ 1 }));
    Eigen::Tensor<int, 1> labels_values_t1a3(1);
    labels_values_t1a3.setValues({ 0 });
    select_labels_t1a3->setData(labels_values_t1a3);
    SelectClause<int, DeviceT> select_clause3("1", "3", "z", select_labels_t1a3);

    TensorSelect tensorSelect;

    // Select the axes
    tensorSelect.selectClause(tensor_collection, select_clause1, device);
    tensorSelect.selectClause(tensor_collection, select_clause2, device);
    tensorSelect.selectClause(tensor_collection, select_clause3, device);

    // Apply the select clause
    if (apply_select)
      tensorSelect.applySelect(tensor_collection, { "1" }, device);
  }
  bool apply_select = true;
};

/// The delete select Functor for table 1
struct DeleteTable1 {
  template<typename DeviceT>
  void operator() (std::shared_ptr<TensorCollection<DeviceT>>& tensor_collection, DeviceT& device) {
    // Set up the SelectClauses for table 1 and axis 2 where labels=1
    std::shared_ptr<TensorDataDefaultDevice<int, 1>> select_labels_t1a2 = std::make_shared<TensorDataDefaultDevice<int, 1>>(Eigen::array<Eigen::Index, 1>({ 1 }));
    Eigen::Tensor<int, 1> labels_values_t1a2(1);
    labels_values_t1a2.setValues({ 1 });
    select_labels_t1a2->setData(labels_values_t1a2);
    SelectClause<int, DeviceT> select_clause2("1", "2", "y", select_labels_t1a2);

    TensorSelect tensorSelect;

    // Select the axes
    tensorSelect.selectClause(tensor_collection, select_clause2, device);
  }
};

/*TensorAppendToAxis Tests*/
BOOST_AUTO_TEST_CASE(constructorTensorAppendToAxis)
{
  TensorAppendToAxis<int, float, Eigen::DefaultDevice, 3>* ptr = nullptr;
  TensorAppendToAxis<int, float, Eigen::DefaultDevice, 3>* nullPointer = nullptr;
	ptr = new TensorAppendToAxis<int, float, Eigen::DefaultDevice, 3>();
  BOOST_CHECK_NE(ptr, nullPointer);
}

BOOST_AUTO_TEST_CASE(destructorTensorAppendToAxis)
{
  TensorAppendToAxis<int, float, Eigen::DefaultDevice, 3>* ptr = nullptr;
	ptr = new TensorAppendToAxis<int, float, Eigen::DefaultDevice, 3>();
  delete ptr;
}

BOOST_AUTO_TEST_CASE(redoAndUndoTensorAppendToAxis)
{
  Eigen::DefaultDevice device;

  // Set up the axes
  Eigen::Tensor<std::string, 1> dimensions1(1), dimensions2(1), dimensions3(1);
  dimensions1(0) = "x";
  dimensions2(0) = "y";
  dimensions3(0) = "z";
  int nlabels1 = 2, nlabels2 = 3, nlabels3 = 5;
  Eigen::Tensor<int, 2> labels1(1, nlabels1), labels2(1, nlabels2), labels3(1, nlabels3);
  labels1.setValues({ { 0, 1} });
  labels2.setValues({ { 0, 1, 2 } });
  labels3.setValues({ { 0, 1, 2, 3, 4 } });

  // Set up table 1
  TensorTableDefaultDevice<float, 3> tensorTable1("1");
  auto axis_1_ptr = std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1));
  auto axis_2_ptr = std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2));
  auto axis_3_ptr = std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("3", dimensions3, labels3));
  tensorTable1.addTensorAxis(axis_1_ptr);
  tensorTable1.addTensorAxis(axis_2_ptr);
  tensorTable1.addTensorAxis(axis_3_ptr);
  tensorTable1.setAxes(device);

  Eigen::Tensor<float, 3> tensor_values1(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        tensor_values1(i, j, k) = i + j * nlabels1 + k * nlabels1*nlabels2;
      }
    }
  }
  tensorTable1.setData(tensor_values1);
  std::shared_ptr<TensorTableDefaultDevice<float, 3>> tensorTable1_ptr = std::make_shared<TensorTableDefaultDevice<float, 3>>(tensorTable1);

  // set up table 2
  TensorTableDefaultDevice<int, 2> tensorTable2("2");
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable2.setAxes(device);

  Eigen::Tensor<int, 2> tensor_values2(Eigen::array<Eigen::Index, 2>({ nlabels1, nlabels2 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      tensor_values2(i, j) = i + j * nlabels1;
    }
  }
  tensorTable2.setData(tensor_values2);
  std::shared_ptr<TensorTableDefaultDevice<int, 2>> tensorTable2_ptr = std::make_shared<TensorTableDefaultDevice<int, 2>>(tensorTable2);

  // Set up the collection
  TensorCollectionDefaultDevice collection_1;
  collection_1.addTensorTable(tensorTable1_ptr, "1");
  collection_1.addTensorTable(tensorTable2_ptr, "1");
  std::shared_ptr<TensorCollection<Eigen::DefaultDevice>> collection_1_ptr = std::make_shared<TensorCollectionDefaultDevice>(collection_1);

  // Set up the new labels
  Eigen::Tensor<int, 2> labels_new_values(1, nlabels2);
  labels_new_values.setValues({ {3, 4, 5} });
  TensorDataDefaultDevice<int, 2> labels_new(Eigen::array<Eigen::Index, 2>({1, 3}));
  labels_new.setData(labels_new_values);
  std::shared_ptr<TensorData<int, Eigen::DefaultDevice, 2>> labels_new_ptr = std::make_shared<TensorDataDefaultDevice<int, 2>>(labels_new);

  // Set up the new values
  Eigen::Tensor<float, 3> tensor_values_new(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        tensor_values_new(i, j, k) = i + j * nlabels1 + k * nlabels1*nlabels2 + nlabels1 + nlabels2 * nlabels1 + nlabels3 * nlabels1*nlabels2;
      }
    }
  }
  TensorDataDefaultDevice<float, 3> values_new(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
  values_new.setData(tensor_values_new);
  std::shared_ptr<TensorData<float, Eigen::DefaultDevice, 3>> values_new_ptr = std::make_shared<TensorDataDefaultDevice<float, 3>>(values_new);

  // Reset `is_modified` to test for the new additions after the call to `setData`
  for (auto& tensor_table_map : collection_1_ptr->tables_) {
    for (auto& is_modified_map : tensor_table_map.second->getIsModified()) {
      is_modified_map.second->getData() = is_modified_map.second->getData().constant(0);
    }
  }

  // Test redo to append the new values
  TensorAppendToAxis<int, float, Eigen::DefaultDevice, 3> appendToAxis("1", "2", labels_new_ptr, values_new_ptr);
  appendToAxis.redo(collection_1_ptr, device);

  // Test for the expected table data
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), tensor_values1(i, j, k));
      }
    }
  }
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j + nlabels2, k), tensor_values_new(i, j, k));
      }
    }
  }

  // Test for the expected axis data
  BOOST_CHECK_EQUAL(axis_2_ptr->getNLabels(), nlabels2 + nlabels2);
  for (int i = 0; i < nlabels2 + nlabels2; ++i) {
    BOOST_CHECK_EQUAL(axis_2_ptr->getLabels()(0, i), i);
  }

  // Test for the expected indices data
  BOOST_CHECK_EQUAL(tensorTable1_ptr->getDimensions().at(tensorTable1_ptr->getDimFromAxisName("2")), nlabels2 + nlabels2);
  for (int i = 0; i < nlabels2 + nlabels2; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("2")->getData()(i), 0);
    if (i < nlabels2) {
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 0);
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 1);
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 1);
    }
    else {
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 1);
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 2);
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i - nlabels2 + 1);
    }
  }
  for (int i = 0; i < nlabels1; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("1")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("1")->getData()(i), i + 1);
  }
  for (int i = 0; i < nlabels3; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("3")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("3")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("3")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("3")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("3")->getData()(i), i + 1);
  }

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_labels_inserted_, 3);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_indices_inserted_, 3);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_data_inserted_, 30);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_data_inserted_, 0);

  // Test undo to remove the appended values
  appendToAxis.undo(collection_1_ptr, device);

  // Test for the expected table data
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), tensor_values1(i, j, k));
      }
    }
  }

  // Test for the expected axis data
  BOOST_CHECK_EQUAL(axis_2_ptr->getNLabels(), nlabels2);
  for (int i = 0; i < nlabels2; ++i) {
    BOOST_CHECK_EQUAL(axis_2_ptr->getLabels()(0, i), i);
  }

  // Test for the expected indices data
  BOOST_CHECK_EQUAL(tensorTable1_ptr->getDimensions().at(tensorTable1_ptr->getDimFromAxisName("2")), nlabels2);
  for (int i = 0; i < nlabels2; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 1);
  }
  for (int i = 0; i < nlabels1; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("1")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("1")->getData()(i), i + 1);
  }
  for (int i = 0; i < nlabels3; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("3")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("3")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("3")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("3")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("3")->getData()(i), i + 1);
  }

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_labels_inserted_, 3);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_indices_inserted_, 3);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getRedoLog().n_data_inserted_, 30);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_labels_deleted_, 3);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_indices_deleted_, 3);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_data_deleted_, 30);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(appendToAxis.getUndoLog().n_data_inserted_, 0);
}

/*TensorDeleteFromAxis Tests*/
BOOST_AUTO_TEST_CASE(constructorTensorDeleteFromAxis)
{
  TensorDeleteFromAxisDefaultDevice<int, float, 3>* ptr = nullptr;
  TensorDeleteFromAxisDefaultDevice<int, float, 3>* nullPointer = nullptr;
  ptr = new TensorDeleteFromAxisDefaultDevice<int, float, 3>();
  BOOST_CHECK_NE(ptr, nullPointer);
}

BOOST_AUTO_TEST_CASE(destructorTensorDeleteFromAxis)
{
  TensorDeleteFromAxisDefaultDevice<int, float, 3>* ptr = nullptr;
  ptr = new TensorDeleteFromAxisDefaultDevice<int, float, 3>();
  delete ptr;
}

BOOST_AUTO_TEST_CASE(redoAndTensorDeleteFromAxis)
{
  Eigen::DefaultDevice device;

  // Set up the axes
  Eigen::Tensor<std::string, 1> dimensions1(1), dimensions2(1), dimensions3(1);
  dimensions1(0) = "x";
  dimensions2(0) = "y";
  dimensions3(0) = "z";
  int nlabels1 = 2, nlabels2 = 3, nlabels3 = 5;
  Eigen::Tensor<int, 2> labels1(1, nlabels1), labels2(1, nlabels2), labels3(1, nlabels3);
  labels1.setValues({ { 0, 1} });
  labels2.setValues({ { 0, 1, 2 } });
  labels3.setValues({ { 0, 1, 2, 3, 4 } });

  // Set up table 1
  TensorTableDefaultDevice<float, 3> tensorTable1("1");
  auto axis_1_ptr = std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1));
  auto axis_2_ptr = std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2));
  auto axis_3_ptr = std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("3", dimensions3, labels3));
  tensorTable1.addTensorAxis(axis_1_ptr);
  tensorTable1.addTensorAxis(axis_2_ptr);
  tensorTable1.addTensorAxis(axis_3_ptr);
  tensorTable1.setAxes(device);

  Eigen::Tensor<float, 3> tensor_values1(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        tensor_values1(i, j, k) = i + j * nlabels1 + k * nlabels1*nlabels2;
      }
    }
  }
  tensorTable1.setData(tensor_values1);
  std::shared_ptr<TensorTableDefaultDevice<float, 3>> tensorTable1_ptr = std::make_shared<TensorTableDefaultDevice<float, 3>>(tensorTable1);

  // set up table 2
  TensorTableDefaultDevice<int, 2> tensorTable2("2");
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable2.setAxes(device);

  Eigen::Tensor<int, 2> tensor_values2(Eigen::array<Eigen::Index, 2>({ nlabels1, nlabels2 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      tensor_values2(i, j) = i + j * nlabels1;
    }
  }
  tensorTable2.setData(tensor_values2);
  std::shared_ptr<TensorTableDefaultDevice<int, 2>> tensorTable2_ptr = std::make_shared<TensorTableDefaultDevice<int, 2>>(tensorTable2);

  // Set up the collection
  TensorCollectionDefaultDevice collection_1;
  collection_1.addTensorTable(tensorTable1_ptr, "1");
  collection_1.addTensorTable(tensorTable2_ptr, "1");
  std::shared_ptr<TensorCollection<Eigen::DefaultDevice>> collection_1_ptr = std::make_shared<TensorCollectionDefaultDevice>(collection_1);

  // Make the expected tensor data
  Eigen::Tensor<float, 3> expected_tensor_values(nlabels1, nlabels2 - 1, nlabels3);
  for (int i = 0; i < nlabels1; ++i) {
    int iter = 0;
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        if (j != 1)
          expected_tensor_values(i, iter, k) = i + j * nlabels1 + k * nlabels1*nlabels2;
      }
      if (j != 1) ++iter;
    }
  }

  // Reset `is_modified` to test for the new additions after the call to `setData`
  for (auto& tensor_table_map : collection_1_ptr->tables_) {
    for (auto& is_modified_map : tensor_table_map.second->getIsModified()) {
      is_modified_map.second->getData() = is_modified_map.second->getData().constant(0);
    }
  }

  // Test redo to delete the specified values
  TensorDeleteFromAxisDefaultDevice<int, float, 3> deleteFromAxis("1", "2", DeleteTable1());
  deleteFromAxis.redo(collection_1_ptr, device);

  // Test for the expected table data
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2 - 1; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), expected_tensor_values(i, j, k));
      }
    }
  }

  // Test for the expected axis data
  BOOST_CHECK_EQUAL(axis_2_ptr->getNLabels(), nlabels2 - 1);
  for (int i = 0; i < nlabels2 - 1; ++i) {
    if (i<1)
      BOOST_CHECK_EQUAL(axis_2_ptr->getLabels()(0, i), i);
    else
      BOOST_CHECK_EQUAL(axis_2_ptr->getLabels()(0, i), i + 1);
  }

  // Test for the expected indices data
  BOOST_CHECK_EQUAL(tensorTable1_ptr->getDimensions().at(tensorTable1_ptr->getDimFromAxisName("2")), nlabels2 - 1);
  for (int i = 0; i < nlabels2 - 1; ++i) {
    if (i < 1) {
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 1);
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 1);
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 1);
    }
    else {
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 2);
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 2);
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 2);
    }
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 1);
  }

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_labels_deleted_, 1);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_indices_deleted_, 1);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_data_deleted_, 10);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_data_inserted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_data_inserted_, 0);

  // Test redo to restore the deleted values
  deleteFromAxis.undo(collection_1_ptr, device);

  // Test for the expected table data
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), tensor_values1(i, j, k));
      }
    }
  }

  // Test for the expected axis data
  BOOST_CHECK_EQUAL(axis_2_ptr->getNLabels(), nlabels2);
  for (int i = 0; i < nlabels2; ++i) {
    BOOST_CHECK_EQUAL(axis_2_ptr->getLabels()(0, i), i);
  }

  // Test for the expected indices data
  BOOST_CHECK_EQUAL(tensorTable1_ptr->getDimensions().at(tensorTable1_ptr->getDimFromAxisName("2")), nlabels2);
  for (int i = 0; i < nlabels2; ++i) {
    if (i == 1) {
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 1);
    }
    else {
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 0);
    }
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 1);
  }

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_labels_deleted_, 1);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_indices_deleted_, 1);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_data_deleted_, 10);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getRedoLog().n_data_inserted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_labels_inserted_, 1);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_indices_inserted_, 1);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(deleteFromAxis.getUndoLog().n_data_inserted_, 10);
}

/*TensorUpdateSelectValues Tests*/
BOOST_AUTO_TEST_CASE(constructorTensorUpdateSelectValues)
{
  TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>* ptr = nullptr;
  TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>* nullPointer = nullptr;
  ptr = new TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>();
  BOOST_CHECK_NE(ptr, nullPointer);
}

BOOST_AUTO_TEST_CASE(destructorTensorUpdateSelectValues)
{
  TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>* ptr = nullptr;
  ptr = new TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>();
  delete ptr;
}

BOOST_AUTO_TEST_CASE(redoAndUndoTensorUpdateSelectValues)
{
  Eigen::DefaultDevice device;

  // Set up the axes
  Eigen::Tensor<std::string, 1> dimensions1(1), dimensions2(1), dimensions3(1);
  dimensions1(0) = "x";
  dimensions2(0) = "y";
  dimensions3(0) = "z";
  int nlabels1 = 2, nlabels2 = 3, nlabels3 = 5;
  Eigen::Tensor<int, 2> labels1(1, nlabels1), labels2(1, nlabels2), labels3(1, nlabels3);
  labels1.setValues({ { 0, 1} });
  labels2.setValues({ { 0, 1, 2 } });
  labels3.setValues({ { 0, 1, 2, 3, 4 } });

  // Set up table 1
  TensorTableDefaultDevice<float, 3> tensorTable1("1");
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("3", dimensions3, labels3)));
  tensorTable1.setAxes(device);

  Eigen::Tensor<float, 3> tensor_values1(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        tensor_values1(i, j, k) = i + j * nlabels1 + k * nlabels1*nlabels2;
      }
    }
  }
  tensorTable1.setData(tensor_values1);
  std::shared_ptr<TensorTableDefaultDevice<float, 3>> tensorTable1_ptr = std::make_shared<TensorTableDefaultDevice<float, 3>>(tensorTable1);

  // set up table 2
  TensorTableDefaultDevice<int, 2> tensorTable2("2");
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable2.setAxes(device);

  Eigen::Tensor<int, 2> tensor_values2(Eigen::array<Eigen::Index, 2>({ nlabels1, nlabels2 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      tensor_values2(i, j) = i + j * nlabels1;
    }
  }
  tensorTable2.setData(tensor_values2);
  std::shared_ptr<TensorTableDefaultDevice<int, 2>> tensorTable2_ptr = std::make_shared<TensorTableDefaultDevice<int, 2>>(tensorTable2);

  // Set up the collection
  TensorCollectionDefaultDevice collection_1;
  collection_1.addTensorTable(tensorTable1_ptr, "1");
  collection_1.addTensorTable(tensorTable2_ptr, "1");
  std::shared_ptr<TensorCollection<Eigen::DefaultDevice>> collection_1_ptr = std::make_shared<TensorCollectionDefaultDevice>(collection_1);

  // Set up the update values
  Eigen::Tensor<float, 3> values_new_values(nlabels1, 1, 1);
  for (int i = 0; i < nlabels1; ++i)
    values_new_values(i, 0, 0) = (float)((i + 1) * 10);
  TensorDataDefaultDevice<float, 3> values_new(Eigen::array<Eigen::Index, 3>({ nlabels1, 1, 1 }));
  values_new.setData(values_new_values);

  // Set up the update
  TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3> tensorUpdate("1", SelectTable1(), std::make_shared<TensorDataDefaultDevice<float, 3>>(values_new));

  // Reset `is_modified` to test for the new additions after the call to `setData`
  for (auto& tensor_table_map : collection_1_ptr->tables_) {
    for (auto& is_modified_map : tensor_table_map.second->getIsModified()) {
      is_modified_map.second->getData() = is_modified_map.second->getData().constant(0);
    }
  }

  // Test redo
  tensorUpdate.redo(collection_1_ptr, device);
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < 1; ++j) {
      for (int k = 0; k < 1; ++k) {
        BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), values_new_values(i, j, k));
        BOOST_CHECK_EQUAL(tensorUpdate.getValuesOld()->getData()(i, j, k), tensor_values1(i, j, k));
      }
    }
  }

  // Test the changed indices
  for (int i = 0; i < nlabels1; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("1")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("1")->getData()(i), i + 1);
  }

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_inserted_, 0);

  // Test undo
  tensorUpdate.undo(collection_1_ptr, device);
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < 1; ++j) {
      for (int k = 0; k < 1; ++k) {
        BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), tensor_values1(i, j, k));
      }
    }
  }

  // Test the changed indices
  for (int i = 0; i < nlabels1; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("1")->getData()(i), 1); // TODO: should be reverted to 0
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("1")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("1")->getData()(i), i + 1);
  }

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_inserted_, 0);
}

/*TensorUpdateValues Tests*/
BOOST_AUTO_TEST_CASE(constructorTensorUpdateValues)
{
	TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>* ptr = nullptr;
	TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>* nullPointer = nullptr;
	ptr = new TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>();
	BOOST_CHECK_NE(ptr, nullPointer);
}

BOOST_AUTO_TEST_CASE(destructorTensorUpdateValues)
{
	TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>* ptr = nullptr;
	ptr = new TensorUpdateSelectValues<float, Eigen::DefaultDevice, 3>();
	delete ptr;
}

BOOST_AUTO_TEST_CASE(redoAndUndoTensorUpdateValues)
{
	Eigen::DefaultDevice device;

	// Set up the axes
	Eigen::Tensor<std::string, 1> dimensions1(1), dimensions2(1), dimensions3(1);
	dimensions1(0) = "x";
	dimensions2(0) = "y";
	dimensions3(0) = "z";
	int nlabels1 = 2, nlabels2 = 3, nlabels3 = 5;
	Eigen::Tensor<int, 2> labels1(1, nlabels1), labels2(1, nlabels2), labels3(1, nlabels3);
	labels1.setValues({ { 0, 1} });
	labels2.setValues({ { 0, 1, 2 } });
	labels3.setValues({ { 0, 1, 2, 3, 4 } });

	// Set up table 1
	TensorTableDefaultDevice<float, 3> tensorTable1("1");
	tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
	tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
	tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("3", dimensions3, labels3)));
	tensorTable1.setAxes(device);

	Eigen::Tensor<float, 3> tensor_values1(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
	for (int i = 0; i < nlabels1; ++i) {
		for (int j = 0; j < nlabels2; ++j) {
			for (int k = 0; k < nlabels3; ++k) {
				tensor_values1(i, j, k) = i + j * nlabels1 + k * nlabels1 * nlabels2;
			}
		}
	}
	tensorTable1.setData(tensor_values1);
	std::shared_ptr<TensorTableDefaultDevice<float, 3>> tensorTable1_ptr = std::make_shared<TensorTableDefaultDevice<float, 3>>(tensorTable1);

	// set up table 2
	TensorTableDefaultDevice<int, 2> tensorTable2("2");
	tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
	tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
	tensorTable2.setAxes(device);

	Eigen::Tensor<int, 2> tensor_values2(Eigen::array<Eigen::Index, 2>({ nlabels1, nlabels2 }));
	for (int i = 0; i < nlabels1; ++i) {
		for (int j = 0; j < nlabels2; ++j) {
			tensor_values2(i, j) = i + j * nlabels1;
		}
	}
	tensorTable2.setData(tensor_values2);
	std::shared_ptr<TensorTableDefaultDevice<int, 2>> tensorTable2_ptr = std::make_shared<TensorTableDefaultDevice<int, 2>>(tensorTable2);

	// Set up the collection
	TensorCollectionDefaultDevice collection_1;
	collection_1.addTensorTable(tensorTable1_ptr, "1");
	collection_1.addTensorTable(tensorTable2_ptr, "1");
	std::shared_ptr<TensorCollection<Eigen::DefaultDevice>> collection_1_ptr = std::make_shared<TensorCollectionDefaultDevice>(collection_1);

	// Set up the update values
	Eigen::Tensor<float, 3> values_new_values(nlabels1, 1, 1);
	for (int i = 0; i < nlabels1; ++i)
		values_new_values(i, 0, 0) = (float)((i + 1) * 10);
	TensorDataDefaultDevice<float, 3> values_new(Eigen::array<Eigen::Index, 3>({ nlabels1, 1, 1 }));
	values_new.setData(values_new_values);

	// Set up the update
	SelectTable1 selectTable1;
	selectTable1.apply_select = false;
	TensorUpdateValues<float, Eigen::DefaultDevice, 3> tensorUpdate("1", selectTable1, std::make_shared<TensorDataDefaultDevice<float, 3>>(values_new));

	// Reset `is_modified` to test for the new additions after the call to `setData`
	for (auto& tensor_table_map : collection_1_ptr->tables_) {
		for (auto& is_modified_map : tensor_table_map.second->getIsModified()) {
			is_modified_map.second->getData() = is_modified_map.second->getData().constant(0);
		}
	}

	// Test redo
	tensorUpdate.redo(collection_1_ptr, device);
	for (int i = 0; i < nlabels1; ++i) {
		BOOST_CHECK_EQUAL(tensorUpdate.getValuesOld()->getData()(i), tensor_values1(i, 0, 0));
		for (int j = 0; j < nlabels2; ++j) {
			for (int k = 0; k < nlabels3; ++k) {
				if (j == 0 && k == 0)
					BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), values_new_values(i, j, k));
				else
					BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), tensor_values1(i, j, k));
			}
		}
	}
	for (int i = 0; i < nlabels1; ++i) {
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("1")->getData()(i), i + 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("1")->getData()(i), i + 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("1")->getData()(i), 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("1")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("1")->getData()(i), 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("1")->getData()(i), i + 1);
	}
	for (int i = 0; i < nlabels2; ++i) {
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 1);
		if (i == 0)
			BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 1);
		else
			BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("2")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 1);
	}
	for (int i = 0; i < nlabels3; ++i) {
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("3")->getData()(i), i + 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("3")->getData()(i), i + 1);
		if (i == 0)
			BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 1);
		else
			BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("3")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("3")->getData()(i), 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("3")->getData()(i), i + 1);
	}

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_inserted_, 0);

	// Test undo
	tensorUpdate.undo(collection_1_ptr, device);
	for (int i = 0; i < nlabels1; ++i) {
		for (int j = 0; j < nlabels2; ++j) {
			for (int k = 0; k < nlabels3; ++k) {
				BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), tensor_values1(i, j, k));
			}
		}
	}
	for (int i = 0; i < nlabels1; ++i) {
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("1")->getData()(i), i + 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("1")->getData()(i), i + 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("1")->getData()(i), 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("1")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("1")->getData()(i), 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("1")->getData()(i), i + 1);
	}
	for (int i = 0; i < nlabels2; ++i) {
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 1);
		if (i == 0)
			BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 1);
		else
			BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("2")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 1);
	}
	for (int i = 0; i < nlabels3; ++i) {
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("3")->getData()(i), i + 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("3")->getData()(i), i + 1);
		if (i == 0)
			BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 1);
		else
			BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("3")->getData()(i), 0);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("3")->getData()(i), 1);
		BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("3")->getData()(i), i + 1);
	}

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_inserted_, 0);
}

/*TensorUpdateConstant Tests*/
BOOST_AUTO_TEST_CASE(constructorTensorUpdateConstant)
{
  TensorUpdateConstant<float, Eigen::DefaultDevice>* ptr = nullptr;
  TensorUpdateConstant<float, Eigen::DefaultDevice>* nullPointer = nullptr;
  ptr = new TensorUpdateConstant<float, Eigen::DefaultDevice>();
  BOOST_CHECK_NE(ptr, nullPointer);
}

BOOST_AUTO_TEST_CASE(destructorTensorUpdateConstant)
{
  TensorUpdateConstant<float, Eigen::DefaultDevice>* ptr = nullptr;
  ptr = new TensorUpdateConstant<float, Eigen::DefaultDevice>();
  delete ptr;
}

BOOST_AUTO_TEST_CASE(redoAndUndoTensorUpdateConstant)
{
  Eigen::DefaultDevice device;

  // Set up the axes
  Eigen::Tensor<std::string, 1> dimensions1(1), dimensions2(1), dimensions3(1);
  dimensions1(0) = "x";
  dimensions2(0) = "y";
  dimensions3(0) = "z";
  int nlabels1 = 2, nlabels2 = 3, nlabels3 = 5;
  Eigen::Tensor<int, 2> labels1(1, nlabels1), labels2(1, nlabels2), labels3(1, nlabels3);
  labels1.setValues({ { 0, 1} });
  labels2.setValues({ { 0, 1, 2 } });
  labels3.setValues({ { 0, 1, 2, 3, 4 } });

  // Set up table 1
  TensorTableDefaultDevice<float, 3> tensorTable1("1");
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("3", dimensions3, labels3)));
  tensorTable1.setAxes(device);

  Eigen::Tensor<float, 3> tensor_values1(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        tensor_values1(i, j, k) = i + j * nlabels1 + k * nlabels1*nlabels2;
      }
    }
  }
  tensorTable1.setData(tensor_values1);
  std::shared_ptr<TensorTableDefaultDevice<float, 3>> tensorTable1_ptr = std::make_shared<TensorTableDefaultDevice<float, 3>>(tensorTable1);

  // set up table 2
  TensorTableDefaultDevice<int, 2> tensorTable2("2");
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable2.setAxes(device);

  Eigen::Tensor<int, 2> tensor_values2(Eigen::array<Eigen::Index, 2>({ nlabels1, nlabels2 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      tensor_values2(i, j) = i + j * nlabels1;
    }
  }
  tensorTable2.setData(tensor_values2);
  std::shared_ptr<TensorTableDefaultDevice<int, 2>> tensorTable2_ptr = std::make_shared<TensorTableDefaultDevice<int, 2>>(tensorTable2);

  // Set up the collection
  TensorCollectionDefaultDevice collection_1;
  collection_1.addTensorTable(tensorTable1_ptr, "1");
  collection_1.addTensorTable(tensorTable2_ptr, "1");
  std::shared_ptr<TensorCollection<Eigen::DefaultDevice>> collection_1_ptr = std::make_shared<TensorCollectionDefaultDevice>(collection_1);

  // Set up the update values
  Eigen::Tensor<float, 1> values_new_values(1);
  values_new_values.setConstant(100);
  TensorDataDefaultDevice<float, 1> values_new(Eigen::array<Eigen::Index, 1>({ 1 }));
  values_new.setData(values_new_values);
  auto values_new_ptr = std::make_shared<TensorDataDefaultDevice<float, 1>>(values_new);

  // Set up the update
  SelectTable1 selectTable1;
  selectTable1.apply_select = false;
  TensorUpdateConstant<float, Eigen::DefaultDevice> tensorUpdate("1", selectTable1, values_new_ptr);

  // Reset `is_modified` to test for the new additions after the call to `setData`
  for (auto& tensor_table_map : collection_1_ptr->tables_) {
    for (auto& is_modified_map : tensor_table_map.second->getIsModified()) {
      is_modified_map.second->getData() = is_modified_map.second->getData().constant(0);
    }
  }

  // Test redo
  tensorUpdate.redo(collection_1_ptr, device);
  for (int i = 0; i < nlabels1; ++i) {
    BOOST_CHECK_EQUAL(tensorUpdate.getValuesOld()->getData()(i), tensor_values1(i, 0, 0));
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        if (j==0 && k==0)
          BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), 100);
        else
          BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), tensor_values1(i,j,k));
      }
    }
  }
  for (int i = 0; i < nlabels1; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("1")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("1")->getData()(i), i + 1);
  }
  for (int i = 0; i < nlabels2; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 1);
    if (i==0)
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 1);
    else
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 1);
  }
  for (int i = 0; i < nlabels3; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("3")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("3")->getData()(i), i + 1);
    if (i==0)
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 1);
    else
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("3")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("3")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("3")->getData()(i), i + 1);
  }

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_inserted_, 0);

  // Test undo
  tensorUpdate.undo(collection_1_ptr, device);
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        BOOST_CHECK_EQUAL(tensorTable1_ptr->getData()(i, j, k), tensor_values1(i, j, k));
      }
    }
  }
  for (int i = 0; i < nlabels1; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("1")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("1")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("1")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("1")->getData()(i), i + 1);
  }
  for (int i = 0; i < nlabels2; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("2")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("2")->getData()(i), i + 1);
    if (i == 0)
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 1);
    else
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("2")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("2")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("2")->getData()(i), i + 1);
  }
  for (int i = 0; i < nlabels3; ++i) {
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndices().at("3")->getData()(i), i + 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getIndicesView().at("3")->getData()(i), i + 1);
    if (i == 0)
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 1);
    else
      BOOST_CHECK_EQUAL(tensorTable1_ptr->getIsModified().at("3")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getNotInMemory().at("3")->getData()(i), 0);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardId().at("3")->getData()(i), 1);
    BOOST_CHECK_EQUAL(tensorTable1_ptr->getShardIndices().at("3")->getData()(i), i + 1);
  }

  // Test for the expected redo/undo log data
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getRedoLog().n_data_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_labels_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_updated_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_indices_inserted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_deleted_, 0);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_updated_, 2);
  BOOST_CHECK_EQUAL(tensorUpdate.getUndoLog().n_data_inserted_, 0);
}

/*TensorAddTable Tests*/
BOOST_AUTO_TEST_CASE(constructorTensorAddTable)
{
  TensorAddTable<TensorTable<float, Eigen::DefaultDevice, 3>, Eigen::DefaultDevice>* ptr = nullptr;
  TensorAddTable<TensorTable<float, Eigen::DefaultDevice, 3>, Eigen::DefaultDevice>* nullPointer = nullptr;
  ptr = new TensorAddTable<TensorTable<float, Eigen::DefaultDevice, 3>, Eigen::DefaultDevice>();
  BOOST_CHECK_NE(ptr, nullPointer);
}

BOOST_AUTO_TEST_CASE(destructorTensorAddTable)
{
  TensorAddTable<TensorTable<float, Eigen::DefaultDevice, 3>, Eigen::DefaultDevice>* ptr = nullptr;
  ptr = new TensorAddTable<TensorTable<float, Eigen::DefaultDevice, 3>, Eigen::DefaultDevice>();
  delete ptr;
}

BOOST_AUTO_TEST_CASE(redoAndUndoTensorAddTable)
{
  Eigen::DefaultDevice device;

  // Set up the axes
  Eigen::Tensor<std::string, 1> dimensions1(1), dimensions2(1), dimensions3(1);
  dimensions1(0) = "x";
  dimensions2(0) = "y";
  dimensions3(0) = "z";
  int nlabels1 = 2, nlabels2 = 3, nlabels3 = 5;
  Eigen::Tensor<int, 2> labels1(1, nlabels1), labels2(1, nlabels2), labels3(1, nlabels3);
  labels1.setValues({ { 0, 1} });
  labels2.setValues({ { 0, 1, 2 } });
  labels3.setValues({ { 0, 1, 2, 3, 4 } });

  // Set up table 1
  TensorTableDefaultDevice<float, 3> tensorTable1("1");
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("3", dimensions3, labels3)));
  tensorTable1.setAxes(device);

  Eigen::Tensor<float, 3> tensor_values1(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        tensor_values1(i, j, k) = i + j * nlabels1 + k * nlabels1*nlabels2;
      }
    }
  }
  tensorTable1.setData(tensor_values1);
  std::shared_ptr<TensorTableDefaultDevice<float, 3>> tensorTable1_ptr = std::make_shared<TensorTableDefaultDevice<float, 3>>(tensorTable1);

  // set up table 2
  TensorTableDefaultDevice<int, 2> tensorTable2("2");
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable2.setAxes(device);

  Eigen::Tensor<int, 2> tensor_values2(Eigen::array<Eigen::Index, 2>({ nlabels1, nlabels2 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      tensor_values2(i, j) = i + j * nlabels1;
    }
  }
  tensorTable2.setData(tensor_values2);
  std::shared_ptr<TensorTableDefaultDevice<int, 2>> tensorTable2_ptr = std::make_shared<TensorTableDefaultDevice<int, 2>>(tensorTable2);

  // Set up the collection
  TensorCollectionDefaultDevice collection_1;
  collection_1.addTensorTable(tensorTable1_ptr, "1");
  std::shared_ptr<TensorCollection<Eigen::DefaultDevice>> collection_1_ptr = std::make_shared<TensorCollectionDefaultDevice>(collection_1);

  // Test Redo
  TensorAddTable<TensorTable<int, Eigen::DefaultDevice, 2>, Eigen::DefaultDevice> tensorAddTable(tensorTable2_ptr, "1");
  tensorAddTable.redo(collection_1_ptr, device);
  BOOST_CHECK(collection_1_ptr->getTableNames() == std::vector<std::string>({ "1", "2" }));

  // Test Undo
  tensorAddTable.undo(collection_1_ptr, device);
  BOOST_CHECK(collection_1_ptr->getTableNames() == std::vector<std::string>({ "1" }));
}

/*TensorDropTable Tests*/
BOOST_AUTO_TEST_CASE(constructorTensorDropTable)
{
  TensorDropTable<Eigen::DefaultDevice>* ptr = nullptr;
  TensorDropTable<Eigen::DefaultDevice>* nullPointer = nullptr;
  ptr = new TensorDropTable<Eigen::DefaultDevice>();
  BOOST_CHECK_NE(ptr, nullPointer);
}

BOOST_AUTO_TEST_CASE(destructorTensorDropTable)
{
  TensorDropTable<Eigen::DefaultDevice>* ptr = nullptr;
  ptr = new TensorDropTable<Eigen::DefaultDevice>();
  delete ptr;
}

BOOST_AUTO_TEST_CASE(redoAndUndoTensorDropTable)
{
  Eigen::DefaultDevice device;

  // Set up the axes
  Eigen::Tensor<std::string, 1> dimensions1(1), dimensions2(1), dimensions3(1);
  dimensions1(0) = "x";
  dimensions2(0) = "y";
  dimensions3(0) = "z";
  int nlabels1 = 2, nlabels2 = 3, nlabels3 = 5;
  Eigen::Tensor<int, 2> labels1(1, nlabels1), labels2(1, nlabels2), labels3(1, nlabels3);
  labels1.setValues({ { 0, 1} });
  labels2.setValues({ { 0, 1, 2 } });
  labels3.setValues({ { 0, 1, 2, 3, 4 } });

  // Set up table 1
  TensorTableDefaultDevice<float, 3> tensorTable1("1");
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable1.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("3", dimensions3, labels3)));
  tensorTable1.setAxes(device);

  Eigen::Tensor<float, 3> tensor_values1(Eigen::array<Eigen::Index, 3>({ nlabels1, nlabels2, nlabels3 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      for (int k = 0; k < nlabels3; ++k) {
        tensor_values1(i, j, k) = i + j * nlabels1 + k * nlabels1*nlabels2;
      }
    }
  }
  tensorTable1.setData(tensor_values1);
  std::shared_ptr<TensorTableDefaultDevice<float, 3>> tensorTable1_ptr = std::make_shared<TensorTableDefaultDevice<float, 3>>(tensorTable1);

  // set up table 2
  TensorTableDefaultDevice<int, 2> tensorTable2("2");
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("1", dimensions1, labels1)));
  tensorTable2.addTensorAxis(std::make_shared<TensorAxisDefaultDevice<int>>(TensorAxisDefaultDevice<int>("2", dimensions2, labels2)));
  tensorTable2.setAxes(device);

  Eigen::Tensor<int, 2> tensor_values2(Eigen::array<Eigen::Index, 2>({ nlabels1, nlabels2 }));
  for (int i = 0; i < nlabels1; ++i) {
    for (int j = 0; j < nlabels2; ++j) {
      tensor_values2(i, j) = i + j * nlabels1;
    }
  }
  tensorTable2.setData(tensor_values2);
  std::shared_ptr<TensorTableDefaultDevice<int, 2>> tensorTable2_ptr = std::make_shared<TensorTableDefaultDevice<int, 2>>(tensorTable2);

  // Set up the collection
  TensorCollectionDefaultDevice collection_1;
  collection_1.addTensorTable(tensorTable1_ptr, "1");
  collection_1.addTensorTable(tensorTable2_ptr, "1");
  std::shared_ptr<TensorCollection<Eigen::DefaultDevice>> collection_1_ptr = std::make_shared<TensorCollectionDefaultDevice>(collection_1);

  // Test redo drop table
  TensorDropTable<Eigen::DefaultDevice> tensorDropTable("1");
  tensorDropTable.redo(collection_1_ptr, device);
  BOOST_CHECK(collection_1_ptr->getTableNames() == std::vector<std::string>({ "2" }));

  // Test undo drop table
  tensorDropTable.undo(collection_1_ptr, device);
  BOOST_CHECK(collection_1_ptr->getTableNames() == std::vector<std::string>({ "1", "2" }));
}

BOOST_AUTO_TEST_SUITE_END()