// Copyright 2015 The Weave Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <weave/device.h>

#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <weave/provider/test/fake_task_runner.h>
#include <weave/provider/test/mock_bluetooth.h>
#include <weave/provider/test/mock_config_store.h>
#include <weave/provider/test/mock_dns_service_discovery.h>
#include <weave/provider/test/mock_http_client.h>
#include <weave/provider/test/mock_http_server.h>
#include <weave/provider/test/mock_network.h>
#include <weave/provider/test/mock_wifi.h>
#include <weave/test/mock_command.h>
#include <weave/test/mock_device.h>
#include <weave/test/unittest_utils.h>

#include "src/bind_lambda.h"

using testing::_;
using testing::AtLeast;
using testing::AtMost;
using testing::HasSubstr;
using testing::InSequence;
using testing::Invoke;
using testing::InvokeWithoutArgs;
using testing::MatchesRegex;
using testing::Mock;
using testing::Return;
using testing::ReturnRefOfCopy;
using testing::StartsWith;
using testing::StrictMock;
using testing::WithArgs;

namespace weave {

namespace {

using provider::HttpClient;
using provider::Network;
using provider::test::MockHttpClientResponse;
using test::CreateDictionaryValue;
using test::ValueToString;

const char kTraitDefs[] = R"({
  "trait1": {
    "commands": {
      "reboot": {
        "minimalRole": "user"
      },
      "shutdown": {
        "minimalRole": "user",
        "parameters": {},
        "results": {}
      }
    },
    "state": {
      "firmwareVersion": {"type": "string"}
    }
  },
  "trait2": {
    "state": {
      "battery_level": {"type": "integer"}
    }
  }
})";

const char kDeviceResource[] = R"({
  "kind": "weave#device",
  "id": "CLOUD_ID",
  "channel": {
    "supportedType": "pull"
  },
  "deviceKind": "vendor",
  "modelManifestId": "ABCDE",
  "systemName": "",
  "name": "TEST_NAME",
  "displayName": "",
  "description": "Developer device",
  "stateValidationEnabled": true,
  "commandDefs":{
    "trait1": {
      "reboot": {
        "minimalRole": "user",
        "parameters": {"delay": {"type": "integer"}},
        "results": {}
      },
      "shutdown": {
        "minimalRole": "user",
        "parameters": {},
        "results": {}
      }
    }
  },
  "state":{
    "trait1": {"firmwareVersion":"FIRMWARE_VERSION"},
    "trait2": {"battery_level":44}
  },
  "traits": {
    "trait1": {
      "commands": {
        "reboot": {
          "minimalRole": "user"
        },
        "shutdown": {
          "minimalRole": "user",
          "parameters": {},
          "results": {}
        }
      },
      "state": {
        "firmwareVersion": {"type": "string"}
      }
    },
    "trait2": {
      "state": {
        "battery_level": {"type": "integer"}
      }
    }
  },
  "components": {
    "myComponent": {
      "traits": ["trait1", "trait2"],
      "state": {
        "trait1": {"firmwareVersion":"FIRMWARE_VERSION"},
        "trait2": {"battery_level":44}
      }
    }
  }
})";

const char kRegistrationResponse[] = R"({
  "kind": "weave#registrationTicket",
  "id": "TICKET_ID",
  "deviceId": "CLOUD_ID",
  "oauthClientId": "CLIENT_ID",
  "userEmail": "USER@gmail.com",
  "creationTimeMs": "1440087183738",
  "expirationTimeMs": "1440087423738"
})";

const char kRegistrationFinalResponse[] = R"({
  "kind": "weave#registrationTicket",
  "id": "TICKET_ID",
  "deviceId": "CLOUD_ID",
  "oauthClientId": "CLIENT_ID",
  "userEmail": "USER@gmail.com",
  "robotAccountEmail": "ROBO@gmail.com",
  "robotAccountAuthorizationCode": "AUTH_CODE",
  "creationTimeMs": "1440087183738",
  "expirationTimeMs": "1440087423738"
})";

const char kAuthTokenResponse[] = R"({
  "access_token" : "ACCESS_TOKEN",
  "token_type" : "Bearer",
  "expires_in" : 3599,
  "refresh_token" : "REFRESH_TOKEN"
})";

