// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/policy/remote_commands/device_command_reset_euicc_job.h"

#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chrome/test/base/chrome_ash_test_base.h"
#include "chromeos/ash/components/dbus/hermes/hermes_clients.h"
#include "chromeos/ash/components/dbus/hermes/hermes_euicc_client.h"
#include "chromeos/ash/components/dbus/hermes/hermes_manager_client.h"
#include "chromeos/ash/components/dbus/shill/shill_clients.h"
#include "chromeos/ash/components/dbus/shill/shill_manager_client.h"
#include "chromeos/ash/components/network/network_handler.h"
#include "chromeos/ash/components/network/network_handler_test_helper.h"
#include "chromeos/ash/components/network/network_state_handler.h"
#include "chromeos/ash/components/network/network_state_handler_observer.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/message_center/message_center.h"

namespace policy {

namespace em = enterprise_management;

namespace {

const base::TimeDelta kNetworkListWaitTimeout = base::Seconds(20);
const char kTestEuiccPath[] = "/test/hermes/123456789";
const char kTestEid[] = "123456789";
const RemoteCommandJob::UniqueIDType kUniqueID = 123456789;
const char kResetEuiccOperationResultHistogram[] =
    "Network.Cellular.ESim.Policy.ResetEuicc.Result";
const char kResetEuiccDurationHistogram[] =
    "Network.Cellular.ESim.Policy.ResetEuicc.Duration";

em::RemoteCommand GenerateResetEuiccCommandProto(
    base::TimeDelta age_of_command) {
  em::RemoteCommand command_proto;
  command_proto.set_type(em::RemoteCommand_Type_DEVICE_RESET_EUICC);
  command_proto.set_command_id(kUniqueID);
  command_proto.set_age_of_command(age_of_command.InMilliseconds());
  return command_proto;
}

void VerifyEuiccProfileCount(size_t expected_count) {
  ash::HermesEuiccClient::Properties* euicc_properties =
      ash::HermesEuiccClient::Get()->GetProperties(
          dbus::ObjectPath(kTestEuiccPath));
  const std::vector<dbus::ObjectPath>& profile_paths =
      euicc_properties->profiles().value();
  EXPECT_EQ(expected_count, profile_paths.size());
}

void VerifyJobResult(const RemoteCommandJob& job,
                     RemoteCommandJob::Status expected_status,
                     size_t expected_profile_count) {
  EXPECT_EQ(expected_status, job.status());
  VerifyEuiccProfileCount(expected_profile_count);
}

// Helper that waits until NetworkStateHandler has registered the expected
// number of Shill cellular services for the test EUICC.
//
// Adding fake eSIM profiles asynchronously creates backing services in Shill.
// We need to wait for these services to appear in NetworkStateHandler before
// running the reset job, which requires them to be present to remove them.
class ESimServiceListWaiter : public ash::NetworkStateHandlerObserver {
 public:
  explicit ESimServiceListWaiter(size_t expected_service_count)
      : expected_service_count_(expected_service_count) {
    observation_.Observe(ash::NetworkHandler::Get()->network_state_handler());
  }

  ESimServiceListWaiter(const ESimServiceListWaiter&) = delete;
  ESimServiceListWaiter& operator=(const ESimServiceListWaiter&) = delete;

  ~ESimServiceListWaiter() override = default;

  void Wait() { run_loop_.Run(); }

 private:
  void NetworkListChanged() override {
    ash::NetworkStateHandler::NetworkStateList networks;
    ash::NetworkHandler::Get()->network_state_handler()->GetNetworkListByType(
        ash::NetworkTypePattern::Cellular(),
        /*configured_only=*/false,
        /*visible_only=*/false, /*limit=*/0, &networks);

    size_t service_count = 0;
    for (const ash::NetworkState* network : networks) {
      if (network->eid() == kTestEid && !network->IsNonShillCellularNetwork()) {
        ++service_count;
      }
    }
    if (service_count == expected_service_count_) {
      run_loop_.Quit();
    }
  }

  const size_t expected_service_count_;
  base::RunLoop run_loop_;
  ash::NetworkStateHandlerScopedObservation observation_{this};
};

}  // namespace

class DeviceCommandResetEuiccJobTest : public ChromeAshTestBase {
 public:
  DeviceCommandResetEuiccJobTest()
      : ChromeAshTestBase(std::unique_ptr<base::test::TaskEnvironment>(
            std::make_unique<content::BrowserTaskEnvironment>(
                base::test::TaskEnvironment::TimeSource::MOCK_TIME))) {}
  DeviceCommandResetEuiccJobTest(const DeviceCommandResetEuiccJobTest&) =
      delete;
  DeviceCommandResetEuiccJobTest& operator=(
      const DeviceCommandResetEuiccJobTest&) = delete;
  ~DeviceCommandResetEuiccJobTest() override = default;

