// 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 <utility>

#include "base/bind.h"
#include "base/json/json_writer.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/values.h"
#include "chrome/browser/extensions/api/storage/settings_sync_util.h"
#include "chrome/browser/extensions/extension_apitest.h"
#include "chrome/browser/extensions/extension_system_factory.h"
#include "chrome/browser/policy/schema_registry_service.h"
#include "chrome/browser/policy/schema_registry_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_bundle.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_namespace.h"
#include "components/policy/core/common/policy_types.h"
#include "components/policy/core/common/schema.h"
#include "components/policy/core/common/schema_map.h"
#include "components/policy/core/common/schema_registry.h"
#include "extensions/browser/api/storage/settings_namespace.h"
#include "extensions/browser/api/storage/storage_frontend.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/value_builder.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "sync/api/fake_sync_change_processor.h"
#include "sync/api/sync_change.h"
#include "sync/api/sync_change_processor.h"
#include "sync/api/sync_change_processor_wrapper_for_test.h"
#include "sync/api/sync_error_factory.h"
#include "sync/api/sync_error_factory_mock.h"
#include "sync/api/syncable_service.h"
#include "testing/gmock/include/gmock/gmock.h"

namespace extensions {

using settings_namespace::LOCAL;
using settings_namespace::MANAGED;
using settings_namespace::Namespace;
using settings_namespace::SYNC;
using settings_namespace::ToString;
using testing::Mock;
using testing::Return;
using testing::_;

namespace {

// TODO(kalman): test both EXTENSION_SETTINGS and APP_SETTINGS.
const syncer::ModelType kModelType = syncer::EXTENSION_SETTINGS;

// The managed_storage extension has a key defined in its manifest, so that
// its extension ID is well-known and the policy system can push policies for
// the extension.
const char kManagedStorageExtensionId[] = "kjmkgkdkpedkejedfhmfcenooemhbpbo";

class MockSchemaRegistryObserver : public policy::SchemaRegistry::Observer {
 public:
  MockSchemaRegistryObserver() {}
  virtual ~MockSchemaRegistryObserver() {}

  MOCK_METHOD1(OnSchemaRegistryUpdated, void(bool));
};

}  // namespace

class ExtensionSettingsApiTest : public ExtensionApiTest {
 protected:
  void SetUpInProcessBrowserTestFixture() override {
    ExtensionApiTest::SetUpInProcessBrowserTestFixture();

    EXPECT_CALL(policy_provider_, IsInitializationComplete(_))
        .WillRepeatedly(Return(true));
    policy_provider_.SetAutoRefresh();
    policy::BrowserPolicyConnector::SetPolicyProviderForTesting(
        &policy_provider_);
  }

  void ReplyWhenSatisfied(
      Namespace settings_namespace,
      const std::string& normal_action,
      const std::string& incognito_action) {
    MaybeLoadAndReplyWhenSatisfied(
        settings_namespace, normal_action, incognito_action, NULL, false);
  }

  const Extension* LoadAndReplyWhenSatisfied(
      Namespace settings_namespace,
      const std::string& normal_action,
      const std::string& incognito_action,
      const std::string& extension_dir) {
    return MaybeLoadAndReplyWhenSatisfied(
        settings_namespace,
        normal_action,
        incognito_action,
        &extension_dir,
        false);
  }

  void FinalReplyWhenSatisfied(
      Namespace settings_namespace,
      const std::string& normal_action,
      const std::string& incognito_action) {
    MaybeLoadAndReplyWhenSatisfied(
        settings_namespace, normal_action, incognito_action, NULL, true);
  }

  syncer::SyncableService* GetSyncableService() {
    return settings_sync_util::GetSyncableService(browser()->profile(),
                                                  kModelType);
  }

  void InitSync(syncer::SyncChangeProcessor* sync_processor) {
    base::MessageLoop::current()->RunUntilIdle();
    InitSyncWithSyncableService(sync_processor, GetSyncableService());
  }

