 
   TYPED_TEST_CASE(GDFGroupByTest, TestCases);
 
-// TODO Update the create_input function to ensure all keys are actually unique
-TYPED_TEST(GDFGroupByTest, AggregationTestDeviceAllUnique)
+TYPED_TEST(GDFGroupByTest, ExampleTest)
 {
   const int num_keys = 10;
   const int num_values_per_key = 1;
   auto input = this->create_reference_input(num_keys, num_values_per_key, 5, 5, true);
   auto expected_values = this->compute_reference_solution(input.first, input.second, true);
+
+  gdf_col_pointer gdf_groupby_column = create_gdf_column(input.first);
+  gdf_col_pointer gdf_aggregation_column = create_gdf_column(input.second);
+
+
 //
 //  this->build_aggregation_table_device(input);
 //  this->verify_aggregation_table(expected_values);

  ~GDFGroupByTest() {} 

  std::pair<std::vector<key_type>, std::vector<value_type>>
  create_reference_input(const size_t num_keys, const size_t num_values_per_key, const int max_key = RAND_MAX, const int max_value = RAND_MAX, bool print = false) 
  {
    const size_t input_size = num_keys * num_values_per_key;

    std::vector<key_type> groupby_column;
    std::vector<value_type> aggregation_column;

    groupby_column.reserve(input_size);
    aggregation_column.reserve(input_size);

    for(int i = 0; i < num_keys; ++i )
    {
      // Create random key
      key_type current_key = std::rand() % max_key;

      // Don't use unused_key
      while(current_key == this->unused_key)
      {
        current_key = std::rand();
      }

      // For the current key, generate random values
      for(int j = 0; j < num_values_per_key; ++j)
      {
        value_type current_value = std::rand() % max_value;

        // Don't use unused_value
        while(current_value == this->unused_value)
        {
          current_value = std::rand() % max_value;
        }

        // Store current key and value
        groupby_column.push_back(current_key);
        aggregation_column.push_back(current_value);
      }
    }

    if(print)
    {
      std::cout << "Number of unique keys: " << num_keys 
                << " Values per key: " << num_values_per_key << "\n";

      std::cout << "Group By Column. Size: " << groupby_column.size() << " \n";
      std::copy(groupby_column.begin(), groupby_column.end(), std::ostream_iterator<key_type>(std::cout, " "));
      std::cout << "\n";

      std::cout << "Aggregation Column. Size: " << aggregation_column.size() << "\n";
      std::copy(aggregation_column.begin(), aggregation_column.end(), std::ostream_iterator<value_type>(std::cout, " "));
      std::cout << "\n";
    }

    return std::make_pair(groupby_column, aggregation_column);
  }

  // Creates a gdf_column from a std::vector
  template <typename col_type>
  gdf_col_pointer create_gdf_column(std::vector<col_type> const & host_vector)
  {

    // Create a new instance of a gdf_column with a custom deleter that will free
    // the associated device memory when it eventually goes out of scope
    gdf_col_pointer the_column{new gdf_column, gdf_col_deleter};
    // Allocate device storage for gdf_column and copy contents from host_vector
    cudaMalloc(&(the_column->data), host_vector.size() * sizeof(col_type));
    cudaMemcpy(the_column->data, host_vector.data(), host_vector.size() * sizeof(col_type), cudaMemcpyHostToDevice);

    // Deduce the type and set the gdf_dtype accordingly
    gdf_dtype gdf_col_type;
    if(std::is_same<col_type,int8_t>::value) gdf_col_type = GDF_INT8;
    else if(std::is_same<col_type,uint8_t>::value) gdf_col_type = GDF_INT8;
    else if(std::is_same<col_type,int16_t>::value) gdf_col_type = GDF_INT16;
    else if(std::is_same<col_type,uint16_t>::value) gdf_col_type = GDF_INT16;
    else if(std::is_same<col_type,int32_t>::value) gdf_col_type = GDF_INT32;
    else if(std::is_same<col_type,uint32_t>::value) gdf_col_type = GDF_INT32;
    else if(std::is_same<col_type,int64_t>::value) gdf_col_type = GDF_INT64;
    else if(std::is_same<col_type,uint64_t>::value) gdf_col_type = GDF_INT64;
    else if(std::is_same<col_type,float>::value) gdf_col_type = GDF_FLOAT32;
    else if(std::is_same<col_type,double>::value) gdf_col_type = GDF_FLOAT64;
    // Fill the gdf_column members
    the_column->valid = nullptr;
    the_column->size = host_vector.size();
    the_column->dtype = gdf_col_type;
    gdf_dtype_extra_info extra_info;
    extra_info.time_unit = TIME_UNIT_NONE;
    the_column->dtype_info = extra_info;
    return the_column;
  }

  template <class aggregation_operation>
  std::map<key_type, value_type> 
  compute_reference_solution(std::vector<key_type> const & groupby_column,
                             std::vector<value_type> const & aggregation_column,
                             bool print = false)
  {
    std::map<key_type, value_type> expected_values;

    // Computing the reference solution for AVG has to be handled uniquely
    if(std::is_same<aggregation_operation, avg_op<value_type>>::value)
    {

      // For each unique key, compute the SUM and COUNT aggregation
      std::map<key_type, value_type> counts = compute_reference_solution<count_op<value_type>>(groupby_column, aggregation_column);
      std::map<key_type, value_type> sums = compute_reference_solution<sum_op<value_type>>(groupby_column, aggregation_column);
      
      // For each unique key, compute it's AVG as SUM / COUNT
      for(auto & sum: sums)
      {
        const auto current_key = sum.first;
    
        auto count = counts.find(current_key);

        EXPECT_NE(count, counts.end()) << "Failed to find match for key " << current_key << " from the SUM solution in the COUNT solution";

        // Compute the AVG in place on the SUM map
        sum.second = sum.second / count->second;
      }

      expected_values = sums;
    }
    else
    {
      aggregation_operation op;

      for(size_t i = 0; i < groupby_column.size(); ++i){

        key_type current_key = groupby_column[i];
        value_type current_value = aggregation_column[i];

        // Use a STL map to keep track of the aggregation for each key
        auto found = expected_values.find(current_key);

        // Key doesn't exist yet, insert it
        if(found == expected_values.end())
        {
          // To support operations like `count`, on the first insert, perform the
          // operation on the new value and the operation's identity value and store the result
          current_value = op(current_value, aggregation_operation::IDENTITY);

          expected_values.insert(std::make_pair(current_key,current_value)); 

          if(print)
            std::cout << "First Insert of Key: " << current_key << " value: " << current_value << std::endl;
        }
        // Key exists, update the value with the operator
        else
        {
          value_type new_value = op(current_value, found->second);
          if(print)
            std::cout << "Insert of Key: " << current_key << " inserting value: " << current_value 
              << " storing: " << new_value << std::endl;
          found->second = new_value;
        }
      }
    }

    if(print)
    {
      for(auto const & a : expected_values)
      {
        std::cout << a.first << ", " << a.second << std::endl;
      }
    }


    return expected_values;
  }

  // Dispatches computing the reference solution based on which aggregation is to be performed
  // determined by the test_parameters class template argument
  std::map<key_type, value_type> 
  compute_reference_solution(std::vector<key_type> const & groupby_column, 
                             std::vector<value_type> const & aggregation_column,
                             bool print = false)
  {
    switch(test_parameters::the_aggregator)
    {
      // FIXME May need to use this->template compute_reference_solution<...>(...);
      case agg_op::MIN:  return compute_reference_solution<min_op<value_type>>(groupby_column,aggregation_column, print);
      case agg_op::MAX:  return compute_reference_solution<max_op<value_type>>(groupby_column,aggregation_column, print);
      case agg_op::SUM:  return compute_reference_solution<sum_op<value_type>>(groupby_column,aggregation_column, print);
      case agg_op::COUNT:return compute_reference_solution<count_op<value_type>>(groupby_column,aggregation_column, print);
      case agg_op::AVG:  return compute_reference_solution<avg_op<value_type>>(groupby_column,aggregation_column, print);
      default: std::cout << "Invalid aggregation operation.\n";
    }

    return std::map<key_type, value_type>();
  }
};

