// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/omnibox/browser/calculator_provider.h"

#include <memory>
#include <string>
#include <vector>

#include "base/memory/scoped_refptr.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/timer/elapsed_timer.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_match.h"
#include "components/omnibox/browser/autocomplete_match_type.h"
#include "components/omnibox/browser/fake_autocomplete_provider_client.h"
#include "components/omnibox/browser/provider_state_service.h"
#include "components/omnibox/browser/search_provider.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "components/omnibox/common/omnibox_feature_configs.h"

namespace {
class FakeSearchProvider : public SearchProvider {
 public:
  using SearchProvider::done_;
  using SearchProvider::matches_;
  using SearchProvider::SearchProvider;

 protected:
  ~FakeSearchProvider() override = default;
};

struct SearchMatch {
  std::u16string contents;
  bool is_calc = true;
};

}  // namespace

class CalculatorProviderTest : public testing::Test,
                               public AutocompleteProviderListener {
 protected:
  CalculatorProviderTest() {
    client_ = std::make_unique<FakeAutocompleteProviderClient>();
    search_provider_ = new FakeSearchProvider(client_.get(), this);
    calculator_provider_ =
        new CalculatorProvider(client_.get(), this, search_provider_.get());
  }

  ~CalculatorProviderTest() override {
    client_->GetProviderStateService()->calculator_provider_cache.clear();
  }

  // AutocompleteProviderListener:
  void OnProviderUpdate(bool updated_matches,
                        const AutocompleteProvider* provider) override {}

  // Helper to simulate the search and calc providers starting. The search
  // provider will complete if `search_completes_sync` is true. It will have
  // `search_matches` regardless of whether it completes.
  void StartAutocompletion(bool search_completes_sync,
                           std::vector<SearchMatch> search_matches,
                           AutocompleteInput input) {
    search_provider_->matches_.clear();
    for (const auto& search_match : search_matches) {
      AutocompleteMatch match(search_provider_.get(), 1000, true,
                              search_match.is_calc
                                  ? AutocompleteMatchType::CALCULATOR
                                  : AutocompleteMatchType::SEARCH_SUGGEST);
      match.contents = search_match.contents;
      search_provider_->matches_.push_back(match);
    }
    search_provider_->done_ = search_completes_sync;
    calculator_provider_->Start(input, false);
  }

  // Helper to simulate the search and calc providers starting. The search
  // provider will complete with either a single non-calc match, or an
  // additional second calc match.
  void TypeAndStartAutocompletion(std::u16string input_text,
                                  bool include_calc) {
    std::vector<SearchMatch> search_matches = {{input_text, false}};
    if (include_calc)
      search_matches.push_back({input_text});
    AutocompleteInput input{input_text, metrics::OmniboxEventProto::OTHER,
                            TestSchemeClassifier()};
    StartAutocompletion(true, search_matches, input);
  }

  std::vector<std::u16string> GetCalcMatches() {
    std::vector<std::u16string> contents;
    for (const auto& m : calculator_provider_->matches())
      contents.push_back(m.contents);
    return contents;
  }

  base::test::ScopedFeatureList feature_list_{
      omnibox_feature_configs::CalcProvider::kCalcProvider};
  std::unique_ptr<FakeAutocompleteProviderClient> client_;
  scoped_refptr<FakeSearchProvider> search_provider_;
  scoped_refptr<CalculatorProvider> calculator_provider_;
  // Used to simulate time passing.
  base::test::SingleThreadTaskEnvironment task_environment_{
      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
};

TEST_F(CalculatorProviderTest, SkipSyncInputs) {
  // Returns early when `omit_asynchronous_matches()` is true.
  AutocompleteInput input;
  input.set_omit_asynchronous_matches(true);
  StartAutocompletion(true, {{u"0+1=1"}}, input);
  EXPECT_TRUE(calculator_provider_->done());
}

TEST_F(CalculatorProviderTest, RunSynclyForSyncSearchProvider) {
  // If search provider finished syncly, so should the calc provider.

  // Search completes syncly with 0 matches.
  StartAutocompletion(true, {}, {});
  EXPECT_TRUE(calculator_provider_->done());
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());

  // Search completes syncly with matches.
  StartAutocompletion(true, {{u"search", false}, {u"0+1=1"}, {u"0+2=2"}}, {});
  EXPECT_TRUE(calculator_provider_->done());
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"0+1=1", u"0+2=2"));
}

