// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

#include <gtest/gtest.h>

#include <string>

#include "parquet/encryption/encryption.h"
#include "parquet/encryption/test_encryption_util.h"

namespace parquet::encryption::test {

TEST(TestColumnEncryptionProperties, ColumnEncryptedWithOwnKey) {
  ColumnEncryptionProperties::Builder column_builder_1;
  column_builder_1.key(kColumnEncryptionKey1);
  column_builder_1.key_id("kc1");
  std::shared_ptr<ColumnEncryptionProperties> column_props_1 = column_builder_1.build();

  ASSERT_EQ(true, column_props_1->is_encrypted());
  ASSERT_EQ(false, column_props_1->is_encrypted_with_footer_key());
  ASSERT_EQ(kColumnEncryptionKey1, column_props_1->key());
  ASSERT_EQ("kc1", column_props_1->key_metadata());

  // Same with shorthand API
  column_props_1 =
      ColumnEncryptionProperties::WithColumnKey(kColumnEncryptionKey1, "kc1");
  ASSERT_EQ(true, column_props_1->is_encrypted());
  ASSERT_EQ(false, column_props_1->is_encrypted_with_footer_key());
  ASSERT_EQ(kColumnEncryptionKey1, column_props_1->key());
  ASSERT_EQ("kc1", column_props_1->key_metadata());
}

TEST(TestColumnEncryptionProperties, ColumnEncryptedWithFooterKey) {
  ColumnEncryptionProperties::Builder column_builder_1;
  std::shared_ptr<ColumnEncryptionProperties> column_props_1 = column_builder_1.build();

  ASSERT_EQ(true, column_props_1->is_encrypted());
  ASSERT_EQ(true, column_props_1->is_encrypted_with_footer_key());

  // Same with shorthand API
  column_props_1 = ColumnEncryptionProperties::WithFooterKey();
  ASSERT_EQ(true, column_props_1->is_encrypted());
  ASSERT_EQ(true, column_props_1->is_encrypted_with_footer_key());
}

TEST(TestColumnEncryptionProperties, UnencryptedColumn) {
  ColumnEncryptionProperties::Builder column_builder_1;

  auto column_props_1 = ColumnEncryptionProperties::Unencrypted();
  ASSERT_EQ(false, column_props_1->is_encrypted());
  ASSERT_EQ(false, column_props_1->is_encrypted_with_footer_key());
}

// Encrypt all columns and the footer with the same key.
// (uniform encryption)
TEST(TestEncryptionProperties, UniformEncryption) {
  FileEncryptionProperties::Builder builder(kFooterEncryptionKey);
  builder.footer_key_metadata("kf");
  std::shared_ptr<FileEncryptionProperties> props = builder.build();

  ASSERT_EQ(true, props->encrypted_footer());
  ASSERT_EQ(kDefaultEncryptionAlgorithm, props->algorithm().algorithm);
  ASSERT_EQ(kFooterEncryptionKey, props->footer_key());
  ASSERT_EQ("kf", props->footer_key_metadata());

  std::shared_ptr<parquet::schema::ColumnPath> column_path =
      parquet::schema::ColumnPath::FromDotString("a_column");
  std::shared_ptr<ColumnEncryptionProperties> out_col_props =
      props->column_encryption_properties(column_path->ToDotString());

  ASSERT_EQ(true, out_col_props->is_encrypted());
  ASSERT_EQ(true, out_col_props->is_encrypted_with_footer_key());
}

// Encrypt two columns with their own keys and the same key for
// the footer and other columns
TEST(TestEncryptionProperties, EncryptFooterAndTwoColumns) {
  std::shared_ptr<parquet::schema::ColumnPath> column_path_1 =
      parquet::schema::ColumnPath::FromDotString("column_1");
  std::shared_ptr<parquet::schema::ColumnPath> column_path_2 =
      parquet::schema::ColumnPath::FromDotString("column_2");

  std::map<std::string, std::shared_ptr<ColumnEncryptionProperties>> encrypted_columns;
  encrypted_columns[column_path_1->ToDotString()] =
      ColumnEncryptionProperties::WithColumnKey(kColumnEncryptionKey1, "kc1");
  encrypted_columns[column_path_2->ToDotString()] =
      ColumnEncryptionProperties::WithColumnKey(kColumnEncryptionKey2, "kc2");

  FileEncryptionProperties::Builder builder(kFooterEncryptionKey);
  builder.footer_key_metadata("kf");
  builder.encrypted_columns(std::move(encrypted_columns));
  std::shared_ptr<FileEncryptionProperties> props = builder.build();

  ASSERT_EQ(true, props->encrypted_footer());
  ASSERT_EQ(kDefaultEncryptionAlgorithm, props->algorithm().algorithm);
  ASSERT_EQ(kFooterEncryptionKey, props->footer_key());

  std::shared_ptr<ColumnEncryptionProperties> out_col_props_1 =
      props->column_encryption_properties(column_path_1->ToDotString());

  ASSERT_EQ(true, out_col_props_1->is_encrypted());
  ASSERT_EQ(false, out_col_props_1->is_encrypted_with_footer_key());
  ASSERT_EQ(kColumnEncryptionKey1, out_col_props_1->key());
  ASSERT_EQ("kc1", out_col_props_1->key_metadata());

  std::shared_ptr<ColumnEncryptionProperties> out_col_props_2 =
      props->column_encryption_properties(column_path_2->ToDotString());

  ASSERT_EQ(true, out_col_props_2->is_encrypted());
  ASSERT_EQ(false, out_col_props_2->is_encrypted_with_footer_key());
  ASSERT_EQ(kColumnEncryptionKey2, out_col_props_2->key());
  ASSERT_EQ("kc2", out_col_props_2->key_metadata());

  std::shared_ptr<parquet::schema::ColumnPath> column_path_3 =
      parquet::schema::ColumnPath::FromDotString("column_3");
  std::shared_ptr<ColumnEncryptionProperties> out_col_props_3 =
      props->column_encryption_properties(column_path_3->ToDotString());

  ASSERT_EQ(NULLPTR, out_col_props_3);
}

// Encryption configuration 3: Encrypt two columns, don’t encrypt footer.
// (plaintext footer mode, readable by legacy readers)
TEST(TestEncryptionProperties, EncryptTwoColumnsNotFooter) {
  std::shared_ptr<parquet::schema::ColumnPath> column_path_1 =
      parquet::schema::ColumnPath::FromDotString("column_1");
  std::shared_ptr<parquet::schema::ColumnPath> column_path_2 =
      parquet::schema::ColumnPath::FromDotString("column_2");

  std::map<std::string, std::shared_ptr<ColumnEncryptionProperties>> encrypted_columns;
  encrypted_columns[column_path_1->ToDotString()] =
      ColumnEncryptionProperties::WithColumnKey(kColumnEncryptionKey1, "kc1");
  encrypted_columns[column_path_2->ToDotString()] =
      ColumnEncryptionProperties::WithColumnKey(kColumnEncryptionKey2, "kc2");

  FileEncryptionProperties::Builder builder(kFooterEncryptionKey);
  builder.footer_key_metadata("kf");
  builder.set_plaintext_footer();
  builder.encrypted_columns(std::move(encrypted_columns));
  std::shared_ptr<FileEncryptionProperties> props = builder.build();

  ASSERT_EQ(false, props->encrypted_footer());
  ASSERT_EQ(kDefaultEncryptionAlgorithm, props->algorithm().algorithm);
  ASSERT_EQ(kFooterEncryptionKey, props->footer_key());

  std::shared_ptr<ColumnEncryptionProperties> out_col_props_1 =
      props->column_encryption_properties(column_path_1->ToDotString());

  ASSERT_EQ(true, out_col_props_1->is_encrypted());
  ASSERT_EQ(false, out_col_props_1->is_encrypted_with_footer_key());
  ASSERT_EQ(kColumnEncryptionKey1, out_col_props_1->key());
  ASSERT_EQ("kc1", out_col_props_1->key_metadata());

  std::shared_ptr<ColumnEncryptionProperties> out_col_props_2 =
      props->column_encryption_properties(column_path_2->ToDotString());

  ASSERT_EQ(true, out_col_props_2->is_encrypted());
  ASSERT_EQ(false, out_col_props_2->is_encrypted_with_footer_key());
  ASSERT_EQ(kColumnEncryptionKey2, out_col_props_2->key());
  ASSERT_EQ("kc2", out_col_props_2->key_metadata());

  // other columns: encrypted with footer, footer is not encrypted
  // so column is not encrypted as well
  std::string column_path_3 = "column_3";
  std::shared_ptr<ColumnEncryptionProperties> out_col_props_3 =
      props->column_encryption_properties(column_path_3);

  ASSERT_EQ(NULLPTR, out_col_props_3);
}

// Encryption configuration 4: Encrypt nested columns by their parent field.
TEST(TestEncryptionProperties, EncryptNestedColumns) {
  std::shared_ptr<parquet::schema::ColumnPath> column_path_1 =
      parquet::schema::ColumnPath::FromDotString("a_map");
  ColumnEncryptionProperties::Builder column_builder_1;
  column_builder_1.key(kColumnEncryptionKey1);
  column_builder_1.key_id("kc1");

  std::shared_ptr<parquet::schema::ColumnPath> column_path_2 =
      parquet::schema::ColumnPath::FromDotString("a_list");
  ColumnEncryptionProperties::Builder column_builder_2;
  column_builder_2.key(kColumnEncryptionKey2);
  column_builder_2.key_id("kc2");

  std::shared_ptr<parquet::schema::ColumnPath> column_path_3 =
      parquet::schema::ColumnPath::FromDotString("a_struct");
  ColumnEncryptionProperties::Builder column_builder_3;
  column_builder_3.key(kColumnEncryptionKey3);
  column_builder_3.key_id("kc3");

  std::map<std::string, std::shared_ptr<ColumnEncryptionProperties>> encrypted_columns;
  encrypted_columns[column_path_1->ToDotString()] = column_builder_1.build();
  encrypted_columns[column_path_2->ToDotString()] = column_builder_2.build();
  encrypted_columns[column_path_3->ToDotString()] = column_builder_3.build();

  FileEncryptionProperties::Builder builder(kFooterEncryptionKey);
  builder.footer_key_metadata("kf");
  builder.set_plaintext_footer();
  builder.encrypted_columns(std::move(encrypted_columns));
  std::shared_ptr<FileEncryptionProperties> props = builder.build();

  ASSERT_EQ(false, props->encrypted_footer());
  ASSERT_EQ(kDefaultEncryptionAlgorithm, props->algorithm().algorithm);
  ASSERT_EQ(kFooterEncryptionKey, props->footer_key());

  for (const std::string column_path :
       {"a_map", "a_map.key_value.key", "a_map.key_value.value"}) {
    std::shared_ptr<ColumnEncryptionProperties> out_col_props =
        props->column_encryption_properties(column_path);

    ASSERT_EQ(true, out_col_props->is_encrypted());
    ASSERT_EQ(false, out_col_props->is_encrypted_with_footer_key());
    ASSERT_EQ(kColumnEncryptionKey1, out_col_props->key());
    ASSERT_EQ("kc1", out_col_props->key_metadata());
  }

  for (const std::string column_path : {"a_list", "a_list.list.element"}) {
    std::shared_ptr<ColumnEncryptionProperties> out_col_props =
        props->column_encryption_properties(column_path);

    ASSERT_EQ(true, out_col_props->is_encrypted());
    ASSERT_EQ(false, out_col_props->is_encrypted_with_footer_key());
    ASSERT_EQ(kColumnEncryptionKey2, out_col_props->key());
    ASSERT_EQ("kc2", out_col_props->key_metadata());
  }

  for (const std::string column_path : {"a_struct", "a_struct.f1", "a_struct.f2"}) {
    std::shared_ptr<ColumnEncryptionProperties> out_col_props =
        props->column_encryption_properties(column_path);

    ASSERT_EQ(true, out_col_props->is_encrypted());
    ASSERT_EQ(false, out_col_props->is_encrypted_with_footer_key());
    ASSERT_EQ(kColumnEncryptionKey3, out_col_props->key());
    ASSERT_EQ("kc3", out_col_props->key_metadata());
  }

  // other columns: encrypted with footer, footer is not encrypted
  // so column is not encrypted as well
  // Note: character '-' is lexicographically before '.', and '/' is after
  for (const std::string column_path :
       {"id", "a_map-column", "a_map/column", "a_list-column", "a_list/column",
        "a_struct-column", "a_struct/column"}) {
    std::shared_ptr<ColumnEncryptionProperties> out_col_props =
        props->column_encryption_properties(column_path);
    ASSERT_EQ(NULLPTR, out_col_props);
  }
}

// Use aad_prefix
TEST(TestEncryptionProperties, UseAadPrefix) {
  FileEncryptionProperties::Builder builder(kFooterEncryptionKey);
  builder.aad_prefix(kFileName);
  std::shared_ptr<FileEncryptionProperties> props = builder.build();

  ASSERT_EQ(kFileName, props->algorithm().aad.aad_prefix);
  ASSERT_EQ(false, props->algorithm().aad.supply_aad_prefix);
}

// Use aad_prefix and
// disable_aad_prefix_storage.
TEST(TestEncryptionProperties, UseAadPrefixNotStoreInFile) {
  FileEncryptionProperties::Builder builder(kFooterEncryptionKey);
  builder.aad_prefix(kFileName);
  builder.disable_aad_prefix_storage();
  std::shared_ptr<FileEncryptionProperties> props = builder.build();

  ASSERT_EQ("", props->algorithm().aad.aad_prefix);
  ASSERT_EQ(true, props->algorithm().aad.supply_aad_prefix);
}

// Use AES_GCM_CTR_V1 algorithm
TEST(TestEncryptionProperties, UseAES_GCM_CTR_V1Algorithm) {
  FileEncryptionProperties::Builder builder(kFooterEncryptionKey);
  builder.algorithm(ParquetCipher::AES_GCM_CTR_V1);
  std::shared_ptr<FileEncryptionProperties> props = builder.build();

  ASSERT_EQ(ParquetCipher::AES_GCM_CTR_V1, props->algorithm().algorithm);
}

TEST(TestDecryptionProperties, UseKeyRetriever) {
  std::shared_ptr<parquet::StringKeyIdRetriever> string_kr1 =
      std::make_shared<parquet::StringKeyIdRetriever>();
  string_kr1->PutKey("kf", kFooterEncryptionKey);
  string_kr1->PutKey("kc1", kColumnEncryptionKey1);
  string_kr1->PutKey("kc2", kColumnEncryptionKey2);
  std::shared_ptr<parquet::DecryptionKeyRetriever> kr1 =
      std::static_pointer_cast<parquet::StringKeyIdRetriever>(string_kr1);

  parquet::FileDecryptionProperties::Builder builder;
  builder.key_retriever(std::move(kr1));
  std::shared_ptr<parquet::FileDecryptionProperties> props = builder.build();

  auto out_key_retriever = props->key_retriever();
  ASSERT_EQ(kFooterEncryptionKey, out_key_retriever->GetKey("kf"));
  ASSERT_EQ(kColumnEncryptionKey1, out_key_retriever->GetKey("kc1"));
  ASSERT_EQ(kColumnEncryptionKey2, out_key_retriever->GetKey("kc2"));
}

TEST(TestDecryptionProperties, SupplyAadPrefix) {
  parquet::FileDecryptionProperties::Builder builder;
  builder.footer_key(kFooterEncryptionKey);
  builder.aad_prefix(kFileName);
  std::shared_ptr<parquet::FileDecryptionProperties> props = builder.build();

  ASSERT_EQ(kFileName, props->aad_prefix());
}

TEST(ColumnDecryptionProperties, SetKey) {
  std::shared_ptr<parquet::schema::ColumnPath> column_path_1 =
      parquet::schema::ColumnPath::FromDotString("column_1");
  ColumnDecryptionProperties::Builder col_builder_1(*column_path_1);
  col_builder_1.key(kColumnEncryptionKey1);

  auto props = col_builder_1.build();
  ASSERT_EQ(kColumnEncryptionKey1, props->key());
}

TEST(TestDecryptionProperties, UsingExplicitFooterAndColumnKeys) {
  std::string column_path_1 = "column_1";
  std::string column_path_2 = "column_2";
  std::map<std::string, std::shared_ptr<parquet::ColumnDecryptionProperties>>
      decryption_cols;
  parquet::ColumnDecryptionProperties::Builder col_builder_1(column_path_1);
  parquet::ColumnDecryptionProperties::Builder col_builder_2(column_path_2);

  decryption_cols[column_path_1] = col_builder_1.key(kColumnEncryptionKey1)->build();
  decryption_cols[column_path_2] = col_builder_2.key(kColumnEncryptionKey2)->build();

  parquet::FileDecryptionProperties::Builder builder;
  builder.footer_key(kFooterEncryptionKey);
  builder.column_keys(std::move(decryption_cols));
  std::shared_ptr<parquet::FileDecryptionProperties> props = builder.build();

  ASSERT_EQ(kFooterEncryptionKey, props->footer_key());
  ASSERT_EQ(kColumnEncryptionKey1, props->column_key(column_path_1));
  ASSERT_EQ(kColumnEncryptionKey2, props->column_key(column_path_2));
}

}  // namespace parquet::encryption::test
