/**
 * Copyright 2024 Centreon
 *
 * 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.
 *
 * For more information : contact@centreon.com
 */

#include <gtest/gtest.h>

#include "check.hh"

using namespace com::centreon::agent;

TEST(duration_from_str, values) {
  EXPECT_EQ(duration{0}, duration_from_string("", 's'));
  EXPECT_EQ(std::chrono::seconds(45), duration_from_string("45s", 'd'));
  EXPECT_EQ(std::chrono::seconds(45), duration_from_string("45", 's'));
  EXPECT_EQ(std::chrono::minutes(33), duration_from_string("33m", 'd'));
  EXPECT_EQ(std::chrono::minutes(33), duration_from_string("33", 'm'));
  EXPECT_EQ(std::chrono::hours(17), duration_from_string("17h", 'd'));
  EXPECT_EQ(std::chrono::hours(17), duration_from_string("17", 'h'));
  EXPECT_EQ(std::chrono::hours(13 * 24), duration_from_string("13d", 'd'));
  EXPECT_EQ(std::chrono::hours(13 * 24), duration_from_string("13d", 'h'));
  EXPECT_EQ(std::chrono::hours(13 * 24), duration_from_string("13", 'd'));
  EXPECT_EQ(std::chrono::hours(11 * 7 * 24), duration_from_string("11w", 'd'));
  EXPECT_EQ(std::chrono::hours(11 * 7 * 24), duration_from_string("11", 'w'));
  EXPECT_EQ(std::chrono::hours(-11 * 7 * 24), duration_from_string("-11", 'w'));
  EXPECT_EQ(std::chrono::hours(-11 * 7 * 24),
            duration_from_string("-11w", 'd'));
  EXPECT_EQ(std::chrono::hours(11 * 7 * 24),
            duration_from_string("-11w", 'd', true));
  EXPECT_EQ(std::chrono::hours(7 * 24) + std::chrono::hours(3 * 24) +
                std::chrono::hours(13) + std::chrono::minutes(5) +
                std::chrono::seconds(30),
            duration_from_string("1w3d13h5m30", 's'));
}

extern std::shared_ptr<asio::io_context> g_io_context;

class check_test : public testing::Test {
 public:
  Service serv;

  check_test() {
    serv.set_service_description("my_serv");
    serv.set_command_name("my_command_name");
    serv.set_command_line("my_command_line");
    serv.set_check_interval(2);
    serv.set_retry_interval(1);
    serv.set_max_attempts(3);
  }
};
class dummy_check : public check {
  duration _command_duration;
  asio::system_timer _command_timer;

 public:
  void start_check(const duration& timeout) override {
    if (!_start_check(timeout)) {
      return;
    }
    _command_timer.expires_after(_command_duration);
    _command_timer.async_wait([me = shared_from_this(), this,
                               running_index = _get_running_check_index()](
                                  const boost::system::error_code& err) {
      if (err) {
        return;
      }
      on_completion(running_index, 1,
                    std::list<com::centreon::common::perfdata>(),
                    {"output dummy_check of " + get_command_line()});
    });
  }

  template <typename handler_type>
  dummy_check(const Service& serv,
              const duration& command_duration,
              handler_type&& handler)
      : check(g_io_context,
              spdlog::default_logger(),
              std::chrono::system_clock::now(),
              serv,
              nullptr,
              handler,
              std::make_shared<checks_statistics>()),
        _command_duration(command_duration),
        _command_timer(*g_io_context) {}
};

TEST_F(check_test, timeout) {
  unsigned status = 0;
  std::string output;
  std::mutex cond_m;
  std::condition_variable cond;
  unsigned handler_call_cpt = 0;

  std::shared_ptr<dummy_check> checker = std::make_shared<dummy_check>(
      serv, std::chrono::milliseconds(500),
      [&status, &output, &handler_call_cpt, &cond](
          const std::shared_ptr<check>&, unsigned statuss,
          [[maybe_unused]] const std::list<com::centreon::common::perfdata>&
              perfdata,
          const std::list<std::string>& outputs) {
        status = statuss;
        if (outputs.size() == 1) {
          output = *outputs.begin();
        }
        ++handler_call_cpt;
        cond.notify_all();
      });

  checker->start_check(std::chrono::milliseconds(100));

  std::unique_lock l(cond_m);
  cond.wait(l);

  ASSERT_EQ(status, 3);
  ASSERT_EQ(handler_call_cpt, 1);
  ASSERT_EQ(output, "Timeout at execution of my_command_line");

  // completion handler not called twice
  std::this_thread::sleep_for(std::chrono::milliseconds(500));
  ASSERT_EQ(status, 3);
  ASSERT_EQ(handler_call_cpt, 1);
  ASSERT_EQ(output, "Timeout at execution of my_command_line");
}

TEST_F(check_test, no_timeout) {
  unsigned status = 0;
  std::string output;
  std::mutex cond_m;
  std::condition_variable cond;
  unsigned handler_call_cpt = 0;

  std::shared_ptr<dummy_check> checker = std::make_shared<dummy_check>(
      serv, std::chrono::milliseconds(100),
      [&status, &output, &handler_call_cpt, &cond](
          const std::shared_ptr<check>&, unsigned statuss,
          [[maybe_unused]] const std::list<com::centreon::common::perfdata>&
              perfdata,
          const std::list<std::string>& outputs) {
        status = statuss;
        if (outputs.size() == 1) {
          output = *outputs.begin();
        }
        ++handler_call_cpt;
        cond.notify_all();
      });

  checker->start_check(std::chrono::milliseconds(200));

  std::unique_lock l(cond_m);
  cond.wait(l);

  ASSERT_EQ(status, 1);
  ASSERT_EQ(handler_call_cpt, 1);
  ASSERT_EQ(output, "output dummy_check of my_command_line");

  // completion handler not called twice
  std::this_thread::sleep_for(std::chrono::milliseconds(500));
  ASSERT_EQ(status, 1);
  ASSERT_EQ(handler_call_cpt, 1);
  ASSERT_EQ(output, "output dummy_check of my_command_line");
}

