// Copyright 2018 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 "components/assist_ranker/classifier_predictor.h"

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

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/feature_list.h"
#include "base/test/scoped_feature_list.h"
#include "components/assist_ranker/example_preprocessing.h"
#include "components/assist_ranker/fake_ranker_model_loader.h"
#include "components/assist_ranker/nn_classifier_test_util.h"
#include "components/assist_ranker/proto/ranker_model.pb.h"
#include "components/assist_ranker/ranker_model.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace assist_ranker {

using ::assist_ranker::testing::FakeRankerModelLoader;
using ::testing::FloatEq;

// Preprocessor feature names.
const char kFeatureName0[] = "feature_0";
const char kFeatureName1[] = "feature_1";
const char kFeatureExtra[] = "feature_extra";

class ClassifierPredictorTest : public ::testing::Test {
 public:
  void SetUp() override;

  std::unique_ptr<ClassifierPredictor> InitPredictor(
      std::unique_ptr<RankerModel> ranker_model,
      const PredictorConfig& config);

  PredictorConfig GetConfig();

 protected:
  base::test::ScopedFeatureList scoped_feature_list_;
};

void ClassifierPredictorTest::SetUp() {
  ::testing::Test::SetUp();
  scoped_feature_list_.Init();
}

std::unique_ptr<ClassifierPredictor> ClassifierPredictorTest::InitPredictor(
    std::unique_ptr<RankerModel> ranker_model,
    const PredictorConfig& config) {
  std::unique_ptr<ClassifierPredictor> predictor(
      new ClassifierPredictor(config));
  auto fake_model_loader = std::make_unique<FakeRankerModelLoader>(
      base::Bind(&ClassifierPredictor::ValidateModel),
      base::Bind(&ClassifierPredictor::OnModelAvailable,
                 base::Unretained(predictor.get())),
      std::move(ranker_model));
  predictor->LoadModel(std::move(fake_model_loader));
  return predictor;
}

const base::Feature kTestRankerQuery{"TestRankerQuery",
                                     base::FEATURE_ENABLED_BY_DEFAULT};

const base::FeatureParam<std::string> kTestRankerUrl{
    &kTestRankerQuery, "url-param-name", "https://default.model.url"};

PredictorConfig ClassifierPredictorTest::GetConfig() {
  return PredictorConfig("model_name", "logging_name", "uma_prefix", LOG_NONE,
                         GetEmptyWhitelist(), &kTestRankerQuery,
                         &kTestRankerUrl, 0);
}

TEST_F(ClassifierPredictorTest, EmptyRankerModel) {
  auto ranker_model = std::make_unique<RankerModel>();
  auto predictor = InitPredictor(std::move(ranker_model), GetConfig());
  EXPECT_FALSE(predictor->IsReady());

  RankerExample ranker_example;
  auto& features = *ranker_example.mutable_features();
  features[kFeatureName0].set_bool_value(true);
  std::vector<float> response;
  EXPECT_FALSE(predictor->Predict(ranker_example, &response));
}

TEST_F(ClassifierPredictorTest, NoInferenceModuleForModel) {
  auto ranker_model = std::make_unique<RankerModel>();
  // TranslateRankerModel does not have an inference module. Validation will
  // fail.
  ranker_model->mutable_proto()
      ->mutable_translate()
      ->mutable_translate_logistic_regression_model()
      ->set_bias(1);
  auto predictor = InitPredictor(std::move(ranker_model), GetConfig());
  EXPECT_FALSE(predictor->IsReady());

  RankerExample ranker_example;
  auto& features = *ranker_example.mutable_features();
  features[kFeatureName0].set_bool_value(true);
  std::vector<float> response;
  EXPECT_FALSE(predictor->Predict(ranker_example, &response));
  EXPECT_FALSE(predictor->Predict({0, 0}, &response));
}