MATCHER_P(MatchTxt, txt, "") {
  std::vector<std::string> txt_copy = txt;
  std::sort(txt_copy.begin(), txt_copy.end());
  std::vector<std::string> arg_copy = arg;
  std::sort(arg_copy.begin(), arg_copy.end());
  return (arg_copy == txt_copy);
}

template <class Map>
std::set<typename Map::key_type> GetKeys(const Map& map) {
  std::set<typename Map::key_type> result;
  for (const auto& pair : map)
    result.insert(pair.first);
  return result;
}

}  // namespace

class WeaveTest : public ::testing::Test {
 protected:
  void SetUp() override {
    EXPECT_CALL(wifi_, IsWifi24Supported()).WillRepeatedly(Return(true));
    EXPECT_CALL(wifi_, IsWifi50Supported()).WillRepeatedly(Return(false));
  }

  template <class UrlMatcher>
  void ExpectRequest(HttpClient::Method method,
                     const UrlMatcher& url_matcher,
                     const std::string& json_response) {
    EXPECT_CALL(http_client_, SendRequest(method, url_matcher, _, _, _))
        .WillOnce(WithArgs<4>(Invoke(
            [json_response](const HttpClient::SendRequestCallback& callback) {
              std::unique_ptr<provider::test::MockHttpClientResponse> response{
                  new StrictMock<provider::test::MockHttpClientResponse>};
              EXPECT_CALL(*response, GetStatusCode())
                  .Times(AtLeast(1))
                  .WillRepeatedly(Return(200));
              EXPECT_CALL(*response, GetContentType())
                  .Times(AtLeast(1))
                  .WillRepeatedly(Return("application/json; charset=utf-8"));
              EXPECT_CALL(*response, GetData())
                  .WillRepeatedly(Return(json_response));
              callback.Run(std::move(response), nullptr);
            })));
  }

  void InitNetwork() {
    EXPECT_CALL(network_, AddConnectionChangedCallback(_))
        .WillRepeatedly(Invoke(
            [this](const provider::Network::ConnectionChangedCallback& cb) {
              network_callbacks_.push_back(cb);
            }));
    EXPECT_CALL(network_, GetConnectionState())
        .WillRepeatedly(Return(Network::State::kOffline));
  }

  void InitDnsSd() {
    EXPECT_CALL(dns_sd_, PublishService(_, _, _)).WillRepeatedly(Return());
    EXPECT_CALL(dns_sd_, StopPublishing("_privet._tcp")).WillOnce(Return());
  }

  void InitDnsSdPublishing(bool registered, const std::string& flags) {
    std::vector<std::string> txt{
        {"id=TEST_DEVICE_ID"},         {"flags=" + flags}, {"mmid=ABCDE"},
        {"services=developmentBoard"}, {"txtvers=3"},      {"ty=TEST_NAME"}};
    if (registered) {
      txt.push_back("gcd_id=CLOUD_ID");

      // During registration device may announce itself twice:
      // 1. with GCD ID but not connected (DB)
      // 2. with GCD ID and connected (BB)
      EXPECT_CALL(dns_sd_, PublishService("_privet._tcp", 11, MatchTxt(txt)))
          .Times(AtMost(1))
          .WillOnce(Return());

      txt[1] = "flags=BB";
    }

    EXPECT_CALL(dns_sd_, PublishService("_privet._tcp", 11, MatchTxt(txt)))
        .Times(AtMost(1))
        .WillOnce(Return());
  }

  void InitHttpServer() {
    EXPECT_CALL(http_server_, GetHttpPort()).WillRepeatedly(Return(11));
    EXPECT_CALL(http_server_, GetHttpsPort()).WillRepeatedly(Return(12));
    EXPECT_CALL(http_server_, GetRequestTimeout())
        .WillRepeatedly(Return(base::TimeDelta::Max()));
    EXPECT_CALL(http_server_, GetHttpsCertificateFingerprint())
        .WillRepeatedly(Return(std::vector<uint8_t>{1, 2, 3}));
    EXPECT_CALL(http_server_, AddHttpRequestHandler(_, _))
        .WillRepeatedly(Invoke(
            [this](const std::string& path_prefix,
                   const provider::HttpServer::RequestHandlerCallback& cb) {
              http_handlers_[path_prefix] = cb;
            }));
    EXPECT_CALL(http_server_, AddHttpsRequestHandler(_, _))
        .WillRepeatedly(Invoke(
            [this](const std::string& path_prefix,
                   const provider::HttpServer::RequestHandlerCallback& cb) {
              https_handlers_[path_prefix] = cb;
            }));
  }

