// Copyright 2013 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/extensions/activity_log/counting_policy.h"

#include <stddef.h>
#include <stdint.h>

#include <memory>
#include <utility>

#include "base/cancelable_callback.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/waitable_event.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/simple_test_clock.h"
#include "base/test/test_timeouts.h"
#include "base/values.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/extensions/activity_log/activity_log.h"
#include "chrome/browser/extensions/activity_log/activity_log_task_runner.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_profile.h"
#include "content/public/test/browser_task_environment.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_builder.h"
#include "sql/statement.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/login/users/user_manager_delegate_impl.h"
#include "chrome/browser/ash/settings/scoped_cros_settings_test_helper.h"
#include "chrome/browser/browser_process.h"
#include "chromeos/ash/components/settings/cros_settings.h"
#include "components/user_manager/scoped_user_manager.h"
#include "components/user_manager/user_manager_impl.h"
#endif

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

class CountingPolicyTest : public testing::Test {
 public:
  CountingPolicyTest()
      : task_environment_(content::BrowserTaskEnvironment::IO_MAINLOOP) {
    profile_ = std::make_unique<TestingProfile>();
    base::CommandLine::ForCurrentProcess()->
        AppendSwitch(switches::kEnableExtensionActivityLogging);
    base::CommandLine no_program_command_line(base::CommandLine::NO_PROGRAM);
    static_cast<TestExtensionSystem*>(ExtensionSystem::Get(profile_.get()))
        ->CreateExtensionService(&no_program_command_line, base::FilePath(),
                                 false);
    base::RunLoop().RunUntilIdle();
  }

  ~CountingPolicyTest() override {
#if BUILDFLAG(IS_CHROMEOS)
    user_manager_.Reset();
#endif
    base::RunLoop().RunUntilIdle();
    profile_.reset();
    base::RunLoop().RunUntilIdle();
  }

  // Waits for the task queue for the activity log sequence to empty.
  void WaitOnActivityLogSequence() {
    base::RunLoop run_loop;
    GetActivityLogTaskRunner()->PostTaskAndReply(
        FROM_HERE, base::DoNothing(), run_loop.QuitWhenIdleClosure());
    run_loop.Run();
  }

  // A wrapper function for CheckReadFilteredData, so that we don't need to
  // enter empty string values for parameters we don't care about.
  void CheckReadData(
      ActivityLogDatabasePolicy* policy,
      const std::string& extension_id,
      int day,
      base::OnceCallback<void(std::unique_ptr<Action::ActionVector>)> checker) {
    CheckReadFilteredData(policy, extension_id, Action::ACTION_ANY, "", "", "",
                          day, std::move(checker));
  }

  // A helper function to call ReadFilteredData on a policy object and wait for
  // the results to be processed.
  void CheckReadFilteredData(
      ActivityLogDatabasePolicy* policy,
      const std::string& extension_id,
      const Action::ActionType type,
      const std::string& api_name,
      const std::string& page_url,
      const std::string& arg_url,
      int day,
      base::OnceCallback<void(std::unique_ptr<Action::ActionVector>)> checker) {
    // Submit a request to the policy to read back some data, and call the
    // checker function when results are available.  This will happen on the
    // database thread.
    base::RunLoop run_loop;
    policy->ReadFilteredData(
        extension_id, type, api_name, page_url, arg_url, day,
        base::BindOnce(&CountingPolicyTest::CheckWrapper, std::move(checker),
                       run_loop.QuitClosure()));

    // Set up a timeout for receiving results; if we haven't received anything
    // when the timeout triggers then assume that the test is broken.
    base::CancelableOnceClosure timeout(base::BindOnce(
        &CountingPolicyTest::TimeoutCallback, run_loop.QuitWhenIdleClosure()));
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, timeout.callback(), TestTimeouts::action_timeout());

    // Wait for results; either the checker or the timeout callbacks should
    // cause the main loop to exit.
    run_loop.Run();

