// Copyright 2021 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/share/share_ranking.h"

#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/test/bind.h"
#include "chrome/browser/share/fake_share_history.h"
#include "chrome/test/base/testing_profile.h"
#include "components/leveldb_proto/testing/fake_db.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace sharing {

// The static tests are true unit tests that cover the static
// ShareRanking::ComputeHistory method only; to enforce that, they don't set up
// a test fixture, which will cause calls to other ShareRanking methods to
// crash.
using ShareRankingStaticTest = testing::Test;

class ShareRankingTest : public testing::Test {
 public:
  using FakeDB = leveldb_proto::test::FakeDB<proto::ShareRanking>;

  ShareRankingTest() { Init(); }

  void Init(bool do_default_init = true) {
    auto backing_db = std::make_unique<FakeDB>(&backing_entries_);
    backing_db_ = backing_db.get();

    db_ = std::make_unique<ShareRanking>(
        &profile_, base::WrapUnique(backing_db.release()));

    if (do_default_init) {
      backing_db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK);
    }
  }

  void ConfigureDefaultInitialRanking() {
    db()->set_initial_ranking_for_test(ShareRanking::Ranking{
        "aaa",
        "bbb",
        "ccc",
        "ddd",
        "eee",
        "fff",
        "ggg",
        "hhh",
        "iii",
        "jjj",
    });
  }

  std::optional<ShareRanking::Ranking> RankSync(
      ShareHistory* history,
      const std::vector<std::string>& available,
      const std::string& type = "type",
      int fold = 4,
      int length = 4,
      bool persist = true) {
    base::RunLoop loop;
    std::optional<ShareRanking::Ranking> ranking;
    auto callback =
        base::BindLambdaForTesting([&](std::optional<ShareRanking::Ranking> r) {
          ranking = r;
          loop.Quit();
        });

    backing_db()->QueueGetResult(true);
    db()->Rank(history, type, available, fold, length, persist,
               std::move(callback));
    loop.Run();

    return ranking;
  }

  ShareRanking* db() { return db_.get(); }
  leveldb_proto::test::FakeDB<proto::ShareRanking>* backing_db() {
    return backing_db_;
  }
  leveldb_proto::test::FakeDB<proto::ShareRanking>::EntryMap*
  backing_entries() {
    return &backing_entries_;
  }

 private:
  content::BrowserTaskEnvironment environment_{
      // This set of tests must use a mock time source. If they don't, and a
      // test happens to run across UTC midnight, the day can change mid-test,
      // causing surprising results.
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
  TestingProfile profile_;
  std::unique_ptr<ShareRanking> db_;
  leveldb_proto::test::FakeDB<proto::ShareRanking>::EntryMap backing_entries_;
  raw_ptr<leveldb_proto::test::FakeDB<proto::ShareRanking>> backing_db_ =
      nullptr;
};

// The "easy case": the existing usage counts are the same as the current
// ranking, and every app in the ranking is available, so the new ranking and
// the displayed ranking should both be the same as the current ranking, modulo
// the addition of the More target.
TEST(ShareRankingStaticTest, CountsMatchOldRanking) {
  std::map<std::string, int> history = {
      {"foo", 3},
      {"bar", 2},
      {"baz", 1},
  };

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"foo", "bar", "baz"};

  ShareRanking::ComputeRanking(history, history, current, {"foo", "bar", "baz"},
                               4, 4, &displayed, &persisted);

  ShareRanking::Ranking expected_displayed{"foo", "bar", "baz", "$more"};
  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, current);
}

// If the existing ranking includes an above-the-fold app that doesn't exist on
// the system, that app should be replaced by the next available below-the-fold
// app that does.
TEST(ShareRankingStaticTest, UnavailableAppDoesNotShow) {
  std::map<std::string, int> history = {
      {"foo", 5}, {"bar", 4}, {"baz", 3}, {"quxx", 2}, {"blit", 1},
  };

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"foo", "bar", "baz", "quxx", "blit"};

  ShareRanking::ComputeRanking(history, {}, current,
                               {"foo", "quxx", "baz", "blit"}, 4, 4, &displayed,
                               &persisted);
  ShareRanking::Ranking expected_displayed{
      "foo",
      "quxx",
      "baz",
      "$more",
  };
  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, current);
}