// non ok soft  ->  ok soft -> ok hard
TEST_F(check_test, nagios_confirmation_and_retry_soft_recovery) {
  // prepare a dummy checker
  std::shared_ptr<dummy_check> checker = std::make_shared<dummy_check>(
      serv, std::chrono::milliseconds(1),
      [](const std::shared_ptr<check>&, unsigned,
         const std::list<com::centreon::common::perfdata>&,
         const std::list<std::string>&) {});

  // simulate previous non-OK soft state
  checker->set_last_status(2);           // previous status was non-OK
  checker->set_status_confirmed(false);  // previous was NOT confirmed (soft)
  checker->set_current_attempt(1);
  ASSERT_EQ(checker->get_max_attempts(), 3);

  checker->calcul_status_confirmation(0);  // now OK

  // soft recovery: status should remain not confirmed, last_status updated to
  // OK
  EXPECT_FALSE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 0);
  EXPECT_EQ(checker->get_current_attempt(), 2);

  // further OK
  checker->calcul_status_confirmation(0);  // now OK
  // further OK: should be confirmed (hard) and last_status updated to OK
  EXPECT_TRUE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 0);
  EXPECT_EQ(checker->get_current_attempt(), 1);
}

// non ok hard  ->  ok hard
TEST_F(check_test, nagios_confirmation_and_retry_hard_recovery) {
  std::shared_ptr<dummy_check> checker = std::make_shared<dummy_check>(
      serv, std::chrono::milliseconds(1),
      [](const std::shared_ptr<check>&, unsigned,
         const std::list<com::centreon::common::perfdata>&,
         const std::list<std::string>&) {});

  // simulate previous non-OK hard state
  checker->set_last_status(2);          // previous status was non-OK
  checker->set_status_confirmed(true);  // previous was confirmed (hard)

  checker->calcul_status_confirmation(0);  // now OK

  // hard recovery: should be confirmed and last_status updated to OK
  EXPECT_TRUE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 0);
}

// 1st check ok hard
TEST_F(check_test, first_check_ok_hard) {
  std::shared_ptr<dummy_check> checker = std::make_shared<dummy_check>(
      serv, std::chrono::milliseconds(1),
      [](const std::shared_ptr<check>&, unsigned,
         const std::list<com::centreon::common::perfdata>&,
         const std::list<std::string>&) {});

  checker->calcul_status_confirmation(0);  // now OK

  // hard
  EXPECT_TRUE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 0);
}

// 1st check Nok soft
TEST_F(check_test, first_check_nok_hard) {
  std::shared_ptr<dummy_check> checker = std::make_shared<dummy_check>(
      serv, std::chrono::milliseconds(1),
      [](const std::shared_ptr<check>&, unsigned,
         const std::list<com::centreon::common::perfdata>&,
         const std::list<std::string>&) {});

  checker->calcul_status_confirmation(2);  // NOK

  // hard
  EXPECT_FALSE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 2);
}

// max attemps 3 : init ok hard -> non ok soft -> non ok soft -> non ok hard ->
// ok hard -> ok hard
TEST_F(check_test, nagios_confirmation_and_retry_problem_state) {
  std::shared_ptr<dummy_check> checker = std::make_shared<dummy_check>(
      serv, std::chrono::milliseconds(1),
      [](const std::shared_ptr<check>&, unsigned,
         const std::list<com::centreon::common::perfdata>&,
         const std::list<std::string>&) {});

  // first problem: from initial state to non-OK
  checker->set_last_status(0);          // simulate first run before problem
  checker->set_status_confirmed(true);  // irrelevant but explicit

  checker->calcul_status_confirmation(2);  // non-OK

  // first non-OK: should be not confirmed (soft) and last_status updated to
  EXPECT_FALSE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 2);
  EXPECT_EQ(checker->get_current_attempt(), 1);

  // second problem: still non-OK
  checker->calcul_status_confirmation(2);  // non-OK
  // second non-OK: should be not confirmed (soft) and last_status updated to
  EXPECT_FALSE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 2);
  EXPECT_EQ(checker->get_current_attempt(), 2);

  // third problem: still non-OK
  checker->calcul_status_confirmation(2);  // non-OK
  // third non-OK: should be confirmed (hard) and last_status updated to
  EXPECT_TRUE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 2);
  EXPECT_EQ(checker->get_current_attempt(), 3);

  // further problems: still non-OK
  checker->calcul_status_confirmation(2);  // non-OK
  // further non-OK: should be confirmed (hard) and last_status updated to
  EXPECT_TRUE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 2);
  EXPECT_EQ(checker->get_current_attempt(), 3);  // should not increase

  // recovery to OK
  checker->calcul_status_confirmation(0);  // now OK
  // recovery from hard: should be confirmed (hard) and last_status updated to
  EXPECT_TRUE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 0);
  EXPECT_EQ(checker->get_current_attempt(), 1);  // reset to 1

  // further OK
  checker->calcul_status_confirmation(0);  // now OK
  // further OK: should be confirmed (hard) and last_status updated to OK
  EXPECT_TRUE(checker->get_status_confirmed());
  EXPECT_EQ(checker->get_last_status(), 0);
  EXPECT_EQ(checker->get_current_attempt(), 1);  // should not increase
}