TEST_F(ClassifierPredictorTest, PredictFeatureVector) {
  auto ranker_model = std::make_unique<RankerModel>();
  *ranker_model->mutable_proto()->mutable_nn_classifier() =
      nn_classifier::CreateXorClassifierModel();
  auto predictor = InitPredictor(std::move(ranker_model), GetConfig());
  EXPECT_TRUE(predictor->IsReady());

  std::vector<float> response;
  // True responses.
  EXPECT_TRUE(predictor->Predict({0, 1}, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(2.8271765));
  EXPECT_TRUE(predictor->Predict({1, 0}, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(2.6790769));

  // False responses.
  EXPECT_TRUE(predictor->Predict({0, 0}, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(-2.7154054));
  EXPECT_TRUE(predictor->Predict({1, 1}, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(-3.1652793));
}

TEST_F(ClassifierPredictorTest, PredictRankerExampleNoPreprocessor) {
  auto ranker_model = std::make_unique<RankerModel>();
  *ranker_model->mutable_proto()->mutable_nn_classifier() =
      nn_classifier::CreateXorClassifierModel();
  auto predictor = InitPredictor(std::move(ranker_model), GetConfig());
  EXPECT_TRUE(predictor->IsReady());

  // Prediction of RankerExample without preprocessor config should fail.
  std::vector<float> response;
  RankerExample example;
  EXPECT_FALSE(predictor->Predict(RankerExample(), &response));
}

TEST_F(ClassifierPredictorTest, PredictRankerExampleWithPreprocessor) {
  auto ranker_model = std::make_unique<RankerModel>();
  auto& model = *ranker_model->mutable_proto()->mutable_nn_classifier();
  model = nn_classifier::CreateXorClassifierModel();

  // Set up the preprocessor config with two features at feature vector
  // indices 0 and 1.
  auto& indices =
      *model.mutable_preprocessor_config()->mutable_feature_indices();
  indices[kFeatureName0] = 0;
  indices[kFeatureName1] = 1;

  auto predictor = InitPredictor(std::move(ranker_model), GetConfig());
  EXPECT_TRUE(predictor->IsReady());

  // Prediction of RankerExample with preprocessor config should work.
  RankerExample example;
  auto& feature_map = *example.mutable_features();
  std::vector<float> response;

  // True responses.
  feature_map[kFeatureName0].set_float_value(0);
  feature_map[kFeatureName1].set_float_value(1);
  EXPECT_TRUE(predictor->Predict(example, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(2.8271765));

  feature_map[kFeatureName0].set_float_value(1);
  feature_map[kFeatureName1].set_float_value(0);
  EXPECT_TRUE(predictor->Predict(example, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(2.6790769));

  // False responses.
  feature_map[kFeatureName0].set_float_value(0);
  feature_map[kFeatureName1].set_float_value(0);
  EXPECT_TRUE(predictor->Predict(example, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(-2.7154054));

  feature_map[kFeatureName0].set_float_value(1);
  feature_map[kFeatureName1].set_float_value(1);
  EXPECT_TRUE(predictor->Predict(example, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(-3.1652793));

  // Check that extra features do not cause an error.
  feature_map[kFeatureName0].set_float_value(0);
  feature_map[kFeatureName1].set_float_value(1);
  feature_map[kFeatureExtra].set_float_value(1);
  EXPECT_TRUE(predictor->Predict(example, &response));
  EXPECT_EQ(response.size(), 1u);
  EXPECT_THAT(response[0], FloatEq(2.8271765));
}

TEST_F(ClassifierPredictorTest, PredictRankerExamplePreprocessorError) {
  auto ranker_model = std::make_unique<RankerModel>();
  auto& model = *ranker_model->mutable_proto()->mutable_nn_classifier();
  model = nn_classifier::CreateXorClassifierModel();

  // Set up the preprocessor config with two features at feature vector
  // indices 0 and 1.
  auto& config = *model.mutable_preprocessor_config();
  auto& indices = *config.mutable_feature_indices();
  indices[kFeatureName0] = 0;
  indices[kFeatureName1] = 1;
  // Zero normalizer will generate a preprocessing error.
  (*config.mutable_normalizers())[kFeatureName0] = 0;

  auto predictor = InitPredictor(std::move(ranker_model), GetConfig());
  EXPECT_TRUE(predictor->IsReady());

  // Prediction of RankerExample should fail due to preprocessing error.
  RankerExample example;
  auto& feature_map = *example.mutable_features();
  std::vector<float> response;
  feature_map[kFeatureName0].set_float_value(0);
  feature_map[kFeatureName1].set_float_value(1);
  EXPECT_FALSE(predictor->Predict(example, &response));
}
}  // namespace assist_ranker
