//
// Copyright (C) 2014 The Android Open Source Project
//
// 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 "update_engine/update_manager/real_shill_provider.h"

#include <memory>
#include <utility>

#include <base/time/time.h>
#include <brillo/make_unique_ptr.h>
#include <brillo/message_loops/fake_message_loop.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <shill/dbus-constants.h>
#include <shill/dbus-proxies.h>
#include <shill/dbus-proxy-mocks.h>

#include "update_engine/common/fake_clock.h"
#include "update_engine/common/test_utils.h"
#include "update_engine/dbus_test_utils.h"
#include "update_engine/fake_shill_proxy.h"
#include "update_engine/update_manager/umtest_utils.h"

using base::Time;
using base::TimeDelta;
using chromeos_update_engine::FakeClock;
using org::chromium::flimflam::ManagerProxyMock;
using org::chromium::flimflam::ServiceProxyMock;
using std::unique_ptr;
using testing::Mock;
using testing::Return;
using testing::SetArgPointee;
using testing::_;

namespace {

// Fake service paths.
const char* const kFakeEthernetServicePath = "/fake/ethernet/service";
const char* const kFakeWifiServicePath = "/fake/wifi/service";
const char* const kFakeWimaxServicePath = "/fake/wimax/service";
const char* const kFakeBluetoothServicePath = "/fake/bluetooth/service";
const char* const kFakeCellularServicePath = "/fake/cellular/service";
const char* const kFakeVpnServicePath = "/fake/vpn/service";
const char* const kFakeUnknownServicePath = "/fake/unknown/service";

}  // namespace

namespace chromeos_update_manager {

class UmRealShillProviderTest : public ::testing::Test {
 protected:
  // Initialize the RealShillProvider under test.
  void SetUp() override {
    fake_clock_.SetWallclockTime(InitTime());
    loop_.SetAsCurrent();
    provider_.reset(new RealShillProvider(&fake_shill_proxy_, &fake_clock_));

    ManagerProxyMock* manager_proxy_mock = fake_shill_proxy_.GetManagerProxy();

    // The PropertyChanged signal should be subscribed to.
    MOCK_SIGNAL_HANDLER_EXPECT_SIGNAL_HANDLER(
        manager_property_changed_, *manager_proxy_mock, PropertyChanged);
  }

  void TearDown() override {
    provider_.reset();
    // Check for leaked callbacks on the main loop.
    EXPECT_FALSE(loop_.PendingTasks());
  }

  // These methods generate fixed timestamps for use in faking the current time.
  Time InitTime() {
    Time::Exploded now_exp;
    now_exp.year = 2014;
    now_exp.month = 3;
    now_exp.day_of_week = 2;
    now_exp.day_of_month = 18;
    now_exp.hour = 8;
    now_exp.minute = 5;
    now_exp.second = 33;
    now_exp.millisecond = 675;
    return Time::FromLocalExploded(now_exp);
  }

  Time ConnChangedTime() {
    return InitTime() + TimeDelta::FromSeconds(10);
  }

  // Sets the default_service object path in the response from the
  // ManagerProxyMock instance.
  void SetManagerReply(const char* default_service, bool reply_succeeds);

  // Sets the |service_type|, |physical_technology| and |service_tethering|
  // properties in the mocked service |service_path|. If any of the three
  // const char* is a nullptr, the corresponding property will not be included
  // in the response.
  // Returns the mock object pointer, owned by the |fake_shill_proxy_|.
  ServiceProxyMock* SetServiceReply(const std::string& service_path,
                                    const char* service_type,
                                    const char* physical_technology,
                                    const char* service_tethering);

  void InitWithDefaultService(const char* default_service) {
    SetManagerReply(default_service, true);
    // Check that provider initializes correctly.
    EXPECT_TRUE(provider_->Init());
    // RunOnce to notify the signal handler was connected properly.
    EXPECT_TRUE(loop_.RunOnce(false));
  }