    timeout.Cancel();
  }

  // A helper function which verifies that the string_ids and url_ids tables in
  // the database have the specified sizes.
  static void CheckStringTableSizes(CountingPolicy* policy,
                                    int string_size,
                                    int url_size) {
    sql::Database* db = policy->GetDatabaseConnection();
    sql::Statement statement1(db->GetCachedStatement(
        sql::StatementID(SQL_FROM_HERE), "SELECT COUNT(*) FROM string_ids"));
    ASSERT_TRUE(statement1.Step());
    ASSERT_EQ(string_size, statement1.ColumnInt(0));

    sql::Statement statement2(db->GetCachedStatement(
        sql::StatementID(SQL_FROM_HERE), "SELECT COUNT(*) FROM url_ids"));
    ASSERT_TRUE(statement2.Step());
    ASSERT_EQ(url_size, statement2.ColumnInt(0));
  }

  // Checks that the number of queued actions to be written out does not exceed
  // kSizeThresholdForFlush.  Runs on the database thread.
  static void CheckQueueSize(CountingPolicy* policy) {
    // This should be updated if kSizeThresholdForFlush in activity_database.cc
    // changes.
    ASSERT_LE(policy->queued_actions_.size(), 200U);
  }

  static void CheckWrapper(
      base::OnceCallback<void(std::unique_ptr<Action::ActionVector>)> checker,
      base::OnceClosure done,
      std::unique_ptr<Action::ActionVector> results) {
    std::move(checker).Run(std::move(results));
    std::move(done).Run();
  }

  static void TimeoutCallback(base::OnceClosure quit_closure) {
    std::move(quit_closure).Run();
    FAIL() << "Policy test timed out waiting for results";
  }

  static void RetrieveActions_FetchFilteredActions0(
      std::unique_ptr<std::vector<scoped_refptr<Action>>> i) {
    ASSERT_EQ(0u, i->size());
  }

  static void RetrieveActions_FetchFilteredActions1(
      std::unique_ptr<std::vector<scoped_refptr<Action>>> i) {
    ASSERT_EQ(1u, i->size());
  }

  static void RetrieveActions_FetchFilteredActions2(
      std::unique_ptr<std::vector<scoped_refptr<Action>>> i) {
    ASSERT_EQ(2u, i->size());
  }

  static void RetrieveActions_FetchFilteredActions300(
      std::unique_ptr<std::vector<scoped_refptr<Action>>> i) {
    ASSERT_EQ(300u, i->size());
  }

  static void Arguments_Stripped(std::unique_ptr<Action::ActionVector> i) {
    scoped_refptr<Action> last = i->front();
    CheckAction(*last, "odlameecjipmbmbejkplpemijjgpljce",
                Action::ACTION_API_CALL, "extension.connect",
                "[\"hello\",\"world\"]", "", "", "", 1);
  }

  static void Arguments_GetSinglesAction(
      std::unique_ptr<Action::ActionVector> actions) {
    ASSERT_EQ(1u, actions->size());
    CheckAction(*actions->at(0), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "http://www.google.com/", "", "", 1);
  }

  static void Arguments_GetTodaysActions(
      std::unique_ptr<Action::ActionVector> actions) {
    ASSERT_EQ(3u, actions->size());
    CheckAction(*actions->at(0), "punky", Action::ACTION_API_CALL, "brewster",
                "", "", "", "", 2);
    CheckAction(*actions->at(1), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "http://www.google.com/", "", "", 1);
    CheckAction(*actions->at(2), "punky", Action::ACTION_API_CALL,
                "extension.sendMessage", "[\"not\",\"stripped\"]", "", "", "",
                1);
  }

  static void Arguments_GetOlderActions(
      std::unique_ptr<Action::ActionVector> actions) {
    ASSERT_EQ(2u, actions->size());
    CheckAction(*actions->at(0), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "http://www.google.com/", "", "", 1);
    CheckAction(*actions->at(1), "punky", Action::ACTION_API_CALL, "brewster",
                "", "", "", "", 1);
  }

  static void Arguments_CheckMergeCount(
      int count,
      std::unique_ptr<Action::ActionVector> actions) {
    if (count > 0) {
      ASSERT_EQ(1u, actions->size());
      CheckAction(*actions->at(0), "punky", Action::ACTION_API_CALL, "brewster",
                  "", "", "", "", count);
    } else {
      ASSERT_EQ(0u, actions->size());
    }
  }

  static void Arguments_CheckMergeCountAndTime(
      int count,
      const base::Time& time,
      std::unique_ptr<Action::ActionVector> actions) {
    if (count > 0) {
      ASSERT_EQ(1u, actions->size());
      CheckAction(*actions->at(0), "punky", Action::ACTION_API_CALL, "brewster",
                  "", "", "", "", count);
      ASSERT_EQ(time, actions->at(0)->time());
    } else {
      ASSERT_EQ(0u, actions->size());
    }
  }

  static void AllURLsRemoved(std::unique_ptr<Action::ActionVector> actions) {
    ASSERT_EQ(2u, actions->size());
    CheckAction(*actions->at(0), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "", "", "", 1);
    CheckAction(*actions->at(1), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "", "", "", 1);
  }

  static void SomeURLsRemoved(std::unique_ptr<Action::ActionVector> actions) {
    // These will be in the vector in reverse time order.
    ASSERT_EQ(5u, actions->size());
    CheckAction(*actions->at(0), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "http://www.google.com/", "Google", "http://www.args-url.com/",
                1);
    CheckAction(*actions->at(1), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "http://www.google.com/", "Google", "", 1);
    CheckAction(*actions->at(2), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "", "", "", 1);
    CheckAction(*actions->at(3), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "", "", "http://www.google.com/", 1);
    CheckAction(*actions->at(4), "punky", Action::ACTION_DOM_ACCESS, "lets", "",
                "", "", "", 1);
  }

  static void CheckDuplicates(std::unique_ptr<Action::ActionVector> actions) {
    ASSERT_EQ(2u, actions->size());
    int total_count = 0;
    for (const auto& action : *actions) {
      total_count += action->count();
    }
    ASSERT_EQ(3, total_count);
  }

  static void CheckAction(const Action& action,
                          const std::string& expected_id,
                          const Action::ActionType& expected_type,
                          const std::string& expected_api_name,
                          const std::string& expected_args_str,
                          const std::string& expected_page_url,
                          const std::string& expected_page_title,
                          const std::string& expected_arg_url,
                          int expected_count) {
    ASSERT_EQ(expected_id, action.extension_id());
    ASSERT_EQ(expected_type, action.action_type());
    ASSERT_EQ(expected_api_name, action.api_name());
    ASSERT_EQ(expected_args_str,
              ActivityLogPolicy::Util::Serialize(action.args()));
    ASSERT_EQ(expected_page_url, action.SerializePageUrl());
    ASSERT_EQ(expected_page_title, action.page_title());
    ASSERT_EQ(expected_arg_url, action.SerializeArgUrl());
    ASSERT_EQ(expected_count, action.count());
    ASSERT_NE(-1, action.action_id());
  }

  // A helper function initializes the policy with a number of actions, calls
  // RemoveActions on a policy object and then checks the result of the
  // deletion.
  void CheckRemoveActions(
      ActivityLogDatabasePolicy* policy,
      const std::vector<int64_t>& action_ids,
      base::OnceCallback<void(std::unique_ptr<Action::ActionVector>)> checker) {
    // Use a mock clock to ensure that events are not recorded on the wrong day
    // when the test is run close to local midnight.
    mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
    policy->SetClockForTesting(&mock_clock_);

    // Record some actions
    scoped_refptr<Action> action =
        new Action("punky1", mock_clock_.Now() - base::Minutes(40),
                   Action::ACTION_DOM_ACCESS, "lets1");
    action->mutable_args().Append("vamoose1");
    action->set_page_url(GURL("http://www.google1.com"));
    action->set_page_title("Google1");
    action->set_arg_url(GURL("http://www.args-url1.com"));
    policy->ProcessAction(action);
    // Record the same action twice, so there are multiple entries in the
    // database.
    policy->ProcessAction(action);

    action = new Action("punky2", mock_clock_.Now() - base::Minutes(30),
                        Action::ACTION_API_CALL, "lets2");
    action->mutable_args().Append("vamoose2");
    action->set_page_url(GURL("http://www.google2.com"));
    action->set_page_title("Google2");
    action->set_arg_url(GURL("http://www.args-url2.com"));
    policy->ProcessAction(action);
    // Record the same action twice, so there are multiple entries in the
    // database.
    policy->ProcessAction(action);

    // Submit a request to delete actions.
    policy->RemoveActions(action_ids);

    // Check the result of the deletion. The checker function gets all
    // activities in the database.
    CheckReadData(policy, "", -1, std::move(checker));

    // Clean database.
    policy->DeleteDatabase();
  }

  static void AllActionsDeleted(std::unique_ptr<Action::ActionVector> actions) {
    ASSERT_EQ(0u, actions->size());
  }

  static void NoActionsDeleted(std::unique_ptr<Action::ActionVector> actions) {
    // These will be in the vector in reverse time order.
    ASSERT_EQ(2u, actions->size());
    CheckAction(*actions->at(0), "punky2", Action::ACTION_API_CALL, "lets2", "",
                "http://www.google2.com/", "Google2",
                "http://www.args-url2.com/", 2);
    ASSERT_EQ(2, actions->at(0)->action_id());
    CheckAction(*actions->at(1), "punky1", Action::ACTION_DOM_ACCESS, "lets1",
                "", "http://www.google1.com/", "Google1",
                "http://www.args-url1.com/", 2);
    ASSERT_EQ(1, actions->at(1)->action_id());
  }

  static void Action1Deleted(std::unique_ptr<Action::ActionVector> actions) {
    // These will be in the vector in reverse time order.
    ASSERT_EQ(1u, actions->size());
    CheckAction(*actions->at(0), "punky2", Action::ACTION_API_CALL, "lets2", "",
                "http://www.google2.com/", "Google2",
                "http://www.args-url2.com/", 2);
    ASSERT_EQ(2, actions->at(0)->action_id());
  }

  static void Action2Deleted(std::unique_ptr<Action::ActionVector> actions) {
    // These will be in the vector in reverse time order.
    ASSERT_EQ(1u, actions->size());
    CheckAction(*actions->at(0), "punky1", Action::ACTION_DOM_ACCESS, "lets1",
                "", "http://www.google1.com/", "Google1",
                "http://www.args-url1.com/", 2);
    ASSERT_EQ(1, actions->at(0)->action_id());
  }

 protected:
  base::SimpleTestClock mock_clock_;
  raw_ptr<ExtensionService, DanglingUntriaged> extension_service_;
  std::unique_ptr<TestingProfile> profile_;
  content::BrowserTaskEnvironment task_environment_;

