// Copyright 2024 The Bazel Authors. All rights reserved.
//
// Licensed 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 "src/tools/singlejar/log4j2_plugin_dat_combiner.h"

#include <fstream>
#include <string>

#include "googletest/include/gtest/gtest.h"
#include "rules_cc/cc/runfiles/runfiles.h"
#include "src/tools/singlejar/input_jar.h"
#include "src/tools/singlejar/test_util.h"

namespace {

using rules_cc::cc::runfiles::Runfiles;

// A test fixture is used because test case setup is needed.
class Log4J2PluginDatCombinerTest : public ::testing::Test {
 public:
  void SetUp() override { runfiles.reset(Runfiles::CreateForTest()); }

 protected:
  static void SetUpTestCase() { ASSERT_EQ(0, chdir(getenv("TEST_TMPDIR"))); }

  static void TearDownTestCase() {}

  std::unique_ptr<Runfiles> runfiles;
};

// Test Log4J2PluginDatCombiner
TEST_F(Log4J2PluginDatCombinerTest, Log4J2PluginDatCombinerSimple) {
  const std::string plugins_cache_path =
      "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat";
  Log4J2PluginDatCombiner combiner(plugins_cache_path, false);

  InputJar input_jar1;
  ASSERT_TRUE(input_jar1.Open(
      runfiles
          ->Rlocation(
              "io_bazel/src/tools/singlejar/data/log4j2_plugins_set_1.jar")
          .c_str()));

  const LH* lh;
  const CDH* cdh;
  while ((cdh = input_jar1.NextEntry(&lh))) {
    if (cdh->file_name_is(plugins_cache_path.c_str())) {
      ASSERT_TRUE(combiner.Merge(cdh, lh));
    }
  }

  InputJar input_jar2;
  ASSERT_TRUE(input_jar2.Open(
      runfiles
          ->Rlocation(
              "io_bazel/src/tools/singlejar/data/log4j2_plugins_set_2.jar")
          .c_str()));
  while ((cdh = input_jar2.NextEntry(&lh))) {
    if (cdh->file_name_is(plugins_cache_path.c_str())) {
      ASSERT_TRUE(combiner.Merge(cdh, lh));
    }
  }

  LH* entry = reinterpret_cast<LH*>(combiner.OutputEntry(true));
  EXPECT_TRUE(entry->is());
  EXPECT_EQ(20, entry->version());
  EXPECT_EQ(Z_DEFLATED, entry->compression_method());
  uint64_t original_size = entry->uncompressed_file_size();
  uint64_t compressed_size = entry->compressed_file_size();

  std::ifstream expected_file(
      runfiles->Rlocation(
          "io_bazel/src/tools/singlejar/data/log4j2_plugins_set_result.dat"),
      std::ios::binary);
  ASSERT_TRUE(expected_file.is_open());

  expected_file.seekg(0, std::ios::end);
  std::streampos expected_size = expected_file.tellg();
  expected_file.seekg(0, std::ios::beg);
  ASSERT_GT(expected_size, 0);
  char* expected_content = new char[expected_size];
  expected_file.read(expected_content, expected_size);
  ASSERT_TRUE(expected_file.good());
  expected_file.close();

  EXPECT_EQ(expected_size, original_size);
  EXPECT_LE(compressed_size, original_size);
  EXPECT_TRUE(entry->file_name_is(plugins_cache_path.c_str()));
  Inflater inflater;
  inflater.DataToInflate(entry->data(), compressed_size);
  uint8_t buffer[256];
  ASSERT_EQ(Z_STREAM_END, inflater.Inflate((buffer), sizeof(buffer)));
  ASSERT_EQ(
      memcmp(reinterpret_cast<char*>(buffer), expected_content, expected_size),
      0)
      << "Byte arrays are not equal";
  free(reinterpret_cast<void*>(entry));
}

TEST_F(Log4J2PluginDatCombinerTest, Log4J2PluginDatCombinerDuplicate) {
  const std::string plugins_cache_path =
      "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat";
  Log4J2PluginDatCombiner combiner(plugins_cache_path, true);

  InputJar input_jar1;
  ASSERT_TRUE(input_jar1.Open(
      runfiles
          ->Rlocation(
              "io_bazel/src/tools/singlejar/data/log4j2_plugins_set_1.jar")
          .c_str()));

  const LH* lh;
  const CDH* cdh;
  while ((cdh = input_jar1.NextEntry(&lh))) {
    if (cdh->file_name_is(plugins_cache_path.c_str())) {
      ASSERT_TRUE(combiner.Merge(cdh, lh));
    }
  }

  InputJar input_jar2;
  // We reuse log4j2_plugins_set_1.jar on purpose to cause a duplicate.
  ASSERT_TRUE(input_jar2.Open(
      runfiles
          ->Rlocation(
              "io_bazel/src/tools/singlejar/data/log4j2_plugins_set_1.jar")
          .c_str()));
  while ((cdh = input_jar2.NextEntry(&lh))) {
    if (cdh->file_name_is(plugins_cache_path.c_str())) {
      ASSERT_DEATH(combiner.Merge(cdh, lh), "present in multiple jars");
    }
  }
}

TEST_F(Log4J2PluginDatCombinerTest, Log4J2PluginDatCombinerMalformed) {
  const std::string plugins_cache_path =
      "META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat";
  Log4J2PluginDatCombiner combiner(plugins_cache_path, true);
  InputJar input_jar;
  ASSERT_TRUE(
      input_jar.Open(runfiles
                         ->Rlocation("io_bazel/src/tools/singlejar/data/"
                                     "log4j2_malformed_empty_plugin.jar")
                         .c_str()));
  const LH* lh;
  const CDH* cdh;
  while ((cdh = input_jar.NextEntry(&lh))) {
    if (cdh->file_name_is(plugins_cache_path.c_str())) {
      ASSERT_DEATH(combiner.Merge(cdh, lh),
                   "Log4j2Plugins.dat file is malformed");
    }
  }
}

}  // anonymous namespace