  // Sends a signal informing the provider about a default connection
  // |service_path|. Sets the fake connection change time in
  // |conn_change_time_p| if provided.
  void SendDefaultServiceSignal(const std::string& service_path,
                                Time* conn_change_time_p) {
    const Time conn_change_time = ConnChangedTime();
    fake_clock_.SetWallclockTime(conn_change_time);
    ASSERT_TRUE(manager_property_changed_.IsHandlerRegistered());
    manager_property_changed_.signal_callback().Run(
        shill::kDefaultServiceProperty, dbus::ObjectPath(service_path));
    fake_clock_.SetWallclockTime(conn_change_time + TimeDelta::FromSeconds(5));
    if (conn_change_time_p)
      *conn_change_time_p = conn_change_time;
  }

  // Sets up expectations for detection of a connection |service_path| with type
  // |shill_type_str| and tethering mode |shill_tethering_str|. Ensures that the
  // new connection status and change time are properly detected by the
  // provider. Writes the fake connection change time to |conn_change_time_p|,
  // if provided.
  void SetupConnectionAndAttrs(const std::string& service_path,
                               const char* shill_type,
                               const char* shill_tethering,
                               Time* conn_change_time_p) {
    SetServiceReply(service_path, shill_type, nullptr, shill_tethering);
    // Note: We don't setup this |service_path| as the default service path but
    // we instead send a signal notifying the change since the code won't call
    // GetProperties on the Manager object at this point.

    // Send a signal about a new default service.
    Time conn_change_time;
    SendDefaultServiceSignal(service_path, &conn_change_time);

    // Query the connection status, ensure last change time reported correctly.
    UmTestUtils::ExpectVariableHasValue(true, provider_->var_is_connected());
    UmTestUtils::ExpectVariableHasValue(conn_change_time,
                                        provider_->var_conn_last_changed());

    // Write the connection change time to the output argument.
    if (conn_change_time_p)
      *conn_change_time_p = conn_change_time;
  }

  // Sets up a connection and tests that its type is being properly detected by
  // the provider.
  void SetupConnectionAndTestType(const char* service_path,
                                  const char* shill_type,
                                  ConnectionType expected_conn_type) {
    // Set up and test the connection, record the change time.
    Time conn_change_time;
    SetupConnectionAndAttrs(service_path,
                            shill_type,
                            shill::kTetheringNotDetectedState,
                            &conn_change_time);

    // Query the connection type, ensure last change time did not change.
    UmTestUtils::ExpectVariableHasValue(expected_conn_type,
                                        provider_->var_conn_type());
    UmTestUtils::ExpectVariableHasValue(conn_change_time,
                                        provider_->var_conn_last_changed());
  }

  // Sets up a connection and tests that its tethering mode is being properly
  // detected by the provider.
  void SetupConnectionAndTestTethering(
      const char* service_path,
      const char* shill_tethering,
      ConnectionTethering expected_conn_tethering) {
    // Set up and test the connection, record the change time.
    Time conn_change_time;
    SetupConnectionAndAttrs(
        service_path, shill::kTypeEthernet, shill_tethering, &conn_change_time);

    // Query the connection tethering, ensure last change time did not change.
    UmTestUtils::ExpectVariableHasValue(expected_conn_tethering,
                                        provider_->var_conn_tethering());
    UmTestUtils::ExpectVariableHasValue(conn_change_time,
                                        provider_->var_conn_last_changed());
  }

  brillo::FakeMessageLoop loop_{nullptr};
  FakeClock fake_clock_;
  chromeos_update_engine::FakeShillProxy fake_shill_proxy_;

  // The registered signal handler for the signal Manager.PropertyChanged.
  chromeos_update_engine::dbus_test_utils::MockSignalHandler<
      void(const std::string&, const brillo::Any&)> manager_property_changed_;

