// Copyright (c) 2012 The Chromium 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 "chrome/browser/chrome_to_mobile_service.h"

#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/chrome_to_mobile_service_factory.h"
#include "chrome/browser/ui/browser_command_controller.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/prefs/pref_service.h"
#include "chrome/browser/signin/token_service.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/common/chrome_notification_types.h"
#include "chrome/common/extensions/feature_switch.h"
#include "chrome/common/pref_names.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/notification_details.h"
#include "content/public/browser/notification_source.h"
#include "content/public/browser/web_contents.h"
#include "google_apis/gaia/gaia_constants.h"

namespace {

using extensions::FeatureSwitch;

class ChromeToMobileServiceTest : public BrowserWithTestWindowTest {
 public:
  ChromeToMobileServiceTest();
  virtual ~ChromeToMobileServiceTest();

  // Get the ChromeToMobileService for the browser's profile.
  ChromeToMobileService* GetService() const;

  // Ensures the returned command state matches UpdateAndGetCommandState().
  bool UpdateAndGetVerifiedCommandState();

  // Set |count| available mock mobile devices in the profile' prefs.
  void SetDeviceCount(size_t count);

  // Fulfill the requirements to enabling the feature.
  void FulfillFeatureRequirements();

 private:
  FeatureSwitch::ScopedOverride enable_action_box_;