TEST(ShareRankingStaticTest, HighAllUsageAppReplacesLowest) {
  std::map<std::string, int> history = {
      {"foo", 5}, {"bar", 4}, {"baz", 3}, {"quxx", 2}, {"blit", 10}};

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"foo", "bar", "baz", "quxx", "blit"};
  ShareRanking::ComputeRanking(history, {}, current,
                               {"foo", "bar", "quxx", "baz", "blit"}, 5, 5,
                               &displayed, &persisted);
  ShareRanking::Ranking expected_displayed{
      "foo", "bar", "baz", "blit", "$more",
  };
  ShareRanking::Ranking expected_persisted{"foo", "bar", "baz", "blit", "quxx"};
  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, expected_persisted);
}

TEST(ShareRankingStaticTest, HighRecentUsageAppReplacesLowest) {
  std::map<std::string, int> history = {
      {"foo", 5}, {"bar", 4}, {"baz", 3}, {"quxx", 2}, {"blit", 6}};

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"foo", "bar", "baz", "quxx", "blit"};
  ShareRanking::ComputeRanking({}, history, current,
                               {"foo", "bar", "quxx", "baz", "blit"}, 5, 5,
                               &displayed, &persisted);
  ShareRanking::Ranking expected_displayed{"foo", "bar", "baz", "blit",
                                           "$more"};
  ShareRanking::Ranking expected_persisted{"foo", "bar", "baz", "blit", "quxx"};
  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, expected_persisted);
}

TEST(ShareRankingStaticTest, MoreTargetReplacesLast) {
  std::map<std::string, int> history = {
      {"bar", 2},
      {"foo", 3},
      {"baz", 1},
  };

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"foo", "bar", "baz"};

  ShareRanking::ComputeRanking(history, history, current, {"foo", "bar", "baz"},
                               3, 3, &displayed, &persisted);

  std::vector<std::string> expected_displayed{"foo", "bar",
                                              ShareRanking::kMoreTarget};

  ASSERT_EQ(displayed.size(), expected_displayed.size());
  ASSERT_EQ(persisted.size(), current.size());
  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, current);
}

TEST_F(ShareRankingStaticTest, PromotedTargetIsVisible) {
  // Regression test for <https://crbug.com/40785358>: in "fix more" mode, the
  // last slot on the screen is actually unusable for display since it gets
  // replaced with the "More" option that invokes the system share hub. The
  // logic to promote a target into being visible should therefore not promote
  // targets into this slot in the ranking.
  std::map<std::string, int> history = {
      {"bar", 2},
      {"foo", 3},
      {"baz", 10},
  };

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"foo", "bar", "baz"};

  ShareRanking::ComputeRanking(history, history, current, {"foo", "bar", "baz"},
                               3, 3, &displayed, &persisted);

  std::vector<std::string> expected_displayed{"foo", "baz",
                                              ShareRanking::kMoreTarget};
  std::vector<std::string> expected_persisted{"foo", "baz", "bar"};

  ASSERT_EQ(displayed.size(), expected_displayed.size());
  ASSERT_EQ(persisted.size(), current.size());
  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, expected_persisted);
}