  void InitDefaultExpectations() {
    InitNetwork();
    EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
        .WillOnce(Return());
    InitHttpServer();
    InitDnsSd();
  }

  void StartDevice() {
    device_ = weave::Device::Create(&config_store_, &task_runner_,
                                    &http_client_, &network_, &dns_sd_,
                                    &http_server_, &wifi_, &bluetooth_);

    EXPECT_EQ((std::set<std::string>{
                  // clang-format off
                  "/privet/info",
                  "/privet/v3/pairing/cancel",
                  "/privet/v3/pairing/confirm",
                  "/privet/v3/pairing/start",
                  // clang-format on
              }),
              GetKeys(http_handlers_));
    EXPECT_EQ((std::set<std::string>{
                  // clang-format off
                  "/privet/info",
                  "/privet/v3/accessControl/claim",
                  "/privet/v3/accessControl/confirm",
                  "/privet/v3/auth",
                  "/privet/v3/checkForUpdates",
                  "/privet/v3/commandDefs",
                  "/privet/v3/commands/cancel",
                  "/privet/v3/commands/execute",
                  "/privet/v3/commands/list",
                  "/privet/v3/commands/status",
                  "/privet/v3/components",
                  "/privet/v3/pairing/cancel",
                  "/privet/v3/pairing/confirm",
                  "/privet/v3/pairing/start",
                  "/privet/v3/setup/start",
                  "/privet/v3/setup/status",
                  "/privet/v3/state",
                  "/privet/v3/traits",
                  // clang-format on
              }),
              GetKeys(https_handlers_));

    device_->AddTraitDefinitionsFromJson(kTraitDefs);
    EXPECT_TRUE(
        device_->AddComponent("myComponent", {"trait1", "trait2"}, nullptr));
    EXPECT_TRUE(device_->SetStatePropertiesFromJson(
        "myComponent", R"({"trait2": {"battery_level":44}})", nullptr));

    task_runner_.Run();
  }

  void NotifyNetworkChanged(provider::Network::State state,
                            base::TimeDelta delay) {
    auto task = [this, state] {
      EXPECT_CALL(network_, GetConnectionState()).WillRepeatedly(Return(state));
      for (const auto& cb : network_callbacks_)
        cb.Run();
    };

    task_runner_.PostDelayedTask(FROM_HERE, base::Bind(task), delay);
  }

  std::map<std::string, provider::HttpServer::RequestHandlerCallback>
      http_handlers_;
  std::map<std::string, provider::HttpServer::RequestHandlerCallback>
      https_handlers_;

  StrictMock<provider::test::MockConfigStore> config_store_;
  StrictMock<provider::test::FakeTaskRunner> task_runner_;
  StrictMock<provider::test::MockHttpClient> http_client_;
  StrictMock<provider::test::MockNetwork> network_;
  StrictMock<provider::test::MockDnsServiceDiscovery> dns_sd_;
  StrictMock<provider::test::MockHttpServer> http_server_;
  StrictMock<provider::test::MockWifi> wifi_;
  StrictMock<provider::test::MockBluetooth> bluetooth_;

  std::vector<provider::Network::ConnectionChangedCallback> network_callbacks_;

  std::unique_ptr<weave::Device> device_;
};

TEST_F(WeaveTest, Mocks) {
  // Test checks if mock implements entire interface and mock can be
  // instantiated.
  test::MockDevice device;
  test::MockCommand command;
}

TEST_F(WeaveTest, StartMinimal) {
  device_ = weave::Device::Create(&config_store_, &task_runner_, &http_client_,
                                  &network_, nullptr, nullptr, &wifi_, nullptr);
}

TEST_F(WeaveTest, StartNoWifi) {
  InitNetwork();
  InitHttpServer();
  InitDnsSd();
  InitDnsSdPublishing(false, "CB");

  device_ = weave::Device::Create(&config_store_, &task_runner_, &http_client_,
                                  &network_, &dns_sd_, &http_server_, nullptr,
                                  &bluetooth_);
  device_->AddTraitDefinitionsFromJson(kTraitDefs);
  EXPECT_TRUE(
      device_->AddComponent("myComponent", {"trait1", "trait2"}, nullptr));

  task_runner_.Run();
}