// Google Test can only do a parameterized typed-test over a single type, so we have
// to nest multiple types inside of the TestParameters struct

  AVG
};

// unique_ptr wrappers for gdf_columns that will free their data
std::function<void(gdf_column*)> gdf_col_deleter = [](gdf_column* col){col->size = 0; cudaFree(col->data);};
using gdf_col_pointer = typename std::unique_ptr<gdf_column, decltype(gdf_col_deleter)>;

// Creates a gdf_column from a std::vector
template <typename col_type>
gdf_col_pointer create_gdf_column(std::vector<col_type> const & host_vector)
{

  // Create a new instance of a gdf_column with a custom deleter that will free
  // the associated device memory when it eventually goes out of scope
  gdf_col_pointer the_column{new gdf_column, gdf_col_deleter};
  // Allocate device storage for gdf_column and copy contents from host_vector
  cudaMalloc(&(the_column->data), host_vector.size() * sizeof(col_type));
  cudaMemcpy(the_column->data, host_vector.data(), host_vector.size() * sizeof(col_type), cudaMemcpyHostToDevice);

  // Deduce the type and set the gdf_dtype accordingly
  gdf_dtype gdf_col_type;
  if(std::is_same<col_type,int8_t>::value) gdf_col_type = GDF_INT8;
  else if(std::is_same<col_type,uint8_t>::value) gdf_col_type = GDF_INT8;
  else if(std::is_same<col_type,int16_t>::value) gdf_col_type = GDF_INT16;
  else if(std::is_same<col_type,uint16_t>::value) gdf_col_type = GDF_INT16;
  else if(std::is_same<col_type,int32_t>::value) gdf_col_type = GDF_INT32;
  else if(std::is_same<col_type,uint32_t>::value) gdf_col_type = GDF_INT32;
  else if(std::is_same<col_type,int64_t>::value) gdf_col_type = GDF_INT64;
  else if(std::is_same<col_type,uint64_t>::value) gdf_col_type = GDF_INT64;
  else if(std::is_same<col_type,float>::value) gdf_col_type = GDF_FLOAT32;
  else if(std::is_same<col_type,double>::value) gdf_col_type = GDF_FLOAT64;
  // Fill the gdf_column members
  the_column->valid = nullptr;
  the_column->size = host_vector.size();
  the_column->dtype = gdf_col_type;
  gdf_dtype_extra_info extra_info;
  extra_info.time_unit = TIME_UNIT_NONE;
  the_column->dtype_info = extra_info;
  return the_column;
}

// A new instance of this class will be created for each *TEST(GroupByTest, ...)
// Put all repeated stuff for each test here
template <typename test_parameters>