#if BUILDFLAG(IS_CHROMEOS)
  ash::ScopedCrosSettingsTestHelper cros_settings_test_helper_;
  user_manager::ScopedUserManager user_manager_{
      std::make_unique<user_manager::UserManagerImpl>(
          std::make_unique<ash::UserManagerDelegateImpl>(),
          g_browser_process->local_state(),
          ash::CrosSettings::Get())};
#endif
};

TEST_F(CountingPolicyTest, Construct) {
  ActivityLogDatabasePolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  scoped_refptr<const Extension> extension =
      ExtensionBuilder()
          .SetManifest(base::DictValue()
                           .Set("name", "Test extension")
                           .Set("version", "1.0.0")
                           .Set("manifest_version", 2))
          .Build();
  ExtensionRegistrar::Get(profile_.get())->AddExtension(extension);
  scoped_refptr<Action> action = new Action(extension->id(),
                                            base::Time::Now(),
                                            Action::ACTION_API_CALL,
                                            "tabs.testMethod");
  action->set_args(base::ListValue());
  policy->ProcessAction(action);
  policy->Close();
}

TEST_F(CountingPolicyTest, LogWithStrippedArguments) {
  ActivityLogDatabasePolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  scoped_refptr<const Extension> extension =
      ExtensionBuilder()
          .SetManifest(base::DictValue()
                           .Set("name", "Test extension")
                           .Set("version", "1.0.0")
                           .Set("manifest_version", 2))
          .Build();
  ExtensionRegistrar::Get(profile_.get())->AddExtension(extension);

  auto args = base::ListValue().Append("hello");
  args.Append("world");
  scoped_refptr<Action> action = new Action(extension->id(),
                                            base::Time::Now(),
                                            Action::ACTION_API_CALL,
                                            "extension.connect");
  action->set_args(std::move(args));

  policy->ProcessAction(action);
  CheckReadData(policy, extension->id(), 0,
                base::BindOnce(&CountingPolicyTest::Arguments_Stripped));
  policy->Close();
}