class WeaveBasicTest : public WeaveTest {
 public:
  void SetUp() override {
    WeaveTest::SetUp();

    InitDefaultExpectations();
    InitDnsSdPublishing(false, "DB");
  }
};

TEST_F(WeaveBasicTest, Start) {
  StartDevice();
}

TEST_F(WeaveBasicTest, Register) {
  EXPECT_CALL(network_, OpenSslSocket(_, _, _)).WillRepeatedly(Return());
  StartDevice();

  auto draft = CreateDictionaryValue(kDeviceResource);
  auto response = CreateDictionaryValue(kRegistrationResponse);
  response->Set("deviceDraft", draft->DeepCopy());
  ExpectRequest(HttpClient::Method::kPatch,
                "https://www.googleapis.com/weave/v1/registrationTickets/"
                "TICKET_ID?key=TEST_API_KEY",
                ValueToString(*response));

  response = CreateDictionaryValue(kRegistrationFinalResponse);
  response->Set("deviceDraft", draft->DeepCopy());
  ExpectRequest(HttpClient::Method::kPost,
                "https://www.googleapis.com/weave/v1/registrationTickets/"
                "TICKET_ID/finalize?key=TEST_API_KEY",
                ValueToString(*response));

  ExpectRequest(HttpClient::Method::kPost,
                "https://accounts.google.com/o/oauth2/token",
                kAuthTokenResponse);

  ExpectRequest(HttpClient::Method::kPost, HasSubstr("upsertLocalAuthInfo"),
                {});

  InitDnsSdPublishing(true, "DB");

  bool done = false;
  device_->Register("TICKET_ID", base::Bind([this, &done](ErrorPtr error) {
                      EXPECT_FALSE(error);
                      done = true;
                      task_runner_.Break();
                      EXPECT_EQ("CLOUD_ID", device_->GetSettings().cloud_id);
                    }));
  task_runner_.Run();
  EXPECT_TRUE(done);

  done = false;
  device_->Register("TICKET_ID2", base::Bind([this, &done](ErrorPtr error) {
                      EXPECT_TRUE(error->HasError("already_registered"));
                      done = true;
                      task_runner_.Break();
                      EXPECT_EQ("CLOUD_ID", device_->GetSettings().cloud_id);
                    }));
  task_runner_.Run();
  EXPECT_TRUE(done);
}

class WeaveWiFiSetupTest : public WeaveTest {
 public:
  void SetUp() override {
    WeaveTest::SetUp();

    InitHttpServer();
    InitNetwork();
    InitDnsSd();

    EXPECT_CALL(network_, GetConnectionState())
        .WillRepeatedly(Return(provider::Network::State::kOnline));
  }
};

TEST_F(WeaveWiFiSetupTest, StartOnlineNoPrevSsid) {
  StartDevice();

  // Short disconnect.
  NotifyNetworkChanged(provider::Network::State::kOffline, {});
  NotifyNetworkChanged(provider::Network::State::kOnline,
                       base::TimeDelta::FromSeconds(10));
  task_runner_.Run();

  // Long disconnect.
  NotifyNetworkChanged(Network::State::kOffline, {});
  auto offline_from = task_runner_.GetClock()->Now();
  EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
      .WillOnce(InvokeWithoutArgs([this, offline_from]() {
        EXPECT_GT(task_runner_.GetClock()->Now() - offline_from,
                  base::TimeDelta::FromMinutes(1));
        task_runner_.Break();
      }));
  task_runner_.Run();
}