  unique_ptr<RealShillProvider> provider_;
};

void UmRealShillProviderTest::SetManagerReply(const char* default_service,
                                              bool reply_succeeds) {
  ManagerProxyMock* manager_proxy_mock = fake_shill_proxy_.GetManagerProxy();
  if (!reply_succeeds) {
    EXPECT_CALL(*manager_proxy_mock, GetProperties(_, _, _))
        .WillOnce(Return(false));
    return;
  }

  // Create a dictionary of properties and optionally include the default
  // service.
  brillo::VariantDictionary reply_dict;
  reply_dict["SomeOtherProperty"] = 0xC0FFEE;

  if (default_service) {
    reply_dict[shill::kDefaultServiceProperty] =
        dbus::ObjectPath(default_service);
  }
  EXPECT_CALL(*manager_proxy_mock, GetProperties(_, _, _))
      .WillOnce(DoAll(SetArgPointee<0>(reply_dict), Return(true)));
}

ServiceProxyMock* UmRealShillProviderTest::SetServiceReply(
    const std::string& service_path,
    const char* service_type,
    const char* physical_technology,
    const char* service_tethering) {
  brillo::VariantDictionary reply_dict;
  reply_dict["SomeOtherProperty"] = 0xC0FFEE;

  if (service_type)
    reply_dict[shill::kTypeProperty] = std::string(service_type);

  if (physical_technology) {
    reply_dict[shill::kPhysicalTechnologyProperty] =
        std::string(physical_technology);
  }

  if (service_tethering)
    reply_dict[shill::kTetheringProperty] = std::string(service_tethering);

  ServiceProxyMock* service_proxy_mock = new ServiceProxyMock();

  // Plumb return value into mock object.
  EXPECT_CALL(*service_proxy_mock, GetProperties(_, _, _))
      .WillOnce(DoAll(SetArgPointee<0>(reply_dict), Return(true)));

  fake_shill_proxy_.SetServiceForPath(
      dbus::ObjectPath(service_path),
      brillo::make_unique_ptr(service_proxy_mock));
  return service_proxy_mock;
}


// Query the connection status, type and time last changed, as they were set
// during initialization (no signals).
TEST_F(UmRealShillProviderTest, ReadBaseValues) {
  InitWithDefaultService("/");
  // Query the provider variables.
  UmTestUtils::ExpectVariableHasValue(false, provider_->var_is_connected());
  UmTestUtils::ExpectVariableNotSet(provider_->var_conn_type());
  UmTestUtils::ExpectVariableHasValue(InitTime(),
                                      provider_->var_conn_last_changed());
}

// Ensure that invalid DBus paths are ignored.
TEST_F(UmRealShillProviderTest, InvalidServicePath) {
  InitWithDefaultService("invalid");
  UmTestUtils::ExpectVariableHasValue(false, provider_->var_is_connected());
  UmTestUtils::ExpectVariableNotSet(provider_->var_conn_type());
  UmTestUtils::ExpectVariableHasValue(InitTime(),
                                      provider_->var_conn_last_changed());
}

// Ensure that a service path property including a different type is ignored.
TEST_F(UmRealShillProviderTest, InvalidServicePathType) {
  ManagerProxyMock* manager_proxy_mock = fake_shill_proxy_.GetManagerProxy();
  brillo::VariantDictionary reply_dict;
  reply_dict[shill::kDefaultServiceProperty] = "/not/an/object/path";
  EXPECT_CALL(*manager_proxy_mock, GetProperties(_, _, _))
      .WillOnce(DoAll(SetArgPointee<0>(reply_dict), Return(true)));

  EXPECT_TRUE(provider_->Init());
  EXPECT_TRUE(loop_.RunOnce(false));

  UmTestUtils::ExpectVariableHasValue(false, provider_->var_is_connected());
}

// Test that Ethernet connection is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTypeEthernet) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeEthernetServicePath,
                             shill::kTypeEthernet,
                             ConnectionType::kEthernet);
}

// Test that Wifi connection is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTypeWifi) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeWifiServicePath,
                             shill::kTypeWifi,
                             ConnectionType::kWifi);
}

// Test that Wimax connection is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTypeWimax) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeWimaxServicePath,
                             shill::kTypeWimax,
                             ConnectionType::kWimax);
}