  void SendChanges(const syncer::SyncChangeList& change_list) {
    base::MessageLoop::current()->RunUntilIdle();
    SendChangesToSyncableService(change_list, GetSyncableService());
  }

  void SetPolicies(const base::DictionaryValue& policies) {
    scoped_ptr<policy::PolicyBundle> bundle(new policy::PolicyBundle());
    policy::PolicyMap& policy_map = bundle->Get(policy::PolicyNamespace(
        policy::POLICY_DOMAIN_EXTENSIONS, kManagedStorageExtensionId));
    policy_map.LoadFrom(&policies, policy::POLICY_LEVEL_MANDATORY,
                        policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_CLOUD);
    policy_provider_.UpdatePolicy(std::move(bundle));
  }

 private:
  const Extension* MaybeLoadAndReplyWhenSatisfied(
      Namespace settings_namespace,
      const std::string& normal_action,
      const std::string& incognito_action,
      // May be NULL to imply not loading the extension.
      const std::string* extension_dir,
      bool is_final_action) {
    ExtensionTestMessageListener listener("waiting", true);
    ExtensionTestMessageListener listener_incognito("waiting_incognito", true);

    // Only load the extension after the listeners have been set up, to avoid
    // initialisation race conditions.
    const Extension* extension = NULL;
    if (extension_dir) {
      extension = LoadExtensionIncognito(
          test_data_dir_.AppendASCII("settings").AppendASCII(*extension_dir));
      EXPECT_TRUE(extension);
    }

    EXPECT_TRUE(listener.WaitUntilSatisfied());
    EXPECT_TRUE(listener_incognito.WaitUntilSatisfied());

    listener.Reply(
        CreateMessage(settings_namespace, normal_action, is_final_action));
    listener_incognito.Reply(
        CreateMessage(settings_namespace, incognito_action, is_final_action));
    return extension;
  }

  std::string CreateMessage(
      Namespace settings_namespace,
      const std::string& action,
      bool is_final_action) {
    base::DictionaryValue message;
    message.SetString("namespace", ToString(settings_namespace));
    message.SetString("action", action);
    message.SetBoolean("isFinalAction", is_final_action);
    std::string message_json;
    base::JSONWriter::Write(message, &message_json);
    return message_json;
  }

  void InitSyncWithSyncableService(
      syncer::SyncChangeProcessor* sync_processor,
      syncer::SyncableService* settings_service) {
    EXPECT_FALSE(
        settings_service->MergeDataAndStartSyncing(
                              kModelType,
                              syncer::SyncDataList(),
                              scoped_ptr<syncer::SyncChangeProcessor>(
                                  new syncer::SyncChangeProcessorWrapperForTest(
                                      sync_processor)),
                              scoped_ptr<syncer::SyncErrorFactory>(
                                  new syncer::SyncErrorFactoryMock()))
            .error()
            .IsSet());
  }

  void SendChangesToSyncableService(
      const syncer::SyncChangeList& change_list,
      syncer::SyncableService* settings_service) {
    EXPECT_FALSE(
        settings_service->ProcessSyncChanges(FROM_HERE, change_list).IsSet());
  }

 protected:
  policy::MockConfigurationPolicyProvider policy_provider_;
};

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest, SimpleTest) {
  ASSERT_TRUE(RunExtensionTest("settings/simple_test")) << message_;
}