// If device has previously configured WiFi it will run AP for limited time
// after which it will try to re-connect.
TEST_F(WeaveWiFiSetupTest, StartOnlineWithPrevSsid) {
  EXPECT_CALL(config_store_, LoadSettings())
      .WillRepeatedly(Return(R"({"last_configured_ssid": "TEST_ssid"})"));
  StartDevice();

  // Long disconnect.
  NotifyNetworkChanged(Network::State::kOffline, {});

  for (int i = 0; i < 5; ++i) {
    auto offline_from = task_runner_.GetClock()->Now();
    // Temporarily offline mode.
    EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
        .WillOnce(InvokeWithoutArgs([this, &offline_from]() {
          EXPECT_GT(task_runner_.GetClock()->Now() - offline_from,
                    base::TimeDelta::FromMinutes(1));
          task_runner_.Break();
        }));
    task_runner_.Run();

    // Try to reconnect again.
    offline_from = task_runner_.GetClock()->Now();
    EXPECT_CALL(wifi_, StopAccessPoint())
        .WillOnce(InvokeWithoutArgs([this, offline_from]() {
          EXPECT_GT(task_runner_.GetClock()->Now() - offline_from,
                    base::TimeDelta::FromMinutes(5));
          task_runner_.Break();
        }));
    task_runner_.Run();
  }

  NotifyNetworkChanged(Network::State::kOnline, {});
  task_runner_.Run();
}

TEST_F(WeaveWiFiSetupTest, StartOfflineWithSsid) {
  EXPECT_CALL(config_store_, LoadSettings())
      .WillRepeatedly(Return(R"({"last_configured_ssid": "TEST_ssid"})"));
  EXPECT_CALL(network_, GetConnectionState())
      .WillRepeatedly(Return(Network::State::kOffline));

  auto offline_from = task_runner_.GetClock()->Now();
  EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
      .WillOnce(InvokeWithoutArgs([this, &offline_from]() {
        EXPECT_GT(task_runner_.GetClock()->Now() - offline_from,
                  base::TimeDelta::FromMinutes(1));
        task_runner_.Break();
      }));

  StartDevice();
}

TEST_F(WeaveWiFiSetupTest, OfflineLongTimeWithNoSsid) {
  EXPECT_CALL(network_, GetConnectionState())
      .WillRepeatedly(Return(Network::State::kOffline));
  NotifyNetworkChanged(provider::Network::State::kOnline,
                       base::TimeDelta::FromHours(15));

  {
    InSequence s;
    auto time_stamp = task_runner_.GetClock()->Now();

    EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
        .WillOnce(InvokeWithoutArgs([this, &time_stamp]() {
          EXPECT_LE(task_runner_.GetClock()->Now() - time_stamp,
                    base::TimeDelta::FromMinutes(1));
          time_stamp = task_runner_.GetClock()->Now();
        }));

    EXPECT_CALL(wifi_, StopAccessPoint())
        .WillOnce(InvokeWithoutArgs([this, &time_stamp]() {
          EXPECT_GT(task_runner_.GetClock()->Now() - time_stamp,
                    base::TimeDelta::FromMinutes(5));
          time_stamp = task_runner_.GetClock()->Now();
          task_runner_.Break();
        }));
  }

  StartDevice();
}

TEST_F(WeaveWiFiSetupTest, OfflineLongTimeWithSsid) {
  EXPECT_CALL(config_store_, LoadSettings())
      .WillRepeatedly(Return(R"({"last_configured_ssid": "TEST_ssid"})"));
  EXPECT_CALL(network_, GetConnectionState())
      .WillRepeatedly(Return(Network::State::kOffline));
  NotifyNetworkChanged(provider::Network::State::kOnline,
                       base::TimeDelta::FromHours(15));

  {
    InSequence s;
    auto time_stamp = task_runner_.GetClock()->Now();
    for (size_t i = 0; i < 10; ++i) {
      EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
          .WillOnce(InvokeWithoutArgs([this, &time_stamp]() {
            EXPECT_GT(task_runner_.GetClock()->Now() - time_stamp,
                      base::TimeDelta::FromMinutes(1));
            time_stamp = task_runner_.GetClock()->Now();
          }));

      EXPECT_CALL(wifi_, StopAccessPoint())
          .WillOnce(InvokeWithoutArgs([this, &time_stamp]() {
            EXPECT_GT(task_runner_.GetClock()->Now() - time_stamp,
                      base::TimeDelta::FromMinutes(5));
            time_stamp = task_runner_.GetClock()->Now();
          }));
    }

    EXPECT_CALL(wifi_, StartAccessPoint(MatchesRegex("TEST_NAME.*prv")))
        .WillOnce(InvokeWithoutArgs([this]() { task_runner_.Break(); }));
  }

  StartDevice();
}

}  // namespace weave