// Test that Bluetooth connection is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTypeBluetooth) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeBluetoothServicePath,
                             shill::kTypeBluetooth,
                             ConnectionType::kBluetooth);
}

// Test that Cellular connection is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTypeCellular) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeCellularServicePath,
                             shill::kTypeCellular,
                             ConnectionType::kCellular);
}

// Test that an unknown connection is identified as such.
TEST_F(UmRealShillProviderTest, ReadConnTypeUnknown) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeUnknownServicePath,
                             "FooConnectionType",
                             ConnectionType::kUnknown);
}

// Tests that VPN connection is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTypeVpn) {
  InitWithDefaultService("/");
  // Mock logic for returning a default service path and its type.
  SetServiceReply(kFakeVpnServicePath,
                  shill::kTypeVPN,
                  shill::kTypeWifi,
                  shill::kTetheringNotDetectedState);

  // Send a signal about a new default service.
  Time conn_change_time;
  SendDefaultServiceSignal(kFakeVpnServicePath, &conn_change_time);

  // Query the connection type, ensure last change time reported correctly.
  UmTestUtils::ExpectVariableHasValue(ConnectionType::kWifi,
                                      provider_->var_conn_type());
  UmTestUtils::ExpectVariableHasValue(conn_change_time,
                                      provider_->var_conn_last_changed());
}

// Ensure that the connection type is properly cached in the provider through
// subsequent variable readings.
TEST_F(UmRealShillProviderTest, ConnTypeCacheUsed) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeEthernetServicePath,
                             shill::kTypeEthernet,
                             ConnectionType::kEthernet);

  UmTestUtils::ExpectVariableHasValue(ConnectionType::kEthernet,
                                      provider_->var_conn_type());
}

// Ensure that the cached connection type remains valid even when a default
// connection signal occurs but the connection is not changed.
TEST_F(UmRealShillProviderTest, ConnTypeCacheRemainsValid) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeEthernetServicePath,
                             shill::kTypeEthernet,
                             ConnectionType::kEthernet);

  SendDefaultServiceSignal(kFakeEthernetServicePath, nullptr);

  UmTestUtils::ExpectVariableHasValue(ConnectionType::kEthernet,
                                      provider_->var_conn_type());
}

// Ensure that the cached connection type is invalidated and re-read when the
// default connection changes.
TEST_F(UmRealShillProviderTest, ConnTypeCacheInvalidated) {
  InitWithDefaultService("/");
  SetupConnectionAndTestType(kFakeEthernetServicePath,
                             shill::kTypeEthernet,
                             ConnectionType::kEthernet);

  SetupConnectionAndTestType(kFakeWifiServicePath,
                             shill::kTypeWifi,
                             ConnectionType::kWifi);
}

// Test that a non-tethering mode is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTetheringNotDetected) {
  InitWithDefaultService("/");
  SetupConnectionAndTestTethering(kFakeWifiServicePath,
                                  shill::kTetheringNotDetectedState,
                                  ConnectionTethering::kNotDetected);
}

// Test that a suspected tethering mode is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTetheringSuspected) {
  InitWithDefaultService("/");
  SetupConnectionAndTestTethering(kFakeWifiServicePath,
                                  shill::kTetheringSuspectedState,
                                  ConnectionTethering::kSuspected);
}

// Test that a confirmed tethering mode is identified correctly.
TEST_F(UmRealShillProviderTest, ReadConnTetheringConfirmed) {
  InitWithDefaultService("/");
  SetupConnectionAndTestTethering(kFakeWifiServicePath,
                                  shill::kTetheringConfirmedState,
                                  ConnectionTethering::kConfirmed);
}

// Test that an unknown tethering mode is identified as such.
TEST_F(UmRealShillProviderTest, ReadConnTetheringUnknown) {
  InitWithDefaultService("/");
  SetupConnectionAndTestTethering(kFakeWifiServicePath,
                                  "FooConnTethering",
                                  ConnectionTethering::kUnknown);
}