TEST_F(CountingPolicyTest, GetTodaysActions) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  // Disable row expiration for this test by setting a time before any actions
  // we generate.
  policy->set_retention_time(base::Days(14));

  // Use a mock clock to ensure that events are not recorded on the wrong day
  // when the test is run close to local midnight.  Note: Ownership is passed
  // to the policy, but we still keep a pointer locally.  The policy will take
  // care of destruction; this is safe since the policy outlives all our
  // accesses to the mock clock.
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // Record some actions
  scoped_refptr<Action> action =
      new Action("punky", mock_clock_.Now() - base::Minutes(40),
                 Action::ACTION_API_CALL, "brewster");
  action->mutable_args().Append("woof");
  policy->ProcessAction(action);

  action = new Action("punky", mock_clock_.Now() - base::Minutes(30),
                      Action::ACTION_API_CALL, "brewster");
  action->mutable_args().Append("meow");
  policy->ProcessAction(action);

  action = new Action("punky", mock_clock_.Now() - base::Minutes(20),
                      Action::ACTION_API_CALL, "extension.sendMessage");
  action->mutable_args().Append("not");
  action->mutable_args().Append("stripped");
  policy->ProcessAction(action);

  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  action = new Action("scoobydoo", mock_clock_.Now(), Action::ACTION_DOM_ACCESS,
                      "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  CheckReadData(
      policy, "punky", 0,
      base::BindOnce(&CountingPolicyTest::Arguments_GetTodaysActions));
  policy->Close();
}

