/*
 *
 * This file is a part of the CaosDB Project.
 *
 * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
 * Copyright (C) 2021 Florian Spreckelsen <f.spreckelsen@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_test_utility.h"
#include "caosdb/data_type.h"                    // for DataType, AtomicDat...
#include "caosdb/entity.h"                       // for Entity, Parent, Par...
#include "caosdb/entity/v1alpha1/main.grpc.pb.h" // for EntityTransactionSe...
#include "caosdb/entity/v1alpha1/main.pb.h"      // for IdResponse, Message
#include "caosdb/message_code.h"                 // for MessageCode
#include "caosdb/protobuf_helper.h"              // for get_arena
#include "caosdb/status_code.h"                  // for StatusCode, FILE_DO...
#include "caosdb/transaction.h"                  // for Transaction
#include "caosdb/value.h"                        // for Value
#include <exception>
#include <google/protobuf/arena.h> // for Arena
#include <gtest/gtest-message.h>   // for Message
#include <gtest/gtest-test-part.h> // for TestPartResult, Sui...
#include <gtest/gtest_pred_impl.h> // for Test, EXPECT_EQ
#include <iostream>
#include <memory> // for allocator, shared_ptr
#include <stdexcept>
#include <string> // for operator+, string

namespace caosdb::entity {
using caosdb::entity::v1alpha1::IdResponse;
using ProtoEntity = caosdb::entity::v1alpha1::Entity;
using ProtoParent = caosdb::entity::v1alpha1::Parent;
using caosdb::utility::get_arena;

TEST(test_entity, test_parent_setters) {
  auto parent = Parent();
  parent.SetName("RT1");
  parent.SetId("some-id");

  EXPECT_EQ(parent.GetName(), "RT1");
  EXPECT_EQ(parent.GetId(), "some-id");
}

TEST(test_entity, test_append_parent) {
  auto parent = Parent();
  parent.SetId("some-id");

  auto entity = Entity();
  EXPECT_EQ(entity.GetParents().size(), 0);
  entity.AppendParent(parent);
  EXPECT_EQ(entity.GetParents().size(), 1);

  auto same_parent = entity.GetParents().at(0);
  EXPECT_EQ(same_parent.GetId(), "some-id");
}

TEST(test_entity, test_property_setters) {
  auto prop = Property();
  prop.SetName("prop_name");
  prop.SetId("prop_id");
  prop.SetImportance(Importance::OBLIGATORY);
  prop.SetValue("prop_value");
  prop.SetUnit("prop_unit");
  prop.SetDataType("prop_dtype");

  EXPECT_EQ(prop.GetName(), "prop_name");
  EXPECT_EQ(prop.GetId(), "prop_id");
  EXPECT_EQ(prop.GetImportance(), Importance::OBLIGATORY);
  EXPECT_TRUE(prop.GetValue().IsString());
  EXPECT_EQ(prop.GetValue().AsString(), "prop_value");
  EXPECT_EQ(prop.GetUnit(), "prop_unit");
  EXPECT_TRUE(prop.GetDataType().IsReference());
  EXPECT_EQ(prop.GetDataType().AsReference().GetName(), "prop_dtype");
  EXPECT_FALSE(prop.GetDataType().IsList());

  prop.SetDataType(AtomicDataType::DATETIME);
  EXPECT_TRUE(prop.GetDataType().IsAtomic());
  EXPECT_EQ(prop.GetDataType().AsAtomic(), AtomicDataType::DATETIME);
  EXPECT_FALSE(prop.GetDataType().IsList());
}

TEST(test_entity, test_list_property_setters) {
  auto prop = Property();

  prop.SetDataType(AtomicDataType::DATETIME); // Set as atomic first.
  EXPECT_TRUE(prop.GetDataType().IsAtomic());
  EXPECT_EQ(prop.GetDataType().AsAtomic(), AtomicDataType::DATETIME);

  prop.SetDataType(AtomicDataType::DOUBLE, true);
  auto const & dtype = prop.GetDataType();
  EXPECT_FALSE(dtype.IsAtomic()); // Should not be true anymore.
  EXPECT_FALSE(dtype.IsReference());
  EXPECT_TRUE(dtype.IsList());
  EXPECT_NE(dtype.AsAtomic(), AtomicDataType::DATETIME); // Should be overwritten.
  EXPECT_TRUE(dtype.AsList().IsListOfAtomic());
  EXPECT_EQ(dtype.AsList().GetAtomicDataType(),AtomicDataType::DOUBLE);
}

TEST(test_entity, test_append_property) {
  auto entity = Entity();

  auto prop = Property();
  prop.SetName("prop_name");
  prop.SetId("prop_id");
  prop.SetImportance(Importance::RECOMMENDED);
  prop.SetValue("prop_value");
  prop.SetUnit("prop_unit");
  prop.SetDataType("prop_dtype");

  EXPECT_EQ(entity.GetProperties().size(), 0);
  entity.AppendProperty(prop);
  EXPECT_EQ(entity.GetProperties().size(), 1);

  // also test RepeatedPtrFieldWrapper.at()
  const auto &same_prop = entity.GetProperties().at(0);
  EXPECT_THROW((void)entity.GetProperties().at(2), std::out_of_range);
  EXPECT_THROW((void)entity.GetProperties().at(-1), std::out_of_range);

  EXPECT_EQ(prop.GetName(), same_prop.GetName());
  EXPECT_EQ(prop.GetId(), same_prop.GetId());
  EXPECT_EQ(prop.GetImportance(), same_prop.GetImportance());
  EXPECT_EQ(prop.GetValue(), same_prop.GetValue());
  EXPECT_EQ(prop.GetUnit(), same_prop.GetUnit());
  EXPECT_EQ(prop.GetDataType(), same_prop.GetDataType());
}

TEST(test_entity, test_copy_to) {
  auto entity = Entity();
  entity.SetRole(Role::RECORD);
  entity.SetName("orignial_name");

  auto parent = Parent();
  parent.SetId("parent_id");
  parent.SetName("parent_name");
  entity.AppendParent(parent);

  auto prop = Property();
  prop.SetId("prop_id");
  prop.SetName("prop_name");
  entity.AppendProperty(prop);

  // create protobuf entity to which all fields ae copied and then a
  // CaoosDB entity from that protobuf entity.
  auto *proto_copy =
    google::protobuf::Arena::CreateMessage<ProtoEntity>(get_arena());
  entity.CopyTo(proto_copy);
  auto copied = Entity(proto_copy);

  EXPECT_EQ(entity.GetRole(), copied.GetRole());
  EXPECT_EQ(entity.GetName(), copied.GetName());
  EXPECT_EQ(copied.GetParents().at(0).GetId(), parent.GetId());
  EXPECT_EQ(copied.GetParents().at(0).GetName(), parent.GetName());
  EXPECT_EQ(copied.GetProperties().at(0).GetId(), prop.GetId());
  EXPECT_EQ(copied.GetProperties().at(0).GetName(), prop.GetName());
}

TEST(test_entity, test_insert_entity) {
  auto transaction = caosdb::transaction::Transaction(
    std::shared_ptr<transaction::EntityTransactionService::Stub>(nullptr),
    std::shared_ptr<transaction::FileTransmissionService::Stub>(nullptr));

  auto entity = Entity();
  entity.SetRole(Role::RECORD_TYPE);
  entity.SetName("entity_name");

  EXPECT_EQ(entity.GetRole(), Role::RECORD_TYPE);
  EXPECT_EQ(entity.GetName(), "entity_name");

  transaction.InsertEntity(&entity);

  EXPECT_EQ(entity.GetRole(), Role::RECORD_TYPE);
  EXPECT_EQ(entity.GetName(), "entity_name");
}

TEST(test_entity, test_insert_with_role) {
  auto transaction = caosdb::transaction::Transaction(
    std::shared_ptr<transaction::EntityTransactionService::Stub>(nullptr),
    std::shared_ptr<transaction::FileTransmissionService::Stub>(nullptr));

  auto entity = Entity();
  entity.SetRole(Role::PROPERTY);
  entity.SetDataType(AtomicDataType::DOUBLE);
  entity.SetName("Length");
  entity.SetUnit("m");
  entity.SetValue(5.5);

  transaction.InsertEntity(&entity);

  EXPECT_EQ(entity.GetRole(), Role::PROPERTY);
  EXPECT_TRUE(entity.GetDataType().IsAtomic());
  EXPECT_EQ(entity.GetDataType().AsAtomic(), AtomicDataType::DOUBLE);
  EXPECT_EQ(entity.GetName(), "Length");
  EXPECT_EQ(entity.GetUnit(), "m");
  EXPECT_TRUE(entity.GetValue().IsDouble());
  EXPECT_DOUBLE_EQ(entity.GetValue().AsDouble(), 5.5);
}

TEST(test_entity, test_insert_with_parent) {
  auto transaction = caosdb::transaction::Transaction(
    std::shared_ptr<transaction::EntityTransactionService::Stub>(nullptr),
    std::shared_ptr<transaction::FileTransmissionService::Stub>(nullptr));

  auto entity = Entity();
  entity.SetName("entity_name");

  auto parent = Parent();
  parent.SetId("parent_id");
  parent.SetName("parent_name");
  EXPECT_EQ(parent.GetId(), "parent_id");
  EXPECT_EQ(parent.GetName(), "parent_name");

  entity.AppendParent(parent);

  transaction.InsertEntity(&entity);

  EXPECT_EQ(entity.GetName(), "entity_name");
  EXPECT_EQ(entity.GetParents().size(), 1);
  auto inserted_parent = entity.GetParents().at(0);
  EXPECT_EQ(inserted_parent.GetId(), parent.GetId());
  EXPECT_EQ(inserted_parent.GetName(), parent.GetName());
}

TEST(test_entity, test_insert_with_property) {
  auto transaction = caosdb::transaction::Transaction(
    std::shared_ptr<transaction::EntityTransactionService::Stub>(nullptr),
    std::shared_ptr<transaction::FileTransmissionService::Stub>(nullptr));

  auto entity = Entity();
  entity.SetName("entity_name");

  auto prop = Property();
  prop.SetName("prop_name");
  prop.SetId("prop_id");
  prop.SetImportance(Importance::FIX);
  prop.SetValue(Value("prop_value"));
  prop.SetUnit("prop_unit");
  prop.SetDataType("prop_dtype");

  entity.AppendProperty(prop);

  transaction.InsertEntity(&entity);

  EXPECT_EQ(entity.GetProperties().size(), 1);

  const auto &inserted_prop = entity.GetProperties().at(0);

  EXPECT_EQ(prop.GetName(), inserted_prop.GetName());
  EXPECT_EQ(prop.GetId(), inserted_prop.GetId());
  EXPECT_EQ(prop.GetImportance(), inserted_prop.GetImportance());
  EXPECT_EQ(prop.GetValue().ToString(), inserted_prop.GetValue().ToString());
  EXPECT_EQ(prop.GetValue(), inserted_prop.GetValue());
  EXPECT_EQ(prop.GetUnit(), inserted_prop.GetUnit());
  EXPECT_EQ(prop.GetDataType(), inserted_prop.GetDataType());
}

TEST(test_entity, test_from_id_response) {
  IdResponse idResponse;
  idResponse.set_id("entity_id");
  auto *error = idResponse.add_errors();
  error->set_code(MessageCode::ENTITY_DOES_NOT_EXIST);
  error->set_description("error_desc");

  Entity entity(&idResponse);

  EXPECT_EQ(entity.GetId(), "entity_id");
  EXPECT_TRUE(entity.HasErrors());
  EXPECT_EQ(entity.GetErrors().size(), 1);
  EXPECT_EQ(entity.GetErrors().at(0).GetDescription(), "error_desc");
  EXPECT_EQ(entity.GetErrors().at(0).GetCode(),
            MessageCode::ENTITY_DOES_NOT_EXIST);

  IdResponse idr_warnings_and_infos;
  idr_warnings_and_infos.set_id("other_entity_id");
  auto *warning = idr_warnings_and_infos.add_warnings();
  warning->set_description("warning_desc");
  warning->set_code(MessageCode::ENTITY_HAS_NO_PROPERTIES);
  auto *info = idr_warnings_and_infos.add_infos();
  info->set_description("info_desc");
  info->set_code(MessageCode::UNSPECIFIED);

  Entity other_ent(&idr_warnings_and_infos);

  EXPECT_EQ(other_ent.GetId(), "other_entity_id");
  EXPECT_EQ(other_ent.GetWarnings().size(), 1);
  EXPECT_TRUE(other_ent.HasWarnings());
  EXPECT_EQ(other_ent.GetWarnings().at(0).GetDescription(), "warning_desc");
  EXPECT_EQ(other_ent.GetWarnings().at(0).GetCode(),
            MessageCode::ENTITY_HAS_NO_PROPERTIES);
  EXPECT_EQ(other_ent.GetInfos().size(), 1);
  EXPECT_EQ(other_ent.GetInfos().at(0).GetDescription(), "info_desc");
  EXPECT_EQ(other_ent.GetInfos().at(0).GetCode(), MessageCode::UNSPECIFIED);
}

TEST(test_entity, test_add_file_to_non_file_entity) {
  Entity entity;
  EXPECT_EQ(entity.SetLocalPath("local/path"), StatusCode::NOT_A_FILE_ENTITY);
}

TEST(test_entity, test_add_non_existing_file) {
  Entity entity;
  entity.SetRole(Role::FILE);
  EXPECT_EQ(entity.SetLocalPath("non-existing/path"),
            StatusCode::FILE_DOES_NOT_EXIST_LOCALLY);
}

TEST(test_entity, test_add_directory_path) {
  Entity entity;
  entity.SetRole(Role::FILE);
  EXPECT_EQ(entity.SetLocalPath("./"), StatusCode::PATH_IS_A_DIRECTORY);
}

TEST(test_entity, test_remove_property) {
  Entity entity;
  for (int i = 0; i < 5; i++) {
    auto name = "PROPERTY-" + std::to_string(i);

    Property property;
    property.SetName(name);
    EXPECT_EQ(property.GetName(), name);

    entity.AppendProperty(property);
    EXPECT_EQ(property.GetName(), name);

    // not initializing the cache
  }
  ASSERT_EQ(entity.GetProperties().size(), 5);
  for (int i = 5; i < 10; i++) {
    auto name = "PROPERTY-" + std::to_string(i);

    Property property;
    property.SetName(name);
    EXPECT_EQ(property.GetName(), name);

    entity.AppendProperty(property);
    EXPECT_EQ(property.GetName(), name);

    // initializing the cache
    const auto &property_2 = entity.GetProperties().at(i);
    EXPECT_EQ(property_2.GetName(), name);
  }

  ASSERT_EQ(entity.GetProperties().size(), 10);

  for (int i = 5; i < 10; i++) {
    // double checking the cache
    auto name = "PROPERTY-" + std::to_string(i);
    const auto &property = entity.GetProperties().at(i);
    EXPECT_EQ(property.GetName(), name);
  }

  // Remove at index 3
  // P0,P1,P2,P3,P4,P5,P6,P7,P8,P9
  //          ^
  // P0,P1,P2,   P4,P5,P6,P7,P8,P9
  entity.RemoveProperty(3);

  // Remove at index 6
  // P0,P1,P2,   P4,P5,P6,P7,P8,P9
  //                      ^
  // P0,P1,P2,   P4,P5,P6,   P8,P9
  entity.RemoveProperty(6);
  ASSERT_EQ(entity.GetProperties().size(), 8);

  // AppendProperty another property
  // P0,P1,P2,   P4,P5,P6,   P8,P9
  //                               ^
  // P0,P1,P2,   P4,P5,P6,   P8,P9,P10
  Property property10;
  property10.SetName("PROPERTY-10");
  entity.AppendProperty(property10);
  ASSERT_EQ(entity.GetProperties().size(), 9);

  std::cout << "[" << std::endl;
  for (int i = 0; i < 9; i++) {
    std::cout << "  " << entity.GetProperties().at(i).GetName() << ",\n";
  }
  std::cout << "]" << std::endl;

  for (int i = 0; i < 3; i++) {
    auto name = "PROPERTY-" + std::to_string(i);
    const auto &property = entity.GetProperties().at(i);
    EXPECT_EQ(property.GetName(), name);
  }
  for (int i = 3; i < 6; i++) {
    auto name = "PROPERTY-" + std::to_string(i + 1);
    const auto &property = entity.GetProperties().at(i);
    EXPECT_EQ(property.GetName(), name);
  }
  for (int i = 6; i < 9; i++) {
    auto name = "PROPERTY-" + std::to_string(i + 2);
    const auto &property = entity.GetProperties().at(i);
    EXPECT_EQ(property.GetName(), name);
  }
}

TEST(test_entity, test_property_iterator) {
  Entity entity;
  for (int i = 0; i < 5; i++) {
    auto name = "PROPERTY-" + std::to_string(i);

    Property property;
    property.SetName(name);
    EXPECT_EQ(property.GetName(), name);

    entity.AppendProperty(property);
  }
  ASSERT_EQ(entity.GetProperties().size(), 5);
  int counter = 0;
  for (auto &property : entity.GetProperties()) {
    // TODO(tf) Copy constructor was deleted
    // auto nonref_property = entity.GetProperties().at(counter);
    auto name = "PROPERTY-" + std::to_string(counter);
    EXPECT_EQ(property.GetName(), name);
    // EXPECT_EQ(nonref_property.GetName(), name);
    counter++;
  }
  EXPECT_EQ(counter, 5);
}

TEST(test_entity, test_description) {
  Entity entity;
  Property property;
  Parent parent;

  EXPECT_EQ(entity.GetDescription(), "");
  EXPECT_EQ(property.GetDescription(), "");
  EXPECT_EQ(parent.GetDescription(), "");

  entity.SetDescription("desc entity");
  property.SetDescription("desc property");
  // Parent has not setter
  ProtoParent protoParent;
  protoParent.set_description("desc parent");
  parent = Parent(&protoParent);

  EXPECT_EQ(entity.GetDescription(), "desc entity");
  EXPECT_EQ(property.GetDescription(), "desc property");
  EXPECT_EQ(parent.GetDescription(), "desc parent");
}

TEST(test_entity, test_role) {
  Entity entity;
  entity.SetRole(Role::RECORD_TYPE);

  EXPECT_EQ(entity.GetRole(), Role::RECORD_TYPE);
}

TEST(test_entity, test_add_file) {
  Entity entity;
  entity.SetRole(Role::FILE);
  EXPECT_EQ(entity.SetLocalPath(TEST_DATA_DIR + "/test.json"),
            StatusCode::SUCCESS);
}

} // namespace caosdb::entity