TEST_F(ShareRankingStaticTest, UsedAppNotPresentInRanking) {
  std::map<std::string, int> history = {
      {"bar", 2},
      {"foo", 3},
      {"baz", 10},
  };

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"foo", "bar", "quxx"};

  ShareRanking::ComputeRanking(history, history, current, {"foo", "bar", "baz"},
                               3, 3, &displayed, &persisted);

  // Since length is 3 only 2 actual items show, with the visible one with the
  // lowest usage (bar) replaced with baz. Since baz was new (not in the old
  // ranking) it should have been stuck at the end of the ranking, then swapped
  // with bar, leaving bar at the end.
  std::vector<std::string> expected_displayed{"foo", "baz",
                                              ShareRanking::kMoreTarget};
  std::vector<std::string> expected_persisted{"foo", "baz", "quxx", "bar"};

  ASSERT_EQ(displayed.size(), expected_displayed.size());
  ASSERT_EQ(persisted.size(), expected_persisted.size());
  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, expected_persisted);
}

TEST_F(ShareRankingStaticTest, LowestUsageItemSwappedIgnoringDisplayOrder) {
  std::map<std::string, int> history = {
      {"bar", 2},
      {"foo", 3},
      {"baz", 4},
      {"quxx", 5},
  };

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"bar", "foo", "baz"};

  ShareRanking::ComputeRanking(history, history, current,
                               {"foo", "bar", "baz", "quxx"}, 3, 3, &displayed,
                               &persisted);

  ShareRanking::Ranking expected_displayed{"quxx", "foo", "$more"};
  ShareRanking::Ranking expected_persisted{"quxx", "foo", "baz", "bar"};
  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, expected_persisted);
}

TEST_F(ShareRankingStaticTest, SystemAppsReplaceUnavailableInRankedOrder) {
  // This test ensures that unavailable app "slots" in the old ranking are
  // replaced by available apps in the order of the old ranking, *not* in the
  // order supplied by the system.
  const ShareRanking::Ranking current{
      "aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj",
  };
  const std::vector<std::string> available{
      "iii", "ggg", "eee", "ccc", "aaa",
  };

  ShareRanking::Ranking displayed, persisted;

  ShareRanking::ComputeRanking({}, {}, current, available, 4, 4, &displayed,
                               &persisted);

  ShareRanking::Ranking expected_displayed{"aaa", "eee", "ccc", "$more"};

  EXPECT_EQ(displayed, expected_displayed);
}

TEST_F(ShareRankingStaticTest, NotEnoughPreferredApps) {
  const ShareRanking::Ranking current{
      "aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj",
  };
  const std::vector<std::string> available{
      "zzz", "yyy", "xxx", "ccc", "aaa",
  };

  ShareRanking::Ranking displayed, persisted;

  ShareRanking::ComputeRanking({}, {}, current, available, 4, 4, &displayed,
                               &persisted);

  ShareRanking::Ranking expected_displayed{"aaa", "zzz", "ccc", "$more"};

  EXPECT_EQ(displayed, expected_displayed);
}

TEST_F(ShareRankingStaticTest, SwapAboveFold) {
  const ShareRanking::Ranking current{
      "aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg",
  };
  const std::vector<std::string> available{
      "aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii", "jjj",
  };

  std::map<std::string, int> history = {
      {"bbb", 1},
      {"ccc", 1},
      {"ddd", 1},
      {"eee", 1},
  };

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::ComputeRanking(history, history, current, available, 4, 7,
                               &displayed, &persisted);

  ShareRanking::Ranking expected_displayed{
      "eee", "bbb", "ccc", "ddd", "aaa", "fff", "$more",
  };

  EXPECT_EQ(expected_displayed, displayed);
}

// Regression test for https://crbug.com/40780780
TEST_F(ShareRankingTest, OldRankingContainsItemsWithNoRecentHistory) {
  std::map<std::string, int> history = {
      {"bar", 2},
      {"foo", 3},
      {"abc", 4},
  };

  ShareRanking::Ranking displayed, persisted;
  ShareRanking::Ranking current{"foo", "bar", "baz", "abc"};

  ShareRanking::ComputeRanking(history, history, current, {"foo", "bar", "baz"},
                               4, 4, &displayed, &persisted);

  // Note that since "abc" isn't available on the system, it won't appear in the
  // displayed ranking, but the persisted ranking should still get updated.
  ShareRanking::Ranking expected_displayed{"foo", "bar", "baz", "$more"};
  ShareRanking::Ranking expected_persisted{"foo", "bar", "abc", "baz"};

  EXPECT_EQ(displayed, expected_displayed);
  EXPECT_EQ(persisted, expected_persisted);
}