// Ensure that the connection tethering mode is properly cached in the provider.
TEST_F(UmRealShillProviderTest, ConnTetheringCacheUsed) {
  InitWithDefaultService("/");
  SetupConnectionAndTestTethering(kFakeEthernetServicePath,
                                  shill::kTetheringNotDetectedState,
                                  ConnectionTethering::kNotDetected);

  UmTestUtils::ExpectVariableHasValue(ConnectionTethering::kNotDetected,
                                      provider_->var_conn_tethering());
}

// Ensure that the cached connection tethering mode remains valid even when a
// default connection signal occurs but the connection is not changed.
TEST_F(UmRealShillProviderTest, ConnTetheringCacheRemainsValid) {
  InitWithDefaultService("/");
  SetupConnectionAndTestTethering(kFakeEthernetServicePath,
                                  shill::kTetheringNotDetectedState,
                                  ConnectionTethering::kNotDetected);

  SendDefaultServiceSignal(kFakeEthernetServicePath, nullptr);

  UmTestUtils::ExpectVariableHasValue(ConnectionTethering::kNotDetected,
                                      provider_->var_conn_tethering());
}

// Ensure that the cached connection tethering mode is invalidated and re-read
// when the default connection changes.
TEST_F(UmRealShillProviderTest, ConnTetheringCacheInvalidated) {
  InitWithDefaultService("/");
  SetupConnectionAndTestTethering(kFakeEthernetServicePath,
                                  shill::kTetheringNotDetectedState,
                                  ConnectionTethering::kNotDetected);

  SetupConnectionAndTestTethering(kFakeWifiServicePath,
                                  shill::kTetheringConfirmedState,
                                  ConnectionTethering::kConfirmed);
}

// Fake two DBus signals prompting a default connection change, but otherwise
// give the same service path. Check connection status and the time it was last
// changed, making sure that it is the time when the first signal was sent (and
// not the second).
TEST_F(UmRealShillProviderTest, ReadLastChangedTimeTwoSignals) {
  InitWithDefaultService("/");
  // Send a default service signal twice, advancing the clock in between.
  Time conn_change_time;
  SetupConnectionAndAttrs(kFakeEthernetServicePath,
                          shill::kTypeEthernet,
                          shill::kTetheringNotDetectedState,
                          &conn_change_time);
  // This will set the service path to the same value, so it should not call
  // GetProperties() again.
  SendDefaultServiceSignal(kFakeEthernetServicePath, nullptr);

  // Query the connection status, ensure last change time reported as the first
  // time the signal was sent.
  UmTestUtils::ExpectVariableHasValue(true, provider_->var_is_connected());
  UmTestUtils::ExpectVariableHasValue(conn_change_time,
                                      provider_->var_conn_last_changed());
}

// Make sure that the provider initializes correctly even if shill is not
// responding, that variables can be obtained, and that they all return a null
// value (indicating that the underlying values were not set).
TEST_F(UmRealShillProviderTest, NoInitConnStatusReadBaseValues) {
  // Initialize the provider, no initial connection status response.
  SetManagerReply(nullptr, false);
  EXPECT_TRUE(provider_->Init());
  EXPECT_TRUE(loop_.RunOnce(false));
  UmTestUtils::ExpectVariableNotSet(provider_->var_is_connected());
  UmTestUtils::ExpectVariableNotSet(provider_->var_conn_type());
  UmTestUtils::ExpectVariableNotSet(provider_->var_conn_last_changed());
}

// Test that, once a signal is received, the connection status and other info
// can be read correctly.
TEST_F(UmRealShillProviderTest, NoInitConnStatusReadConnTypeEthernet) {
  // Initialize the provider with no initial connection status response.
  SetManagerReply(nullptr, false);
  EXPECT_TRUE(provider_->Init());
  EXPECT_TRUE(loop_.RunOnce(false));

  SetupConnectionAndAttrs(kFakeEthernetServicePath,
                          shill::kTypeEthernet,
                          shill::kTetheringNotDetectedState,
                          nullptr);
  UmTestUtils::ExpectVariableHasValue(true, provider_->var_is_connected());
}

}  // namespace chromeos_update_manager