TEST_F(CalculatorProviderTest, RunAsynclyForAsyncSearchProvider) {
  // If search provider finished asyncly, so should the calc provider.

  // Start but don't complete search.
  StartAutocompletion(false, {}, {});
  EXPECT_FALSE(calculator_provider_->done());
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());

  // Search completes with 0 matches.
  search_provider_->done_ = true;
  search_provider_->NotifyListeners(false);
  EXPECT_TRUE(calculator_provider_->done());
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());

  // Start but don't complete search.
  StartAutocompletion(false, {{u"search", false}, {u"0+1=1"}, {u"0+2=2"}}, {});
  EXPECT_FALSE(calculator_provider_->done());
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());

  // Complete search with matches
  search_provider_->done_ = true;
  search_provider_->NotifyListeners(false);
  EXPECT_TRUE(calculator_provider_->done());
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"0+1=1", u"0+2=2"));
}

TEST_F(CalculatorProviderTest, CachePreviousCalcSuggestions) {
  // Search has a calc suggestion. It should be shown.
  StartAutocompletion(true, {{u"0+1=1"}}, {});
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"0+1=1"));

  // Search has a different calc suggestion. Both the old and new should be
  // shown.
  StartAutocompletion(true, {{u"0+2=2"}}, {});
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"0+1=1", u"0+2=2"));

  // Search doesn't have a calc suggestion. The old 2 should be shown.
  StartAutocompletion(true, {{u"search", false}}, {});
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"0+1=1", u"0+2=2"));

  // Calc suggestions should expire after 1 hour.
  task_environment_.FastForwardBy(base::Minutes(59));
  StartAutocompletion(true, {{u"search", false}}, {});
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"0+1=1", u"0+2=2"));
  task_environment_.FastForwardBy(base::Minutes(60));
  StartAutocompletion(true, {{u"search", false}}, {});
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());
}

TEST_F(CalculatorProviderTest, MaxCacheSize) {
  StartAutocompletion(true, {{u"1=1"}, {u"2=2"}, {u"3=3"}, {u"4=4"}}, {});
  EXPECT_THAT(GetCalcMatches(),
              testing::ElementsAre(u"1=1", u"2=2", u"3=3", u"4=4"));

  // "2=2" is omitted, because it's the oldest. "1=1" is refreshed below.
  StartAutocompletion(true, {{u"1=1"}, {u"5=5"}, {u"6=6"}, {u"7=7"}}, {});
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"3=3", u"4=4", u"1=1",
                                                     u"5=5", u"6=6", u"7=7"));
}

TEST_F(CalculatorProviderTest, DedupeAndCoalesceIntermediateInputs) {
  // When typing 1+2+3+4, shouldn't see intermediate matches (e.g. 1+2).
  TypeAndStartAutocompletion(u"1", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());
  TypeAndStartAutocompletion(u"1+", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());
  TypeAndStartAutocompletion(u"1+2", true);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2"));
  TypeAndStartAutocompletion(u"1+2+", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2"));
  TypeAndStartAutocompletion(u"1+2+3", true);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2+3"));
  TypeAndStartAutocompletion(u"1+2+3+", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2+3"));
  TypeAndStartAutocompletion(u"1+2+3+4", true);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2+3+4"));

  // When backspace and retyping, shouldn't show duplicates.
  TypeAndStartAutocompletion(u"1+2+3+", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2+3+4"));
  TypeAndStartAutocompletion(u"1+2+3+4", true);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2+3+4"));

  // When backspacing and retyping, should show non-duplicates.
  TypeAndStartAutocompletion(u"1+", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2+3+4"));
  TypeAndStartAutocompletion(u"1+2", true);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+2+3+4", u"1+2"));
}

TEST_F(CalculatorProviderTest, ShowCalcSuggestionsForCorrectInputs) {
  // Should stop showing calc suggestions if there have been no recent ones.
  TypeAndStartAutocompletion(u"1+1", true);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+1"));
  TypeAndStartAutocompletion(u"2+2", true);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+1", u"2+2"));
  TypeAndStartAutocompletion(u"2+2+", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+1", u"2+2"));
  TypeAndStartAutocompletion(u"2+2+x", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+1", u"2+2"));
  TypeAndStartAutocompletion(u"2+2+x+", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+1", u"2+2"));
  // The last calc suggestion was too old. The cached ones shouldn't be shown.
  TypeAndStartAutocompletion(u"2+2+x+y", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());
  // A new calc suggestion should resurface the old ones too
  TypeAndStartAutocompletion(u"3+3", true);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+1", u"2+2", u"3+3"));
  // Cached suggestions should be shown when backspacing as well.
  TypeAndStartAutocompletion(u"3+", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre(u"1+1", u"2+2", u"3+3"));
  // But not when typing an input dissimilar from the last input that had a calc
  // suggestion.
  TypeAndStartAutocompletion(u"3-", false);
  EXPECT_THAT(GetCalcMatches(), testing::ElementsAre());
}
