/*
 *
 * This file is a part of the CaosDB Project.
 *
 * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
 * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include "caosdb/configuration.h"
#include "caosdb/status_code.h"    // for StatusCode
#include "caosdb_test_utility.h"   // for EXPECT_THROW_MESSAGE, TEST_DATA_DIR
#include "ccaosdb.h"               // for caosdb_utility_get_env_var
#include <cstring>                 // for strcmp
#include <gtest/gtest-message.h>   // for Message
#include <gtest/gtest-test-part.h> // for SuiteApiResolver, TestFactoryImpl
#include <gtest/gtest_pred_impl.h> // for Test, TestInfo, EXPECT_EQ, TEST
#include <iostream>
#include <string> // for allocator

class test_ccaosdb : public ::testing::Test {
protected:
  void SetUp() override {
    caosdb::configuration::ConfigurationManager::Clear();
    caosdb::configuration::ConfigurationManager::LoadSingleJSONConfiguration(
      TEST_DATA_DIR + "/test_caosdb_client.json");
  }

  void TearDown() override { caosdb::configuration::ConfigurationManager::Clear(); }
};

TEST_F(test_ccaosdb, test_get_env_var) {
  const char *const some_var = caosdb_utility_get_env_var("SOME_ENV_VAR", "fall-back");
  EXPECT_EQ("fall-back", some_var);
}

TEST_F(test_ccaosdb, test_other_client_error) {
  EXPECT_EQ(caosdb_status_code_OTHER_CLIENT_ERROR(), caosdb::StatusCode::OTHER_CLIENT_ERROR);
}

TEST_F(test_ccaosdb, test_get_default_connection) {
  caosdb_connection_connection out;

  caosdb_connection_connection_manager_get_default_connection(&out);
  EXPECT_TRUE(out.wrapped_connection);
}

TEST_F(test_ccaosdb, test_get_connection) {
  caosdb_connection_connection out;

  caosdb_connection_connection_manager_get_connection(&out, "local-caosdb-admin");
  EXPECT_TRUE(out.wrapped_connection);
}

TEST_F(test_ccaosdb, test_execute_transaction) {
  caosdb_connection_connection connection;
  caosdb_connection_connection_manager_get_connection(&connection, "local-caosdb-admin");

  caosdb_transaction_transaction transaction;
  caosdb_connection_connection_create_transaction(&connection, &transaction);

  EXPECT_TRUE(transaction.wrapped_transaction);

  int return_code(caosdb_transaction_transaction_retrieve_by_id(&transaction, "some_id"));
  EXPECT_EQ(return_code, caosdb::StatusCode::GO_ON);

  return_code = caosdb_transaction_transaction_execute(&transaction);
  EXPECT_EQ(return_code, caosdb::StatusCode::CONNECTION_ERROR);

  return_code = caosdb_transaction_delete_transaction(&transaction);
  EXPECT_EQ(return_code, 0);

  caosdb_transaction_transaction multi_transaction;
  caosdb_connection_connection_create_transaction(&connection, &multi_transaction);

  // We explicitely want to define a C-style array here, so we disable
  // linting
  const char *ids[] = {"id1", "id2", "id3"}; // NOLINT
  return_code = caosdb_transaction_transaction_retrieve_by_ids(&multi_transaction, ids, 3);
  EXPECT_EQ(return_code, caosdb::StatusCode::GO_ON);

  return_code = caosdb_transaction_delete_transaction(&multi_transaction);
  EXPECT_EQ(return_code, 0);
}

TEST_F(test_ccaosdb, test_multi_retrieve) {
  std::cout << "Entering test_multi_retrieve ..." << std::endl;
  caosdb_connection_connection connection;
  caosdb_connection_connection_manager_get_connection(&connection, "local-caosdb-admin");

  std::cout << "Creating transaction" << std::endl;
  caosdb_transaction_transaction multi_transaction;
  caosdb_connection_connection_create_transaction(&connection, &multi_transaction);

  // We explicitely want to define a C-style array here, so we disable
  // linting
  const char *ids[] = {"id1", "id2", "id3"}; // NOLINT
  std::cout << "Adding mutli retrieval ..." << std::endl;
  int return_code(caosdb_transaction_transaction_retrieve_by_ids(&multi_transaction, ids, 3));
  EXPECT_EQ(return_code, caosdb::StatusCode::GO_ON);

  std::cout << "Deleting transaction ..." << std::endl;
  return_code = caosdb_transaction_delete_transaction(&multi_transaction);
  EXPECT_EQ(return_code, 0);
}

TEST_F(test_ccaosdb, test_query) {
  caosdb_connection_connection connection;
  caosdb_connection_connection_manager_get_connection(&connection, "local-caosdb-admin");

  caosdb_transaction_transaction transaction;
  caosdb_connection_connection_create_transaction(&connection, &transaction);

  int return_code(caosdb_transaction_transaction_query(&transaction, "FIND ENTITY WITH id=123"));
  EXPECT_EQ(return_code, caosdb::StatusCode::GO_ON);

  return_code = caosdb_transaction_delete_transaction(&transaction);
  EXPECT_EQ(return_code, 0);
}

TEST_F(test_ccaosdb, test_entity) {
  caosdb_entity_entity entity;

  int return_code(caosdb_entity_create_entity(&entity));
  EXPECT_EQ(return_code, 0);

  // cannot be created again without deletion
  return_code = caosdb_entity_create_entity(&entity);
  EXPECT_EQ(return_code, caosdb::StatusCode::EXTERN_C_ASSIGNMENT_ERROR);

  // deletion and re-creation is ok
  return_code = caosdb_entity_delete_entity(&entity);
  EXPECT_EQ(return_code, 0);
  return_code = caosdb_entity_create_entity(&entity);
  EXPECT_EQ(return_code, 0);

  // In-depth check for one pair of setter and getter, just compare
  // the strings for the rest
  return_code = caosdb_entity_entity_set_name(&entity, "length");
  EXPECT_EQ(return_code, 0);
  char out[255] = {"a"}; // NOLINT
  return_code = caosdb_entity_entity_get_name(&entity, out);
  EXPECT_EQ(return_code, 0);
  EXPECT_EQ(strcmp(out, "length"), 0);

  // TODO(fspreck)
  // caosdb_entity_entity_set_role(&entity, "Property");
  // caosdb_entity_entity_get_role(&entity, out);
  // EXPECT_EQ(strcmp(out, "Property"), 0);

  caosdb_entity_entity_set_description(&entity, "The length of an object");
  caosdb_entity_entity_get_description(&entity, out);
  EXPECT_EQ(strcmp(out, "The length of an object"), 0);

  // TODO(fspreck)
  // caosdb_entity_entity_set_datatype(&entity, "DOUBLE");
  // caosdb_entity_entity_get_datatype(&entity, out);
  // EXPECT_EQ(strcmp(out, "DOUBLE"), 0);

  caosdb_entity_entity_set_unit(&entity, "m");
  caosdb_entity_entity_get_unit(&entity, out);
  EXPECT_EQ(strcmp(out, "m"), 0);

  // TODO(fspreck)
  // caosdb_entity_entity_set_value(&entity, "5.0");
  // caosdb_entity_entity_get_value(&entity, out);
  // EXPECT_EQ(strcmp(out, "5.0"), 0);

  return_code = caosdb_entity_delete_entity(&entity);
  EXPECT_EQ(return_code, 0);
}

TEST_F(test_ccaosdb, test_parent) {
  caosdb_entity_parent parent;

  int return_code(caosdb_entity_create_parent(&parent));
  EXPECT_EQ(return_code, 0);

  caosdb_entity_parent_set_id(&parent, "some_id");
  caosdb_entity_parent_set_name(&parent, "some_name");

  char out[255] = {"a"}; // NOLINT
  caosdb_entity_parent_get_id(&parent, out);
  EXPECT_EQ(strcmp(out, "some_id"), 0);

  caosdb_entity_parent_get_name(&parent, out);
  EXPECT_EQ(strcmp(out, "some_name"), 0);

  return_code = caosdb_entity_delete_parent(&parent);
  EXPECT_EQ(return_code, 0);
}

TEST_F(test_ccaosdb, test_property) {
  caosdb_entity_property property;

  int return_code(caosdb_entity_create_property(&property));
  EXPECT_EQ(return_code, 0);

  caosdb_entity_property_set_id(&property, "some_id");
  caosdb_entity_property_set_name(&property, "some_name");
  // TODO(fspreck)
  // caosdb_entity_property_set_datatype(&property, "some_datatype");
  // TODO(fspreck)
  // caosdb_entity_property_set_importance(&property, "some_importance");
  caosdb_entity_property_set_unit(&property, "some_unit");
  // TODO(fspreck)
  // caosdb_entity_property_set_value(&property, "some_value");

  char out[255] = {"a"}; // NOLINT
  caosdb_entity_property_get_id(&property, out);
  EXPECT_EQ(strcmp(out, "some_id"), 0);

  caosdb_entity_property_get_name(&property, out);
  EXPECT_EQ(strcmp(out, "some_name"), 0);

  // TODO(fspreck)
  // caosdb_entity_property_get_datatype(&property, out);
  // EXPECT_EQ(strcmp(out, "some_datatype"), 0);

  // TODO(fspreck)
  // caosdb_entity_property_get_importance(&property, out);
  // EXPECT_EQ(strcmp(out, "some_importance"), 0);

  caosdb_entity_property_get_unit(&property, out);
  EXPECT_EQ(strcmp(out, "some_unit"), 0);

  // TODO(fspreck)
  // caosdb_entity_property_get_value(&property, out);
  // EXPECT_EQ(strcmp(out, "some_value"), 0);

  return_code = caosdb_entity_delete_property(&property);
  EXPECT_EQ(return_code, 0);
}

TEST_F(test_ccaosdb, test_entity_with_parent_and_property) {
  std::cout << "Creating objects ... " << std::endl;
  caosdb_entity_parent input_parent;
  int return_code(caosdb_entity_create_parent(&input_parent));
  EXPECT_EQ(return_code, 0);

  caosdb_entity_parent_set_id(&input_parent, "parent_id");
  caosdb_entity_parent_set_name(&input_parent, "parent_name");

  caosdb_entity_property input_property;
  return_code = caosdb_entity_create_property(&input_property);
  EXPECT_EQ(return_code, 0);

  caosdb_entity_property_set_id(&input_property, "property_id");
  caosdb_entity_property_set_name(&input_property, "property_name");
  // TODO(fspreck)
  // caosdb_entity_property_set_datatype(&input_property, "property_datatype");
  // TODO(fspreck)
  // caosdb_entity_property_set_value(&input_property, "property_value");

  caosdb_entity_entity entity;
  return_code = caosdb_entity_create_entity(&entity);
  EXPECT_EQ(return_code, 0);

  std::cout << "Appending parent and property ..." << std::endl;
  return_code = caosdb_entity_entity_append_parent(&entity, &input_parent);
  EXPECT_EQ(return_code, 0);

  return_code = caosdb_entity_entity_append_property(&entity, &input_property);
  EXPECT_EQ(return_code, 0);

  std::cout << "Counting parents and properties ..." << std::endl;
  int count[] = {0}; // NOLINT
  return_code = caosdb_entity_entity_get_parents_size(&entity, count);
  EXPECT_EQ(return_code, 0);
  EXPECT_EQ(*count, 1);

  return_code = caosdb_entity_entity_get_properties_size(&entity, count);
  EXPECT_EQ(return_code, 0);
  EXPECT_EQ(*count, 1);

  char in[255] = {"a"};  // NOLINT
  char out[255] = {"b"}; // NOLINT

  std::cout << "Comparing ..." << std::endl;
  // cannot assign an already assigned property
  return_code = caosdb_entity_entity_get_property(&entity, &input_property, 0);
  EXPECT_EQ(return_code, caosdb::StatusCode::EXTERN_C_ASSIGNMENT_ERROR);
  caosdb_entity_property output_property;
  return_code = caosdb_entity_entity_get_property(&entity, &output_property, 0);
  std::cout << "Got output property." << std::endl;
  EXPECT_EQ(return_code, 0);

  caosdb_entity_property_get_id(&input_property, in);
  std::cout << "Got input id." << std::endl;
  caosdb_entity_property_get_id(&output_property, out);
  std::cout << "Got output id." << std::endl;
  EXPECT_EQ(strcmp(in, out), 0);

  caosdb_entity_property_get_name(&input_property, in);
  caosdb_entity_property_get_name(&output_property, out);
  EXPECT_EQ(strcmp(in, out), 0);

  // TODO(fspreck)
  // caosdb_entity_property_get_datatype(&input_property, in);
  // caosdb_entity_property_get_datatype(&output_property, out);
  // EXPECT_EQ(strcmp(in, out), 0);

  // TODO(fspreck)
  // caosdb_entity_property_get_value(&input_property, in);
  // caosdb_entity_property_get_value(&output_property, out);
  // EXPECT_EQ(strcmp(in, out), 0);

  std::cout << "Comparing parent..." << std::endl;
  caosdb_entity_parent output_parent;
  return_code = caosdb_entity_entity_get_parent(&entity, &output_parent, 0);
  std::cout << "Got output parent." << std::endl;
  EXPECT_EQ(return_code, 0);

  caosdb_entity_parent_get_id(&input_parent, in);
  caosdb_entity_parent_get_id(&output_parent, out);
  EXPECT_EQ(strcmp(in, out), 0);

  caosdb_entity_parent_get_name(&input_parent, in);
  caosdb_entity_parent_get_name(&output_parent, out);
  EXPECT_EQ(strcmp(in, out), 0);

  // Delete everything
  std::cout << "Deleting ..." << std::endl;
  return_code = caosdb_entity_delete_parent(&input_parent);
  EXPECT_EQ(return_code, 0);
  return_code = caosdb_entity_delete_property(&input_property);
  EXPECT_EQ(return_code, 0);
  return_code = caosdb_entity_delete_entity(&entity);
  EXPECT_EQ(return_code, 0);

  // This tests the `_deletable` flag. The wrapped cpp objects of
  // `output_parent` and `output_property` are owned by the entity, so
  // they have been deleted together with the entity. With a wrong
  // `_deletable` flag, the following would cause segfaults.
  //
  return_code = caosdb_entity_delete_parent(&output_parent);
  EXPECT_EQ(return_code, 0);
  return_code = caosdb_entity_delete_property(&output_property);
  EXPECT_EQ(return_code, 0);
}

TEST_F(test_ccaosdb, test_remove_property) {
  caosdb_entity_entity entity;
  int return_code(caosdb_entity_create_entity(&entity));
  EXPECT_EQ(return_code, 0);

  // Create two properties with names
  caosdb_entity_property in_prop_1;
  return_code = caosdb_entity_create_property(&in_prop_1);
  EXPECT_EQ(return_code, 0);
  return_code = caosdb_entity_property_set_name(&in_prop_1, "Property 1");
  EXPECT_EQ(return_code, 0);

  caosdb_entity_property in_prop_2;
  return_code = caosdb_entity_create_property(&in_prop_2);
  EXPECT_EQ(return_code, 0);
  return_code = caosdb_entity_property_set_name(&in_prop_2, "Property 2");
  EXPECT_EQ(return_code, 0);

  // Append them
  return_code = caosdb_entity_entity_append_property(&entity, &in_prop_1);
  EXPECT_EQ(return_code, 0);
  return_code = caosdb_entity_entity_append_property(&entity, &in_prop_2);
  EXPECT_EQ(return_code, 0);

  // Delete one and see that the number of properties decreases by one
  int count[] = {0}; // NOLINT
  return_code = caosdb_entity_entity_get_properties_size(&entity, count);
  EXPECT_EQ(return_code, 0);
  EXPECT_EQ(*count, 2);

  return_code = caosdb_entity_entity_remove_property(&entity, 0);
  EXPECT_EQ(return_code, 0);

  return_code = caosdb_entity_entity_get_properties_size(&entity, count);
  EXPECT_EQ(return_code, 0);
  EXPECT_EQ(*count, 1);

  caosdb_entity_property out_prop;
  return_code = caosdb_entity_entity_get_property(&entity, &out_prop, 0);
  EXPECT_EQ(return_code, 0);

  char in[255] = {"a"};  // NOLINT
  char out[255] = {"b"}; // NOLINT

  // Deleted the first property, so the second one should remain.
  return_code = caosdb_entity_property_get_name(&in_prop_2, in);
  EXPECT_EQ(return_code, 0);
  return_code = caosdb_entity_property_get_name(&out_prop, out);
  EXPECT_EQ(return_code, 0);

  EXPECT_EQ(strcmp(in, out), 0);

  // Delete everything we have created
  return_code = caosdb_entity_delete_property(&in_prop_2);
  EXPECT_EQ(return_code, 0);

  return_code = caosdb_entity_delete_property(&in_prop_1);
  EXPECT_EQ(return_code, 0);

  return_code = caosdb_entity_delete_entity(&entity);
  EXPECT_EQ(return_code, 0);
}