  void SetUp() override {
    ChromeAshTestBase::SetUp();
    helper_ = std::make_unique<ash::NetworkHandlerTestHelper>();
    helper_->hermes_manager_test()->AddEuicc(
        dbus::ObjectPath(kTestEuiccPath), kTestEid,
        /*is_active=*/true, /*physical_slot=*/0);

    ESimServiceListWaiter service_list_waiter(/*expected_service_count=*/2u);
    AddFakeESimProfile();
    AddFakeESimProfile();

    // The reset needs both profile services in the network list before
    // it can remove them.
    service_list_waiter.Wait();

    VerifyEuiccProfileCount(/*expected_count=*/2u);
  }

  void TearDown() override {
    helper_.reset();
    ChromeAshTestBase::TearDown();
  }

 protected:
  std::unique_ptr<RemoteCommandJob> CreateResetEuiccJob(
      base::TimeTicks issued_time) {
    std::unique_ptr<DeviceCommandResetEuiccJob> job =
        std::make_unique<DeviceCommandResetEuiccJob>();
    auto reset_euicc_command_proto =
        GenerateResetEuiccCommandProto(base::TimeTicks::Now() - issued_time);
    EXPECT_TRUE(job->Init(base::TimeTicks::Now(), reset_euicc_command_proto,
                          em::SignedData()));
    EXPECT_EQ(kUniqueID, job->unique_id());
    EXPECT_EQ(RemoteCommandJob::NOT_STARTED, job->status());
    return job;
  }

  void AddFakeESimProfile() {
    helper_->hermes_euicc_test()->AddFakeCarrierProfile(
        dbus::ObjectPath(kTestEuiccPath), hermes::profile::State::kActive,
        /*activation_code=*/"",
        ash::HermesEuiccClient::TestInterface::AddCarrierProfileBehavior::
            kAddProfileWithService);
  }

  base::HistogramTester histogram_tester_;
  std::unique_ptr<ash::NetworkHandlerTestHelper> helper_;
  base::TimeTicks test_start_time_ = base::TimeTicks::Now();
};

TEST_F(DeviceCommandResetEuiccJobTest, ResetEuicc) {
  std::unique_ptr<RemoteCommandJob> job = CreateResetEuiccJob(test_start_time_);
  base::test::TestFuture<void> job_finished_future;
  EXPECT_TRUE(job->Run(base::Time::Now(), base::TimeTicks::Now(),
                       job_finished_future.GetCallback()));
  ASSERT_TRUE(job_finished_future.Wait()) << "Job did not finish.";
  VerifyJobResult(*job, RemoteCommandJob::Status::SUCCEEDED,
                  /*expected_profile_count=*/0u);

  task_environment()->FastForwardBy(kNetworkListWaitTimeout);
  // Verify that the notification should be displayed.
  EXPECT_TRUE(message_center::MessageCenter::Get()->FindVisibleNotificationById(
      DeviceCommandResetEuiccJob::kResetEuiccNotificationId));
  // Verify that appropriate metrics have been logged.
  histogram_tester_.ExpectTotalCount(kResetEuiccOperationResultHistogram, 1);
  histogram_tester_.ExpectBucketCount(
      kResetEuiccOperationResultHistogram,
      DeviceCommandResetEuiccJob::ResetEuiccResult::kSuccess,
      /*expected_count=*/1);
  histogram_tester_.ExpectTotalCount(kResetEuiccDurationHistogram, 1);
}

TEST_F(DeviceCommandResetEuiccJobTest, ResetEuiccFailure) {
  // Simulate a failure by removing the cellular device.
  ash::ShillManagerClient::Get()->GetTestInterface()->ClearDevices();
  base::test::TestFuture<void> job_finished_future;

  std::unique_ptr<RemoteCommandJob> job = CreateResetEuiccJob(test_start_time_);
  EXPECT_TRUE(job->Run(base::Time::Now(), base::TimeTicks::Now(),
                       job_finished_future.GetCallback()));
  ASSERT_TRUE(job_finished_future.Wait()) << "Job did not finish.";
  VerifyJobResult(*job, RemoteCommandJob::Status::FAILED,
                  /*expected_profile_count=*/2u);

  // Verify that the notification was not displayed.
  EXPECT_FALSE(
      message_center::MessageCenter::Get()->FindVisibleNotificationById(
          DeviceCommandResetEuiccJob::kResetEuiccNotificationId));
  // Verify that appropriate metrics have been logged.
  histogram_tester_.ExpectTotalCount(kResetEuiccOperationResultHistogram, 1);
  histogram_tester_.ExpectBucketCount(
      kResetEuiccOperationResultHistogram,
      DeviceCommandResetEuiccJob::ResetEuiccResult::kHermesResetFailed,
      /*expected_count=*/1);
  histogram_tester_.ExpectTotalCount(kResetEuiccDurationHistogram, 0);
}

}  // namespace policy