TEST_F(ShareRankingTest, InitialStateNoHistory) {
  ConfigureDefaultInitialRanking();

  FakeShareHistory history;
  history.set_history({});

  auto ranking = RankSync(&history, {"aaa", "ccc", "eee", "ggg", "iii"});

  ASSERT_TRUE(ranking);
  // The displayed ranking should only contain apps available on the system.
  EXPECT_EQ(*ranking, std::vector<std::string>({"aaa", "eee", "ccc", "$more"}));

  backing_db()->UpdateCallback(true);
  auto entries = backing_entries()->at("type");
  // The stored ranking should be identical to the default ranking we injected
  // above, since there's no history to influence it.
  EXPECT_EQ(entries.targets().at(0), "aaa");
  EXPECT_EQ(entries.targets().at(1), "bbb");
  EXPECT_EQ(entries.targets().at(2), "ccc");
}

TEST_F(ShareRankingTest, DISABLED_AllHistoryUpdatesRanking) {
  // TODO(crbug.com/40191160): Implement.
}

TEST_F(ShareRankingTest, DISABLED_NoPersistDoesNotPersist) {
  // TODO(crbug.com/40191160): Implement.
}

TEST_F(ShareRankingTest, DISABLED_RecentHistoryUpdatesRanking) {
  // TODO(crbug.com/40191160): Implement.
}

TEST_F(ShareRankingTest, ClearClearsDatabase) {
  ConfigureDefaultInitialRanking();

  FakeShareHistory history;
  history.set_history({{"iii", 10}, {"aaa", 2}, {"ccc", 1}});

  auto pre_ranking = RankSync(&history, {"aaa", "ccc", "eee", "ggg", "iii"});

  ASSERT_TRUE(pre_ranking);
  EXPECT_EQ(*pre_ranking,
            std::vector<std::string>({"aaa", "iii", "ccc", "$more"}));

  backing_db()->UpdateCallback(true);

  auto pre_entries = backing_entries()->at("type");
  // The stored ranking should be identical to the default ranking we injected
  // above, since there's no history to influence it.
  EXPECT_EQ(pre_entries.targets().at(0), "aaa");
  EXPECT_EQ(pre_entries.targets().at(1), "iii");
  EXPECT_EQ(pre_entries.targets().at(2), "ccc");

  history.set_history({});
  db()->Clear();

  // After clearing both the fake history and the ranking, we should get the
  // initial ranking back, with the (present on system) "eee" app swapped in for
  // "bbb", but "bbb" persisted to disk.
  auto post_ranking = RankSync(&history, {"aaa", "ccc", "eee", "ggg", "iii"});
  ASSERT_TRUE(post_ranking);
  EXPECT_EQ(*post_ranking,
            std::vector<std::string>({"aaa", "eee", "ccc", "$more"}));

  auto post_entries = backing_entries()->at("type");
  EXPECT_EQ(post_entries.targets().at(0), "aaa");
  EXPECT_EQ(post_entries.targets().at(1), "bbb");
  EXPECT_EQ(post_entries.targets().at(2), "ccc");
}

// Regression test for https://crbug.com/374693651
TEST_F(ShareRankingTest, TooFewTiles) {
  ConfigureDefaultInitialRanking();

  FakeShareHistory history;
  history.set_history({{"iii", 10}, {"aaa", 2}, {"ccc", 1}});

  auto ranking =
      RankSync(&history, {"aaa", "ccc", "eee", "ggg", "iii"}, "type", 1, 1);
  ASSERT_TRUE(ranking);
  EXPECT_EQ(*ranking, std::vector<std::string>({"$more"}));
}

}  // namespace sharing