  DISALLOW_COPY_AND_ASSIGN(ChromeToMobileServiceTest);
};

// Chrome To Mobile is currently gated on the Action Box UI,
// so need to enable this feature for the test.
ChromeToMobileServiceTest::ChromeToMobileServiceTest()
    : enable_action_box_(FeatureSwitch::action_box(), true) {}

ChromeToMobileServiceTest::~ChromeToMobileServiceTest() {}

ChromeToMobileService* ChromeToMobileServiceTest::GetService() const {
  return ChromeToMobileServiceFactory::GetForProfile(profile());
}

bool ChromeToMobileServiceTest::UpdateAndGetVerifiedCommandState() {
  bool state = ChromeToMobileService::UpdateAndGetCommandState(browser());
  CommandUpdater* updater = browser()->command_controller()->command_updater();
  EXPECT_EQ(state, updater->IsCommandEnabled(IDC_CHROME_TO_MOBILE_PAGE));
  return state;
}

void ChromeToMobileServiceTest::SetDeviceCount(size_t count) {
  ListValue mobiles;
  for (size_t i = 0; i < count; ++i)
    mobiles.Append(new DictionaryValue());
  profile()->GetPrefs()->Set(prefs::kChromeToMobileDeviceList, mobiles);
}

void ChromeToMobileServiceTest::FulfillFeatureRequirements() {
  AddTab(browser(), GURL("http://foo"));
  SetDeviceCount(1);
  GetService()->OnInvalidatorStateChange(syncer::INVALIDATIONS_ENABLED);
  EXPECT_TRUE(UpdateAndGetVerifiedCommandState());
}

// Test that GetMobiles and HasMobiles require Sync Invalidations being enabled.
TEST_F(ChromeToMobileServiceTest, GetMobiles) {
  ChromeToMobileService* service = GetService();
  EXPECT_EQ(NULL, service->GetMobiles());
  EXPECT_FALSE(service->HasMobiles());

  // Add a mock mobile device to the profile prefs.
  SetDeviceCount(1);

  // GetMobiles() still returns NULL until Sync Invalidations are enabled.
  EXPECT_EQ(NULL, service->GetMobiles());
  EXPECT_FALSE(service->HasMobiles());
  service->OnInvalidatorStateChange(syncer::INVALIDATIONS_ENABLED);
  EXPECT_EQ(1U, service->GetMobiles()->GetSize());
  EXPECT_TRUE(service->HasMobiles());

  // Adding and removing devices works while Sync Invalidations are enabled.
  SetDeviceCount(0);
  EXPECT_EQ(0U, service->GetMobiles()->GetSize());
  EXPECT_FALSE(service->HasMobiles());
  SetDeviceCount(2);
  EXPECT_EQ(2U, service->GetMobiles()->GetSize());
  EXPECT_TRUE(service->HasMobiles());

  // GetMobiles() returns NULL after Sync Invalidations are disabled.
  service->OnInvalidatorStateChange(syncer::TRANSIENT_INVALIDATION_ERROR);
  EXPECT_EQ(NULL, service->GetMobiles());
  EXPECT_FALSE(service->HasMobiles());
  service->OnInvalidatorStateChange(syncer::INVALIDATION_CREDENTIALS_REJECTED);
  EXPECT_EQ(NULL, service->GetMobiles());
  EXPECT_FALSE(service->HasMobiles());
}

// Test fulfilling the requirements to enable the feature.
TEST_F(ChromeToMobileServiceTest, RequirementsToEnable) {
  // Navigate to a page with a URL that is valid to send.
  AddTab(browser(), GURL("http://foo"));
  EXPECT_FALSE(UpdateAndGetVerifiedCommandState());

  // Add a mock mobile device to the profile prefs.
  SetDeviceCount(1);
  EXPECT_FALSE(UpdateAndGetVerifiedCommandState());

  // Send a fake notification that Sync Invalidations are enabled.
  GetService()->OnInvalidatorStateChange(syncer::INVALIDATIONS_ENABLED);
  EXPECT_TRUE(UpdateAndGetVerifiedCommandState());
}

// Test that the feature handles mobile device count changes properly.
TEST_F(ChromeToMobileServiceTest, MobileDevicesAreRequired) {
  FulfillFeatureRequirements();

  // Removing all devices disables the feature.
  SetDeviceCount(0);
  EXPECT_FALSE(UpdateAndGetVerifiedCommandState());

  // Restoring mobile devices re-enables the feature.
  SetDeviceCount(1);
  EXPECT_TRUE(UpdateAndGetVerifiedCommandState());
  SetDeviceCount(3);
  EXPECT_TRUE(UpdateAndGetVerifiedCommandState());
}

// Test that the feature handles Sync Invalidations state changes properly.
TEST_F(ChromeToMobileServiceTest, SyncInvalidationsAreRequired) {
  FulfillFeatureRequirements();

  // Disabling Sync Invalidations disables the feature.
  GetService()->OnInvalidatorStateChange(syncer::TRANSIENT_INVALIDATION_ERROR);
  EXPECT_FALSE(UpdateAndGetVerifiedCommandState());

  // Re-enabling Sync Invalidations re-enables the feature.
  GetService()->OnInvalidatorStateChange(syncer::INVALIDATIONS_ENABLED);
  EXPECT_TRUE(UpdateAndGetVerifiedCommandState());
}

// Test that the feature handles various page URLs properly.
TEST_F(ChromeToMobileServiceTest, CertainSchemesAreRequired) {
  FulfillFeatureRequirements();

  // Only http:, https:, and ftp: URLs are valid to send.
  struct {
    std::string url;
    bool enabled;
  } cases[] = {
    { "http://foo", true }, { "https://foo", true }, { "ftp://foo", true },
    { "about:foo", false }, { "chrome://foo", false }, { "file://foo", false },
    { "data://foo", false }, { "view-source:foo", false },
  };

  content::NavigationController* controller =
      &chrome::GetActiveWebContents(browser())->GetController();
  for (size_t i = 0; i < ARRAYSIZE_UNSAFE(cases); ++i) {
    NavigateAndCommit(controller, GURL(cases[i].url));
    EXPECT_EQ(cases[i].enabled, UpdateAndGetVerifiedCommandState());
  }
}

// Ensure that only relevant notifications invalidate the access token.
TEST_F(ChromeToMobileServiceTest, TokenNotifications) {
  const char dummy_string[] = "dummy";
  ChromeToMobileService* service = GetService();
  service->SetAccessTokenForTest(dummy_string);
  ASSERT_FALSE(service->GetAccessTokenForTest().empty());

  // Send dummy service/token details (should not clear the token).
  TokenService::TokenAvailableDetails dummy_details(dummy_string, dummy_string);
  service->Observe(chrome::NOTIFICATION_TOKEN_AVAILABLE,
      content::Source<ChromeToMobileServiceTest>(this),
      content::Details<TokenService::TokenAvailableDetails>(&dummy_details));
  EXPECT_FALSE(service->GetAccessTokenForTest().empty());

  // Send a Gaia OAuth2 Login service dummy token (should clear the token).
  TokenService::TokenAvailableDetails login_details(
      GaiaConstants::kGaiaOAuth2LoginRefreshToken, dummy_string);
  service->Observe(chrome::NOTIFICATION_TOKEN_AVAILABLE,
      content::Source<ChromeToMobileServiceTest>(this),
      content::Details<TokenService::TokenAvailableDetails>(&login_details));
  EXPECT_TRUE(service->GetAccessTokenForTest().empty());
}

}  // namespace