/** @file

  Catch-based tests for RolledLogDeleter.

  @section license License

  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 "swoc/swoc_file.h"

#include <RolledLogDeleter.h>

#define CATCH_CONFIG_MAIN
#include "catch.hpp"

namespace fs = swoc::file;

const fs::path log_dir("/home/y/logs/trafficserver");

void
verify_there_are_no_candidates(RolledLogDeleter &deleter)
{
  CHECK_FALSE(deleter.has_candidates());
  CHECK(deleter.get_candidate_count() == 0);
}

void
verify_rolled_log_behavior(RolledLogDeleter &deleter, fs::path rolled_log1, fs::path rolled_log2, fs::path rolled_log3)
{
  SECTION("Verify we can add a single rolled file")
  {
    constexpr int64_t file_size     = 100;
    constexpr time_t  last_modified = 30;

    REQUIRE(deleter.consider_for_candidacy(rolled_log1.string(), file_size, last_modified));

    CHECK(deleter.has_candidates());
    CHECK(deleter.get_candidate_count() == 1);

    const auto next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_log1.string());

    // Everything has been taken.
    verify_there_are_no_candidates(deleter);
  }

  SECTION("Verify we can add two rolled log files")
  {
    constexpr int64_t file_size          = 100;
    constexpr time_t  oldest_timestamp   = 30;
    constexpr time_t  youngest_timestamp = 60;

    // Intentionally insert them out of order (that is, the first one to delete
    // is the second added).
    REQUIRE(deleter.consider_for_candidacy(rolled_log2.string(), file_size, youngest_timestamp));
    REQUIRE(deleter.consider_for_candidacy(rolled_log1.string(), file_size, oldest_timestamp));

    CHECK(deleter.has_candidates());
    CHECK(deleter.get_candidate_count() == 2);

    // The first candidate should be the oldest modified one.
    auto next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_log1.string());

    CHECK(deleter.has_candidates());
    CHECK(deleter.get_candidate_count() == 1);

    // The second candidate should be the remaining one.
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_log2.string());

    // Everything has been taken.
    verify_there_are_no_candidates(deleter);
  }

  SECTION("Verify we can add three rolled log files")
  {
    constexpr int64_t file_size = 100;

    constexpr time_t oldest_timestamp   = 30;
    constexpr time_t youngest_timestamp = 60;
    constexpr time_t middle_timestamp   = 45;

    // Intentionally insert them out of order.
    REQUIRE(deleter.consider_for_candidacy(rolled_log2.string(), file_size, youngest_timestamp));
    REQUIRE(deleter.consider_for_candidacy(rolled_log1.string(), file_size, oldest_timestamp));
    REQUIRE(deleter.consider_for_candidacy(rolled_log3.string(), file_size, middle_timestamp));

    CHECK(deleter.has_candidates());
    CHECK(deleter.get_candidate_count() == 3);

    // The first candidate should be the oldest modified one.
    auto next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_log1.string());

    CHECK(deleter.has_candidates());
    CHECK(deleter.get_candidate_count() == 2);

    // The second candidate should be the second oldest.
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_log3.string());

    CHECK(deleter.has_candidates());
    CHECK(deleter.get_candidate_count() == 1);

    // The third candidate should be the remaining one.
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_log2.string());

    // Everything has been taken.
    verify_there_are_no_candidates(deleter);
  }
}

TEST_CASE("Rotated diags logs can be added and removed", "[RolledLogDeleter]")
{
  RolledLogDeleter deleter;
  constexpr auto   min_count = 0;
  deleter.register_log_type_for_deletion("diags.log", min_count);

  const fs::path rolled_log1 = log_dir / "diags.log.20191117.16h43m15s-20191118.16h43m15s.old";
  const fs::path rolled_log2 = log_dir / "diags.log.20191118.16h43m15s-20191122.04h07m09s.old";
  const fs::path rolled_log3 = log_dir / "diags.log.20191122.04h07m09s-20191124.00h12m47s.old";

  verify_there_are_no_candidates(deleter);
  verify_rolled_log_behavior(deleter, rolled_log1, rolled_log2, rolled_log3);
}

TEST_CASE("Rotated squid logs can be added and removed", "[RolledLogDeleter]")
{
  RolledLogDeleter deleter;
  constexpr auto   min_count = 0;
  deleter.register_log_type_for_deletion("squid.log", min_count);
  const fs::path rolled_log1 = log_dir / "squid.log_some.hostname.com.20191125.19h00m04s-20191125.19h15m04s.old";
  const fs::path rolled_log2 = log_dir / "squid.log_some.hostname.com.20191125.19h15m04s-20191125.19h30m04s.old";
  const fs::path rolled_log3 = log_dir / "squid.log_some.hostname.com.20191125.19h30m04s-20191125.19h45m04s.old";

  verify_there_are_no_candidates(deleter);
  verify_rolled_log_behavior(deleter, rolled_log1, rolled_log2, rolled_log3);
}

TEST_CASE("clear removes all candidates", "[RolledLogDeleter]")
{
  RolledLogDeleter deleter;
  constexpr auto   min_count = 0;
  deleter.register_log_type_for_deletion("squid.log", min_count);
  deleter.register_log_type_for_deletion("diags.log", min_count);

  constexpr auto   size       = 10;
  constexpr time_t time_stamp = 20;

  // Add some candidates.
  REQUIRE(deleter.consider_for_candidacy("squid.log_arbitrary-text-1", size, time_stamp));
  REQUIRE(deleter.consider_for_candidacy("squid.log_arbitrary-text-2", size, time_stamp));
  REQUIRE(deleter.consider_for_candidacy("squid.log_arbitrary-text-3", size, time_stamp));

  REQUIRE(deleter.consider_for_candidacy("diags.log.arbitrary-text-1", size, time_stamp));
  REQUIRE(deleter.consider_for_candidacy("diags.log.arbitrary-text-2", size, time_stamp));
  REQUIRE(deleter.consider_for_candidacy("diags.log.arbitrary-text-3", size, time_stamp));

  REQUIRE(deleter.has_candidates());
  REQUIRE(deleter.get_candidate_count() == 6);

  deleter.clear_candidates();
  verify_there_are_no_candidates(deleter);
}

TEST_CASE("verify priority enforcement", "[RolledLogDeleter]")
{
  RolledLogDeleter deleter;

  constexpr auto low_min_count     = 1;
  constexpr auto medium_min_count  = 3;
  constexpr auto highest_min_count = 0;

  constexpr int64_t a_size = 10;
  constexpr time_t  a_time = 30;

  deleter.register_log_type_for_deletion("squid.log", low_min_count);
  deleter.register_log_type_for_deletion("traffic.out", medium_min_count);
  deleter.register_log_type_for_deletion("diags.log", highest_min_count);

  /* The previous tests verify selection within a log_type which is done based
   * upon last modified time stamp. These tests focus on selection of
   * candidates across log types, which is based upon number of candidates and
   * the desired min_count. */
  SECTION("Verify selection of a candidate when there is only one.")
  {
    const fs::path rolled_squid = log_dir / "squid.log_some.hostname.com.20191125.19h00m04s-20191125.19h15m04s.old";
    REQUIRE(deleter.consider_for_candidacy(rolled_squid.view(), a_size, a_time));
    const auto next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_squid.string());
    verify_there_are_no_candidates(deleter);
  }

  SECTION("Verify selection of candidates across three types.")
  {
    const fs::path rolled_squid   = log_dir / "squid.log_some.hostname.com.20191125.19h00m04s-20191125.19h15m04s.old";
    const fs::path rolled_traffic = log_dir / "traffic.out.20191118.16h43m11s-20191122.01h30m30s.old";
    const fs::path rolled_diags   = log_dir / "diags.log.20191117.16h43m15s-20191118.16h43m15s.old";

    REQUIRE(deleter.consider_for_candidacy(rolled_squid.view(), a_size, a_time));
    REQUIRE(deleter.consider_for_candidacy(rolled_traffic.view(), a_size, a_time));
    REQUIRE(deleter.consider_for_candidacy(rolled_diags.view(), a_size, a_time));

    // Since the time stamps of both are the same, selection should be made
    // based upon min_count.
    auto next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_squid.string());

    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_traffic.string());

    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_diags.string());

    verify_there_are_no_candidates(deleter);
  }

  SECTION("Verify that number of candidates is taken into account.")
  {
    const fs::path rolled_squid    = log_dir / "squid.log_some.hostname.com.20191125.19h00m04s-20191125.19h15m04s.old";
    const fs::path rolled_traffic1 = log_dir / "traffic.out.20191117.16h43m15s-20191118.16h43m15s.old";
    const fs::path rolled_traffic2 = log_dir / "traffic.out.20191118.16h43m15s-20191122.04h07m09s.old";
    const fs::path rolled_traffic3 = log_dir / "traffic.out.20191122.04h07m09s-20191124.00h12m47s.old";
    const fs::path rolled_traffic4 = log_dir / "traffic.out.20191124.00h12m44s-20191125.00h12m44s.old";

    constexpr time_t old       = 60;
    constexpr time_t older     = 30;
    constexpr time_t oldest    = 10;
    constexpr time_t oldestest = 5;

    REQUIRE(deleter.consider_for_candidacy(rolled_squid.view(), a_size, a_time));
    REQUIRE(deleter.consider_for_candidacy(rolled_traffic1.view(), a_size, old));
    REQUIRE(deleter.consider_for_candidacy(rolled_traffic2.view(), a_size, older));
    REQUIRE(deleter.consider_for_candidacy(rolled_traffic3.view(), a_size, oldest));
    REQUIRE(deleter.consider_for_candidacy(rolled_traffic4.view(), a_size, oldestest));

    // The user has requested a higher number of traffic.out files, but since
    // there are so many of them, the oldest of them should be selected next.
    auto next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_traffic4.string());

    // Next, squid.log should be chosen.
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_squid.string());

    // Now, there's only traffic.out files.
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_traffic3.string());
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_traffic2.string());
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_traffic1.string());

    verify_there_are_no_candidates(deleter);
  }

  SECTION("A mincount of 0 should shield from deletion as much as possible")
  {
    const fs::path rolled_traffic = log_dir / "traffic.out.20191117.16h43m15s-20191118.16h43m15s.old";
    const fs::path rolled_diags1  = log_dir / "diags.log.20191117.16h43m15s-20191118.16h43m15s.old";
    const fs::path rolled_diags2  = log_dir / "diags.log.20191118.16h43m15s-20191122.04h07m09s.old";
    const fs::path rolled_diags3  = log_dir / "diags.log.20191122.04h07m09s-20191124.00h12m47s.old";
    const fs::path rolled_diags4  = log_dir / "diags.log.20191124.00h12m44s-20191125.00h12m44s.old";

    constexpr time_t old       = 60;
    constexpr time_t older     = 30;
    constexpr time_t oldest    = 10;
    constexpr time_t oldestest = 5;

    REQUIRE(deleter.consider_for_candidacy(rolled_traffic.view(), a_size, a_time));
    REQUIRE(deleter.consider_for_candidacy(rolled_diags1.view(), a_size, old));
    REQUIRE(deleter.consider_for_candidacy(rolled_diags2.view(), a_size, older));
    REQUIRE(deleter.consider_for_candidacy(rolled_diags3.view(), a_size, oldest));
    REQUIRE(deleter.consider_for_candidacy(rolled_diags4.view(), a_size, oldestest));

    // Even with so many diags.log files, the traffic.out one should be
    // selected first because the min_count of diags.log is 0.
    auto next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_traffic.string());

    // Now there's only diags.log files.
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_diags4.string());
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_diags3.string());
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_diags2.string());
    next_candidate = deleter.take_next_candidate_to_delete();
    CHECK(next_candidate->rolled_log_path == rolled_diags1.string());

    verify_there_are_no_candidates(deleter);
  }
}