// Check that we can read back less recent actions in the db.
TEST_F(CountingPolicyTest, GetOlderActions) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  policy->set_retention_time(base::Days(14));

  // Use a mock clock to ensure that events are not recorded on the wrong day
  // when the test is run close to local midnight.
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // Record some actions
  scoped_refptr<Action> action =
      new Action("punky", mock_clock_.Now() - base::Days(3) - base::Minutes(40),
                 Action::ACTION_API_CALL, "brewster");
  action->mutable_args().Append("woof");
  policy->ProcessAction(action);

  action = new Action("punky", mock_clock_.Now() - base::Days(3),
                      Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("too new");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  action = new Action("punky", mock_clock_.Now() - base::Days(7),
                      Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("too old");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  CheckReadData(policy, "punky", 3,
                base::BindOnce(&CountingPolicyTest::Arguments_GetOlderActions));

  policy->Close();
}

TEST_F(CountingPolicyTest, LogAndFetchFilteredActions) {
  ActivityLogDatabasePolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  scoped_refptr<const Extension> extension =
      ExtensionBuilder()
          .SetManifest(base::DictValue()
                           .Set("name", "Test extension")
                           .Set("version", "1.0.0")
                           .Set("manifest_version", 2))
          .Build();
  ExtensionRegistrar::Get(profile_.get())->AddExtension(extension);
  GURL gurl("http://www.google.com");

  // Write some API calls
  scoped_refptr<Action> action_api = new Action(extension->id(),
                                                base::Time::Now(),
                                                Action::ACTION_API_CALL,
                                                "tabs.testMethod");
  action_api->set_args(base::ListValue());
  policy->ProcessAction(action_api);

  scoped_refptr<Action> action_dom = new Action(extension->id(),
                                                base::Time::Now(),
                                                Action::ACTION_DOM_ACCESS,
                                                "document.write");
  action_dom->set_args(base::ListValue());
  action_dom->set_page_url(gurl);
  policy->ProcessAction(action_dom);

  CheckReadFilteredData(
      policy, extension->id(), Action::ACTION_API_CALL, "tabs.testMethod", "",
      "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions1));

  // Test for case insensitive matching for api_call.
  CheckReadFilteredData(
      policy, extension->id(), Action::ACTION_API_CALL, "tabs.testmethod", "",
      "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions1));

  // Test for prefix matching for api_call.
  CheckReadFilteredData(
      policy, extension->id(), Action::ACTION_API_CALL, "tabs.testM", "", "",
      -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions1));

  CheckReadFilteredData(
      policy, "", Action::ACTION_DOM_ACCESS, "", "", "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions1));

  CheckReadFilteredData(
      policy, "", Action::ACTION_DOM_ACCESS, "", "http://www.google.com/", "",
      -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions1));

  CheckReadFilteredData(
      policy, "", Action::ACTION_DOM_ACCESS, "", "http://www.google.com", "",
      -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions1));

  CheckReadFilteredData(
      policy, "", Action::ACTION_DOM_ACCESS, "", "http://www.goo", "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions1));

  CheckReadFilteredData(
      policy, extension->id(), Action::ACTION_ANY, "", "", "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions2));

  policy->Close();
}

// Check that merging of actions only occurs within the same day, not across
// days, and that old data can be expired from the database.
TEST_F(CountingPolicyTest, MergingAndExpiring) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  // Initially disable expiration by setting a retention time before any
  // actions we generate.
  policy->set_retention_time(base::Days(14));

  // Use a mock clock to ensure that events are not recorded on the wrong day
  // when the test is run close to local midnight.
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // The first two actions should be merged; the last one is on a separate day
  // and should not be.
  scoped_refptr<Action> action =
      new Action("punky", mock_clock_.Now() - base::Days(3) - base::Minutes(40),
                 Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  action =
      new Action("punky", mock_clock_.Now() - base::Days(3) - base::Minutes(20),
                 Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  action =
      new Action("punky", mock_clock_.Now() - base::Days(2) - base::Minutes(20),
                 Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  CheckReadData(
      policy, "punky", 3,
      base::BindOnce(&CountingPolicyTest::Arguments_CheckMergeCount, 2));
  CheckReadData(
      policy, "punky", 2,
      base::BindOnce(&CountingPolicyTest::Arguments_CheckMergeCount, 1));

  // Clean actions before midnight two days ago.  Force expiration to run by
  // clearing last_database_cleaning_time_ and submitting a new action.
  policy->set_retention_time(base::Days(2));
  policy->last_database_cleaning_time_ = base::Time();
  action = new Action("punky", mock_clock_.Now(), Action::ACTION_API_CALL,
                      "brewster");
  policy->ProcessAction(action);

  CheckReadData(
      policy, "punky", 3,
      base::BindOnce(&CountingPolicyTest::Arguments_CheckMergeCount, 0));
  CheckReadData(
      policy, "punky", 2,
      base::BindOnce(&CountingPolicyTest::Arguments_CheckMergeCount, 1));

  policy->Close();
}

// Test cleaning of old data in the string and URL tables.
TEST_F(CountingPolicyTest, StringTableCleaning) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  // Initially disable expiration by setting a retention time before any
  // actions we generate.
  policy->set_retention_time(base::Days(14));

  mock_clock_.SetNow(base::Time::Now());
  policy->SetClockForTesting(&mock_clock_);

  // Insert an action; this should create entries in both the string table (for
  // the extension and API name) and the URL table (for page_url).
  scoped_refptr<Action> action =
      new Action("punky", mock_clock_.Now() - base::Days(7),
                 Action::ACTION_API_CALL, "brewster");
  action->set_page_url(GURL("http://www.google.com/"));
  policy->ProcessAction(action);

  // Add an action which will not be expired, so that some strings will remain
  // in use.
  action = new Action("punky", mock_clock_.Now(), Action::ACTION_API_CALL,
                      "tabs.create");
  policy->ProcessAction(action);

  // There should now be three strings ("punky", "brewster", "tabs.create") and
  // one URL in the tables.
  policy->Flush();
  policy->ScheduleAndForget(policy,
                            &CountingPolicyTest::CheckStringTableSizes,
                            3,
                            1);
  WaitOnActivityLogSequence();

  // Trigger a cleaning.  The oldest action is expired when we submit a
  // duplicate of the newer action.  After this, there should be two strings
  // and no URLs.
  policy->set_retention_time(base::Days(2));
  policy->last_database_cleaning_time_ = base::Time();
  policy->ProcessAction(action);
  policy->Flush();
  policy->ScheduleAndForget(policy,
                            &CountingPolicyTest::CheckStringTableSizes,
                            2,
                            0);
  WaitOnActivityLogSequence();

  policy->Close();
}

// A stress test for memory- and database-based merging of actions.  Submit
// multiple items, not in chronological order, spanning a few days.  Check that
// items are merged properly and final timestamps are correct.
TEST_F(CountingPolicyTest, MoreMerging) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  policy->set_retention_time(base::Days(14));

  // Use a mock clock to ensure that events are not recorded on the wrong day
  // when the test is run close to local midnight.
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // Create an action 2 days ago, then 1 day ago, then 2 days ago.  Make sure
  // that we end up with two merged records (one for each day), and each has
  // the appropriate timestamp.  These merges should happen in the database
  // since the date keeps changing.
  base::Time time1 = mock_clock_.Now() - base::Days(2) - base::Minutes(40);
  base::Time time2 = mock_clock_.Now() - base::Days(1) - base::Minutes(40);
  base::Time time3 = mock_clock_.Now() - base::Days(2) - base::Minutes(20);

  scoped_refptr<Action> action =
      new Action("punky", time1, Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  action = new Action("punky", time2, Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  action = new Action("punky", time3, Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  CheckReadData(
      policy, "punky", 2,
      base::BindOnce(&CountingPolicyTest::Arguments_CheckMergeCountAndTime, 2,
                     time3));
  CheckReadData(
      policy, "punky", 1,
      base::BindOnce(&CountingPolicyTest::Arguments_CheckMergeCountAndTime, 1,
                     time2));

  // Create three actions today, where the merges should happen in memory.
  // Again these are not chronological; timestamp time5 should win out since it
  // is the latest.
  base::Time time4 = mock_clock_.Now() - base::Minutes(60);
  base::Time time5 = mock_clock_.Now() - base::Minutes(20);
  base::Time time6 = mock_clock_.Now() - base::Minutes(40);

  action = new Action("punky", time4, Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  action = new Action("punky", time5, Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  action = new Action("punky", time6, Action::ACTION_API_CALL, "brewster");
  policy->ProcessAction(action);

  CheckReadData(
      policy, "punky", 0,
      base::BindOnce(&CountingPolicyTest::Arguments_CheckMergeCountAndTime, 3,
                     time5));
  policy->Close();
}

// Check that actions are flushed to disk before letting too many accumulate in
// memory.
TEST_F(CountingPolicyTest, EarlyFlush) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();

  for (int i = 0; i < 500; i++) {
    scoped_refptr<Action> action =
        new Action("punky",
                   base::Time::Now(),
                   Action::ACTION_API_CALL,
                   base::StringPrintf("apicall_%d", i));
    policy->ProcessAction(action);
  }

  policy->ScheduleAndForget(policy, &CountingPolicyTest::CheckQueueSize);
  WaitOnActivityLogSequence();

  policy->Close();
}

TEST_F(CountingPolicyTest, CapReturns) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();

  for (int i = 0; i < 305; i++) {
    scoped_refptr<Action> action =
        new Action("punky",
                   base::Time::Now(),
                   Action::ACTION_API_CALL,
                   base::StringPrintf("apicall_%d", i));
    policy->ProcessAction(action);
  }

  policy->Flush();
  WaitOnActivityLogSequence();

  CheckReadFilteredData(
      policy, "punky", Action::ACTION_ANY, "", "", "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions300));
  policy->Close();
}

TEST_F(CountingPolicyTest, RemoveAllURLs) {
  ActivityLogDatabasePolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();

  // Use a mock clock to ensure that events are not recorded on the wrong day
  // when the test is run close to local midnight.
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // Record some actions
  scoped_refptr<Action> action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  action->set_page_title("Google");
  action->set_arg_url(GURL("http://www.args-url.com"));
  policy->ProcessAction(action);

  mock_clock_.Advance(base::Seconds(1));
  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google2.com"));
  action->set_page_title("Google");
  // Deliberately no arg url set to make sure it stills works if there is no arg
  // url.
  policy->ProcessAction(action);

  // Clean all the URLs.
  std::vector<GURL> no_url_restrictions;
  policy->RemoveURLs(no_url_restrictions);

  CheckReadData(policy, "punky", 0,
                base::BindOnce(&CountingPolicyTest::AllURLsRemoved));
  policy->Close();
}

TEST_F(CountingPolicyTest, RemoveSpecificURLs) {
  ActivityLogDatabasePolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();

  // Use a mock clock to ensure that events are not recorded on the wrong day
  // when the test is run close to local midnight.
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // Record some actions
  // This should have the page url and args url cleared.
  scoped_refptr<Action> action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google1.com"));
  action->set_page_title("Google");
  action->set_arg_url(GURL("http://www.google1.com"));
  policy->ProcessAction(action);

  // This should have the page url cleared but not args url.
  mock_clock_.Advance(base::Seconds(1));
  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google1.com"));
  action->set_page_title("Google");
  action->set_arg_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  // This should have the page url cleared. The args url is deliberately not
  // set to make sure this doesn't cause any issues.
  mock_clock_.Advance(base::Seconds(1));
  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google2.com"));
  action->set_page_title("Google");
  policy->ProcessAction(action);

  // This should have the args url cleared but not the page url or page title.
  mock_clock_.Advance(base::Seconds(1));
  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  action->set_page_title("Google");
  action->set_arg_url(GURL("http://www.google1.com"));
  policy->ProcessAction(action);

  // This should have neither cleared.
  mock_clock_.Advance(base::Seconds(1));
  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  action->set_page_title("Google");
  action->set_arg_url(GURL("http://www.args-url.com"));
  action->set_count(5);
  policy->ProcessAction(action);

    // Clean some URLs.
  std::vector<GURL> urls;
  urls.push_back(GURL("http://www.google1.com"));
  urls.push_back(GURL("http://www.google2.com"));
  urls.push_back(GURL("http://www.url_not_in_db.com"));
  policy->RemoveURLs(urls);

  CheckReadData(policy, "punky", 0,
                base::BindOnce(&CountingPolicyTest::SomeURLsRemoved));
  policy->Close();
}

TEST_F(CountingPolicyTest, RemoveExtensionData) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();

  // Use a mock clock to ensure that events are not recorded on the wrong day
  // when the test is run close to local midnight.
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // Record some actions
  scoped_refptr<Action> action =
      new Action("deleteextensiondata", mock_clock_.Now(),
                 Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_title("Google");
  action->set_arg_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);
  policy->ProcessAction(action);
  policy->ProcessAction(action);

  scoped_refptr<Action> action2 = new Action("dontdelete", mock_clock_.Now(),
                                             Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_title("Google");
  action->set_arg_url(GURL("http://www.google.com"));
  policy->ProcessAction(action2);

  policy->Flush();
  policy->RemoveExtensionData("deleteextensiondata");

  CheckReadFilteredData(
      policy, "deleteextensiondata", Action::ACTION_ANY, "", "", "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions0));

  CheckReadFilteredData(
      policy, "dontdelete", Action::ACTION_ANY, "", "", "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions1));
  policy->Close();
}

TEST_F(CountingPolicyTest, DeleteDatabase) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  // Disable row expiration for this test by setting a time before any actions
  // we generate.
  policy->set_retention_time(base::Days(14));

  // Use a mock clock to ensure that events are not recorded on the wrong day
  // when the test is run close to local midnight.  Note: Ownership is passed
  // to the policy, but we still keep a pointer locally.  The policy will take
  // care of destruction; this is safe since the policy outlives all our
  // accesses to the mock clock.
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // Record some actions
  scoped_refptr<Action> action =
      new Action("punky", mock_clock_.Now() - base::Minutes(40),
                 Action::ACTION_API_CALL, "brewster");
  action->mutable_args().Append("woof");
  policy->ProcessAction(action);

  action = new Action("punky", mock_clock_.Now() - base::Minutes(30),
                      Action::ACTION_API_CALL, "brewster");
  action->mutable_args().Append("meow");
  policy->ProcessAction(action);

  action = new Action("punky", mock_clock_.Now() - base::Minutes(20),
                      Action::ACTION_API_CALL, "extension.sendMessage");
  action->mutable_args().Append("not");
  action->mutable_args().Append("stripped");
  policy->ProcessAction(action);

  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  action = new Action("scoobydoo", mock_clock_.Now(), Action::ACTION_DOM_ACCESS,
                      "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  CheckReadData(
      policy, "punky", 0,
      base::BindOnce(&CountingPolicyTest::Arguments_GetTodaysActions));

  policy->DeleteDatabase();

  CheckReadFilteredData(
      policy, "", Action::ACTION_ANY, "", "", "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions0));

  // The following code tests that the caches of url and string tables were
  // cleared by the deletion above.
  // https://code.google.com/p/chromium/issues/detail?id=341674.
  action =
      new Action("punky", mock_clock_.Now(), Action::ACTION_DOM_ACCESS, "lets");
  action->mutable_args().Append("vamoose");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  CheckReadData(
      policy, "", -1,
      base::BindOnce(&CountingPolicyTest::Arguments_GetSinglesAction));

  policy->DeleteDatabase();

  CheckReadFilteredData(
      policy, "", Action::ACTION_ANY, "", "", "", -1,
      base::BindOnce(
          &CountingPolicyTest::RetrieveActions_FetchFilteredActions0));

  policy->Close();
}

// Tests that duplicate rows in the activity log database are handled properly
// when updating counts.
TEST_F(CountingPolicyTest, DuplicateRows) {
  CountingPolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();
  mock_clock_.SetNow(base::Time::Now().LocalMidnight() + base::Hours(12));
  policy->SetClockForTesting(&mock_clock_);

  // Record two actions with distinct URLs.
  scoped_refptr<Action> action = base::MakeRefCounted<Action>(
      "punky", mock_clock_.Now(), Action::ACTION_API_CALL, "brewster");
  action->set_page_url(GURL("http://www.google.com"));
  policy->ProcessAction(action);

  action = new Action("punky", mock_clock_.Now(), Action::ACTION_API_CALL,
                      "brewster");
  action->set_page_url(GURL("http://www.google.co.uk"));
  policy->ProcessAction(action);

  // Manipulate the database to clear the URLs, so that we end up with
  // duplicate rows.
  std::vector<GURL> no_url_restrictions;
  policy->RemoveURLs(no_url_restrictions);

  // Record one more action, with no URL.  This should increment the count on
  // one, and exactly one, of the existing rows.
  action = new Action("punky", mock_clock_.Now(), Action::ACTION_API_CALL,
                      "brewster");
  policy->ProcessAction(action);

  CheckReadData(policy, "punky", 0,
                base::BindOnce(&CountingPolicyTest::CheckDuplicates));
  policy->Close();
}

TEST_F(CountingPolicyTest, RemoveActions) {
  ActivityLogDatabasePolicy* policy = new CountingPolicy(profile_.get());
  policy->Init();

  std::vector<int64_t> action_ids;

  CheckRemoveActions(policy, action_ids,
                     base::BindOnce(&CountingPolicyTest::NoActionsDeleted));

  action_ids.push_back(-1);
  action_ids.push_back(-10);
  action_ids.push_back(0);
  action_ids.push_back(5);
  action_ids.push_back(10);
  CheckRemoveActions(policy, action_ids,
                     base::BindOnce(&CountingPolicyTest::NoActionsDeleted));
  action_ids.clear();

  for (int i = 0; i < 50; i++) {
    action_ids.push_back(i + 3);
  }
  CheckRemoveActions(policy, action_ids,
                     base::BindOnce(&CountingPolicyTest::NoActionsDeleted));
  action_ids.clear();

  // CheckRemoveActions pushes two actions to the Activity Log database with IDs
  // 1 and 2.
  action_ids.push_back(1);
  action_ids.push_back(2);
  CheckRemoveActions(policy, action_ids,
                     base::BindOnce(&CountingPolicyTest::AllActionsDeleted));
  action_ids.clear();

  action_ids.push_back(1);
  CheckRemoveActions(policy, action_ids,
                     base::BindOnce(&CountingPolicyTest::Action1Deleted));
  action_ids.clear();

  action_ids.push_back(2);
  CheckRemoveActions(policy, action_ids,
                     base::BindOnce(&CountingPolicyTest::Action2Deleted));
  action_ids.clear();

  policy->Close();
}

}  // namespace extensions