// Structure of this test taken from IncognitoSplitMode.
// Note that only split-mode incognito is tested, because spanning mode
// incognito looks the same as normal mode when the only API activity comes
// from background pages.
IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest, SplitModeIncognito) {
  // We need 2 ResultCatchers because we'll be running the same test in both
  // regular and incognito mode.
  ResultCatcher catcher, catcher_incognito;
  catcher.RestrictToBrowserContext(browser()->profile());
  catcher_incognito.RestrictToBrowserContext(
      browser()->profile()->GetOffTheRecordProfile());

  LoadAndReplyWhenSatisfied(SYNC,
      "assertEmpty", "assertEmpty", "split_incognito");
  ReplyWhenSatisfied(SYNC, "noop", "setFoo");
  ReplyWhenSatisfied(SYNC, "assertFoo", "assertFoo");
  ReplyWhenSatisfied(SYNC, "clear", "noop");
  ReplyWhenSatisfied(SYNC, "assertEmpty", "assertEmpty");
  ReplyWhenSatisfied(SYNC, "setFoo", "noop");
  ReplyWhenSatisfied(SYNC, "assertFoo", "assertFoo");
  ReplyWhenSatisfied(SYNC, "noop", "removeFoo");
  FinalReplyWhenSatisfied(SYNC, "assertEmpty", "assertEmpty");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
  EXPECT_TRUE(catcher_incognito.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest,
    OnChangedNotificationsBetweenBackgroundPages) {
  // We need 2 ResultCatchers because we'll be running the same test in both
  // regular and incognito mode.
  ResultCatcher catcher, catcher_incognito;
  catcher.RestrictToBrowserContext(browser()->profile());
  catcher_incognito.RestrictToBrowserContext(
      browser()->profile()->GetOffTheRecordProfile());

  LoadAndReplyWhenSatisfied(SYNC,
      "assertNoNotifications", "assertNoNotifications", "split_incognito");
  ReplyWhenSatisfied(SYNC, "noop", "setFoo");
  ReplyWhenSatisfied(SYNC,
      "assertAddFooNotification", "assertAddFooNotification");
  ReplyWhenSatisfied(SYNC, "clearNotifications", "clearNotifications");
  ReplyWhenSatisfied(SYNC, "removeFoo", "noop");
  FinalReplyWhenSatisfied(SYNC,
      "assertDeleteFooNotification", "assertDeleteFooNotification");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
  EXPECT_TRUE(catcher_incognito.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest,
    SyncAndLocalAreasAreSeparate) {
  // We need 2 ResultCatchers because we'll be running the same test in both
  // regular and incognito mode.
  ResultCatcher catcher, catcher_incognito;
  catcher.RestrictToBrowserContext(browser()->profile());
  catcher_incognito.RestrictToBrowserContext(
      browser()->profile()->GetOffTheRecordProfile());

  LoadAndReplyWhenSatisfied(SYNC,
      "assertNoNotifications", "assertNoNotifications", "split_incognito");

  ReplyWhenSatisfied(SYNC, "noop", "setFoo");
  ReplyWhenSatisfied(SYNC, "assertFoo", "assertFoo");
  ReplyWhenSatisfied(SYNC,
      "assertAddFooNotification", "assertAddFooNotification");
  ReplyWhenSatisfied(LOCAL, "assertEmpty", "assertEmpty");
  ReplyWhenSatisfied(LOCAL, "assertNoNotifications", "assertNoNotifications");

  ReplyWhenSatisfied(SYNC, "clearNotifications", "clearNotifications");

  ReplyWhenSatisfied(LOCAL, "setFoo", "noop");
  ReplyWhenSatisfied(LOCAL, "assertFoo", "assertFoo");
  ReplyWhenSatisfied(LOCAL,
      "assertAddFooNotification", "assertAddFooNotification");
  ReplyWhenSatisfied(SYNC, "assertFoo", "assertFoo");
  ReplyWhenSatisfied(SYNC, "assertNoNotifications", "assertNoNotifications");

  ReplyWhenSatisfied(LOCAL, "clearNotifications", "clearNotifications");

  ReplyWhenSatisfied(LOCAL, "noop", "removeFoo");
  ReplyWhenSatisfied(LOCAL, "assertEmpty", "assertEmpty");
  ReplyWhenSatisfied(LOCAL,
      "assertDeleteFooNotification", "assertDeleteFooNotification");
  ReplyWhenSatisfied(SYNC, "assertFoo", "assertFoo");
  ReplyWhenSatisfied(SYNC, "assertNoNotifications", "assertNoNotifications");

  ReplyWhenSatisfied(LOCAL, "clearNotifications", "clearNotifications");

  ReplyWhenSatisfied(SYNC, "removeFoo", "noop");
  ReplyWhenSatisfied(SYNC, "assertEmpty", "assertEmpty");
  ReplyWhenSatisfied(SYNC,
      "assertDeleteFooNotification", "assertDeleteFooNotification");
  ReplyWhenSatisfied(LOCAL, "assertNoNotifications", "assertNoNotifications");
  FinalReplyWhenSatisfied(LOCAL, "assertEmpty", "assertEmpty");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
  EXPECT_TRUE(catcher_incognito.GetNextResult()) << catcher.message();
}

// Disabled, see crbug.com/101110
IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest,
    DISABLED_OnChangedNotificationsFromSync) {
  // We need 2 ResultCatchers because we'll be running the same test in both
  // regular and incognito mode.
  ResultCatcher catcher, catcher_incognito;
  catcher.RestrictToBrowserContext(browser()->profile());
  catcher_incognito.RestrictToBrowserContext(
      browser()->profile()->GetOffTheRecordProfile());

  const Extension* extension =
      LoadAndReplyWhenSatisfied(SYNC,
          "assertNoNotifications", "assertNoNotifications", "split_incognito");
  const std::string& extension_id = extension->id();

  syncer::FakeSyncChangeProcessor sync_processor;
  InitSync(&sync_processor);

  // Set "foo" to "bar" via sync.
  syncer::SyncChangeList sync_changes;
  base::StringValue bar("bar");
  sync_changes.push_back(settings_sync_util::CreateAdd(
      extension_id, "foo", bar, kModelType));
  SendChanges(sync_changes);

  ReplyWhenSatisfied(SYNC,
      "assertAddFooNotification", "assertAddFooNotification");
  ReplyWhenSatisfied(SYNC, "clearNotifications", "clearNotifications");

  // Remove "foo" via sync.
  sync_changes.clear();
  sync_changes.push_back(settings_sync_util::CreateDelete(
      extension_id, "foo", kModelType));
  SendChanges(sync_changes);

  FinalReplyWhenSatisfied(SYNC,
      "assertDeleteFooNotification", "assertDeleteFooNotification");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
  EXPECT_TRUE(catcher_incognito.GetNextResult()) << catcher.message();
}

// Disabled, see crbug.com/101110
//
// TODO: boring test, already done in the unit tests.  What we really should be
// be testing is that the areas don't overlap.
IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest,
    DISABLED_OnChangedNotificationsFromSyncNotSentToLocal) {
  // We need 2 ResultCatchers because we'll be running the same test in both
  // regular and incognito mode.
  ResultCatcher catcher, catcher_incognito;
  catcher.RestrictToBrowserContext(browser()->profile());
  catcher_incognito.RestrictToBrowserContext(
      browser()->profile()->GetOffTheRecordProfile());

  const Extension* extension =
      LoadAndReplyWhenSatisfied(LOCAL,
          "assertNoNotifications", "assertNoNotifications", "split_incognito");
  const std::string& extension_id = extension->id();

  syncer::FakeSyncChangeProcessor sync_processor;
  InitSync(&sync_processor);

  // Set "foo" to "bar" via sync.
  syncer::SyncChangeList sync_changes;
  base::StringValue bar("bar");
  sync_changes.push_back(settings_sync_util::CreateAdd(
      extension_id, "foo", bar, kModelType));
  SendChanges(sync_changes);

  ReplyWhenSatisfied(LOCAL, "assertNoNotifications", "assertNoNotifications");

  // Remove "foo" via sync.
  sync_changes.clear();
  sync_changes.push_back(settings_sync_util::CreateDelete(
      extension_id, "foo", kModelType));
  SendChanges(sync_changes);

  FinalReplyWhenSatisfied(LOCAL,
      "assertNoNotifications", "assertNoNotifications");

  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
  EXPECT_TRUE(catcher_incognito.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest, IsStorageEnabled) {
  StorageFrontend* frontend = StorageFrontend::Get(browser()->profile());
  EXPECT_TRUE(frontend->IsStorageEnabled(LOCAL));
  EXPECT_TRUE(frontend->IsStorageEnabled(SYNC));

  EXPECT_TRUE(frontend->IsStorageEnabled(MANAGED));
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest, ExtensionsSchemas) {
  // Verifies that the Schemas for the extensions domain are created on startup.
  Profile* profile = browser()->profile();
  ExtensionSystem* extension_system = ExtensionSystem::Get(profile);
  if (!extension_system->ready().is_signaled()) {
    // Wait until the extension system is ready.
    base::RunLoop run_loop;
    extension_system->ready().Post(FROM_HERE, run_loop.QuitClosure());
    run_loop.Run();
    ASSERT_TRUE(extension_system->ready().is_signaled());
  }

  // This test starts without any test extensions installed.
  EXPECT_FALSE(GetSingleLoadedExtension());
  message_.clear();

  policy::SchemaRegistry* registry =
      policy::SchemaRegistryServiceFactory::GetForContext(profile)->registry();
  ASSERT_TRUE(registry);
  EXPECT_FALSE(registry->schema_map()->GetSchema(policy::PolicyNamespace(
      policy::POLICY_DOMAIN_EXTENSIONS, kManagedStorageExtensionId)));

  MockSchemaRegistryObserver observer;
  registry->AddObserver(&observer);

  // Install a managed extension.
  EXPECT_CALL(observer, OnSchemaRegistryUpdated(true));
  const Extension* extension =
      LoadExtension(test_data_dir_.AppendASCII("settings/managed_storage"));
  ASSERT_TRUE(extension);
  Mock::VerifyAndClearExpectations(&observer);
  registry->RemoveObserver(&observer);

  // Verify that its schema has been published, and verify its contents.
  const policy::Schema* schema =
      registry->schema_map()->GetSchema(policy::PolicyNamespace(
          policy::POLICY_DOMAIN_EXTENSIONS, kManagedStorageExtensionId));
  ASSERT_TRUE(schema);

  ASSERT_TRUE(schema->valid());
  ASSERT_EQ(base::Value::TYPE_DICTIONARY, schema->type());
  ASSERT_TRUE(schema->GetKnownProperty("string-policy").valid());
  EXPECT_EQ(base::Value::TYPE_STRING,
            schema->GetKnownProperty("string-policy").type());
  ASSERT_TRUE(schema->GetKnownProperty("int-policy").valid());
  EXPECT_EQ(base::Value::TYPE_INTEGER,
            schema->GetKnownProperty("int-policy").type());
  ASSERT_TRUE(schema->GetKnownProperty("double-policy").valid());
  EXPECT_EQ(base::Value::TYPE_DOUBLE,
            schema->GetKnownProperty("double-policy").type());
  ASSERT_TRUE(schema->GetKnownProperty("boolean-policy").valid());
  EXPECT_EQ(base::Value::TYPE_BOOLEAN,
            schema->GetKnownProperty("boolean-policy").type());

  policy::Schema list = schema->GetKnownProperty("list-policy");
  ASSERT_TRUE(list.valid());
  ASSERT_EQ(base::Value::TYPE_LIST, list.type());
  ASSERT_TRUE(list.GetItems().valid());
  EXPECT_EQ(base::Value::TYPE_STRING, list.GetItems().type());

  policy::Schema dict = schema->GetKnownProperty("dict-policy");
  ASSERT_TRUE(dict.valid());
  ASSERT_EQ(base::Value::TYPE_DICTIONARY, dict.type());
  list = dict.GetKnownProperty("list");
  ASSERT_TRUE(list.valid());
  ASSERT_EQ(base::Value::TYPE_LIST, list.type());
  dict = list.GetItems();
  ASSERT_TRUE(dict.valid());
  ASSERT_EQ(base::Value::TYPE_DICTIONARY, dict.type());
  ASSERT_TRUE(dict.GetProperty("anything").valid());
  EXPECT_EQ(base::Value::TYPE_INTEGER, dict.GetProperty("anything").type());
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest, ManagedStorage) {
  // Set policies for the test extension.
  scoped_ptr<base::DictionaryValue> policy =
      extensions::DictionaryBuilder()
          .Set("string-policy", "value")
          .Set("int-policy", -123)
          .Set("double-policy", 456e7)
          .SetBoolean("boolean-policy", true)
          .Set("list-policy", extensions::ListBuilder()
                                  .Append("one")
                                  .Append("two")
                                  .Append("three")
                                  .Build())
          .Set("dict-policy",
               extensions::DictionaryBuilder()
                   .Set("list", extensions::ListBuilder()
                                    .Append(extensions::DictionaryBuilder()
                                                .Set("one", 1)
                                                .Set("two", 2)
                                                .Build())
                                    .Append(extensions::DictionaryBuilder()
                                                .Set("three", 3)
                                                .Build())
                                    .Build())
                   .Build())
          .Build();
  SetPolicies(*policy);
  // Now run the extension.
  ASSERT_TRUE(RunExtensionTest("settings/managed_storage")) << message_;
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest,
                       DISABLED_PRE_ManagedStorageEvents) {
  ResultCatcher catcher;

  // This test starts without any test extensions installed.
  EXPECT_FALSE(GetSingleLoadedExtension());
  message_.clear();

  // Set policies for the test extension.
  scoped_ptr<base::DictionaryValue> policy = extensions::DictionaryBuilder()
      .Set("constant-policy", "aaa")
      .Set("changes-policy", "bbb")
      .Set("deleted-policy", "ccc")
      .Build();
  SetPolicies(*policy);

  ExtensionTestMessageListener ready_listener("ready", false);
  // Load the extension to install the event listener.
  const Extension* extension = LoadExtension(
      test_data_dir_.AppendASCII("settings/managed_storage_events"));
  ASSERT_TRUE(extension);
  // Wait until the extension sends the "ready" message.
  ASSERT_TRUE(ready_listener.WaitUntilSatisfied());

  // Now change the policies and wait until the extension is done.
  policy = extensions::DictionaryBuilder()
      .Set("constant-policy", "aaa")
      .Set("changes-policy", "ddd")
      .Set("new-policy", "eee")
      .Build();
  SetPolicies(*policy);
  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest,
                       DISABLED_ManagedStorageEvents) {
  // This test runs after PRE_ManagedStorageEvents without having deleted the
  // profile, so the extension is still around. While the browser restarted the
  // policy went back to the empty default, and so the extension should receive
  // the corresponding change events.

  ResultCatcher catcher;

  // Verify that the test extension is still installed.
  const Extension* extension = GetSingleLoadedExtension();
  ASSERT_TRUE(extension);
  EXPECT_EQ(kManagedStorageExtensionId, extension->id());

  // Running the test again skips the onInstalled callback, and just triggers
  // the onChanged notification.
  EXPECT_TRUE(catcher.GetNextResult()) << catcher.message();
}

IN_PROC_BROWSER_TEST_F(ExtensionSettingsApiTest, ManagedStorageDisabled) {
  // Disable the 'managed' namespace.
  StorageFrontend* frontend = StorageFrontend::Get(browser()->profile());
  frontend->DisableStorageForTesting(MANAGED);
  EXPECT_FALSE(frontend->IsStorageEnabled(MANAGED));
  // Now run the extension.
  ASSERT_TRUE(RunExtensionTest("settings/managed_storage_disabled"))
      << message_;
}

}  // namespace extensions