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

#import "components/optimization_guide/core/delivery/prediction_manager.h"

#import "base/files/file_util.h"
#import "base/task/thread_pool.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_command_line.h"
#import "base/test/scoped_feature_list.h"
#import "components/component_updater/pref_names.h"
#import "components/download/internal/background_service/ios/background_download_task_helper.h"
#import "components/optimization_guide/core/delivery/optimization_target_model_observer.h"
#import "components/optimization_guide/core/optimization_guide_enums.h"
#import "components/optimization_guide/core/optimization_guide_features.h"
#import "components/optimization_guide/core/optimization_guide_prefs.h"
#import "components/optimization_guide/core/optimization_guide_permissions_util.h"
#import "components/sync_preferences/pref_service_syncable.h"
#import "components/sync_preferences/testing_pref_service_syncable.h"
#import "components/variations/hashing.h"
#import "components/variations/scoped_variations_ids_provider.h"
#import "ios/chrome/browser/optimization_guide/model/optimization_guide_service.h"
#import "ios/chrome/browser/optimization_guide/model/optimization_guide_service_factory.h"
#import "ios/chrome/browser/optimization_guide/model/optimization_guide_test_utils.h"
#import "ios/chrome/browser/profile/model/test_with_profile.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.h"
#import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
#import "ios/chrome/browser/shared/model/profile/scoped_profile_keep_alive_ios.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/chrome/test/testing_application_context.h"
#import "ios/web/public/test/web_task_environment.h"
#import "net/test/embedded_test_server/default_handlers.h"
#import "net/test/embedded_test_server/embedded_test_server.h"
#import "net/test/embedded_test_server/http_request.h"
#import "net/test/embedded_test_server/http_response.h"
#import "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#import "services/network/test/test_url_loader_factory.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/platform_test.h"

namespace {

constexpr int kSuccessfulModelVersion = 123;

std::unique_ptr<optimization_guide::proto::GetModelsResponse>
BuildGetModelsResponse() {
  auto get_models_response =
      std::make_unique<optimization_guide::proto::GetModelsResponse>();

  auto prediction_model =
      std::make_unique<optimization_guide::proto::PredictionModel>();
  optimization_guide::proto::ModelInfo* model_info =
      prediction_model->mutable_model_info();
  model_info->set_version(2);
  model_info->set_optimization_target(
      optimization_guide::proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD);
  model_info->add_supported_model_engine_versions(
      optimization_guide::proto::ModelEngineVersion::
          MODEL_ENGINE_VERSION_TFLITE_2_8);
  prediction_model->mutable_model()->set_download_url(
      "https://example.com/model");
  *get_models_response->add_models() = *prediction_model.get();

  return get_models_response;
}

class ModelFileObserver
    : public optimization_guide::OptimizationTargetModelObserver {
 public:
  using ModelFileReceivedCallback = base::OnceCallback<void(
      optimization_guide::proto::OptimizationTarget,
      base::optional_ref<const optimization_guide::ModelInfo>)>;

  ModelFileObserver() = default;
  ~ModelFileObserver() override = default;

  void set_model_file_received_callback(ModelFileReceivedCallback callback) {
    file_received_callback_ = std::move(callback);
  }

  void OnModelUpdated(
      optimization_guide::proto::OptimizationTarget optimization_target,
      base::optional_ref<const optimization_guide::ModelInfo> model_info)
      override {
    if (file_received_callback_) {
      std::move(file_received_callback_).Run(optimization_target, model_info);
    }
  }

 private:
  ModelFileReceivedCallback file_received_callback_;
};

enum class PredictionModelsFetcherRemoteResponseType {
  kSuccessfulWithValidModelFile = 0,
  kSuccessfulWithInvalidModelFile = 1,
  kSuccessfulWithValidModelFileAndInvalidAdditionalFiles = 2,
  kSuccessfulWithValidModelFileAndValidAdditionalFiles = 3,
  kUnsuccessful = 4,
};

}  // namespace

class PredictionManagerTestBase : public TestWithProfile {
 public:
  void SetUp() override {
    TestWithProfile::SetUp();

    InitializeFeatureList();

    download::BackgroundDownloadTaskHelper::SetIgnoreLocalSSLErrorForTesting(
        true);

    models_server_ = std::make_unique<net::EmbeddedTestServer>(
        net::EmbeddedTestServer::TYPE_HTTPS);
    models_server_->ServeFilesFromSourceDirectory(
        "ios/chrome/test/data/optimization_guide");
    models_server_->RegisterRequestHandler(
        base::BindRepeating(&PredictionManagerTestBase::HandleGetModelsRequest,
                            base::Unretained(this)));

    ASSERT_TRUE(models_server_->Start());

    https_server_ = std::make_unique<net::EmbeddedTestServer>(
        net::EmbeddedTestServer::TYPE_HTTPS);
    https_server_->ServeFilesFromSourceDirectory(
        "ios/chrome/test/data/optimization_guide");
    ASSERT_TRUE(https_server_->Start());
    https_url_with_content_ = https_server_->GetURL("/english_page.html");
    https_url_without_content_ = https_server_->GetURL("/empty.html");

    model_file_url_ = models_server_->GetURL("/signed_valid_model.crx3");
    model_file_with_good_additional_file_url_ =
        models_server_->GetURL("/additional_file_exists.crx3");
    model_file_with_nonexistent_additional_file_url_ =
        models_server_->GetURL("/additional_file_doesnt_exist.crx3");
    SetUpCommandLine(scoped_command_line_.GetProcessCommandLine());

    scoped_profile_ = CreateProfile("foo");
    TestingApplicationContext::GetGlobal()
        ->GetOptimizationGuideGlobalState()
        ->prediction_manager()
        .SetUrlLoaderFactoryForTesting(
            scoped_profile_.profile()->GetSharedURLLoaderFactory());
  }

  void TearDown() override {
    download::BackgroundDownloadTaskHelper::SetIgnoreLocalSSLErrorForTesting(
        false);
    scoped_profile_.Reset();
    TestWithProfile::TearDown();
  }

  void SetResponseType(
      PredictionModelsFetcherRemoteResponseType response_type) {
    response_type_ = response_type;
  }

  void RegisterWithKeyedService(ModelFileObserver* model_file_observer) {
    OptimizationGuideServiceFactory::GetForProfile(scoped_profile_.profile())
        ->AddObserverForOptimizationTargetModel(
            optimization_guide::proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
            std::nullopt,
            base::ThreadPool::CreateSequencedTaskRunner(
                {base::MayBlock(), base::TaskPriority::BEST_EFFORT}),
            model_file_observer);
  }

  void SetComponentUpdatesEnabled(bool enabled) {
    GetApplicationContext()->GetLocalState()->SetBoolean(
        ::prefs::kComponentUpdatesEnabled, enabled);
  }

 protected:
  virtual void InitializeFeatureList() = 0;

  virtual void SetUpCommandLine(base::CommandLine* cmd) {
    cmd->AppendSwitch("enable-spdy-proxy-auth");

    cmd->AppendSwitch(
        optimization_guide::kGoogleApiKeyConfigurationCheckOverrideSwitch);
    cmd->AppendSwitchASCII(
        optimization_guide::features::
            kOptimizationGuideServiceGetModelsURLSwitch,
        models_server_->GetURL("/").spec());

    cmd->AppendSwitchASCII("force-variation-ids", "4");
    auto* variations_ids_provider =
        variations::VariationsIdsProvider::GetInstance();
    ASSERT_TRUE(variations_ids_provider != nullptr);
    variations_ids_provider->ForceVariationIdsForTesting({}, {"4"});
    cmd->AppendSwitch("append-variations-headers-to-localhost");
  }

  std::unique_ptr<net::test_server::HttpResponse> HandleGetModelsRequest(
      const net::test_server::HttpRequest& request) {
    // Returning nullptr will cause the test server to fallback to serving the
    // file from the test data directory.
    if (request.GetURL() == model_file_url_) {
      return nullptr;
    }
    if (request.GetURL() == model_file_with_good_additional_file_url_) {
      return nullptr;
    }
    if (request.GetURL() == model_file_with_nonexistent_additional_file_url_) {
      return nullptr;
    }

    std::unique_ptr<net::test_server::BasicHttpResponse> response;

    response = std::make_unique<net::test_server::BasicHttpResponse>();
    // The request to the remote Optimization Guide Service should always be a
    // POST.
    EXPECT_EQ(request.method, net::test_server::METHOD_POST);
    // Variations headers are not supported by iOS model fetcher.

    optimization_guide::proto::GetModelsRequest models_request;
    EXPECT_TRUE(models_request.ParseFromString(request.content));

    response->set_code(net::HTTP_OK);
    std::unique_ptr<optimization_guide::proto::GetModelsResponse>
        get_models_response = BuildGetModelsResponse();
    if (response_type_ == PredictionModelsFetcherRemoteResponseType::
                              kSuccessfulWithInvalidModelFile) {
      get_models_response->mutable_models(0)->mutable_model()->set_download_url(
          https_url_with_content_.spec());
    } else if (response_type_ == PredictionModelsFetcherRemoteResponseType::
                                     kSuccessfulWithValidModelFile) {
      get_models_response->mutable_models(0)->mutable_model()->set_download_url(
          model_file_url_.spec());
    } else if (response_type_ ==
               PredictionModelsFetcherRemoteResponseType::
                   kSuccessfulWithValidModelFileAndInvalidAdditionalFiles) {
      get_models_response->mutable_models(0)->mutable_model()->set_download_url(
          model_file_with_nonexistent_additional_file_url_.spec());
    } else if (response_type_ ==
               PredictionModelsFetcherRemoteResponseType::
                   kSuccessfulWithValidModelFileAndValidAdditionalFiles) {
      get_models_response->mutable_models(0)->mutable_model()->set_download_url(
          model_file_with_good_additional_file_url_.spec());
    } else if (response_type_ ==
               PredictionModelsFetcherRemoteResponseType::kUnsuccessful) {
      response->set_code(net::HTTP_NOT_FOUND);
    }

    std::string serialized_response;
    get_models_response->SerializeToString(&serialized_response);
    response->set_content(serialized_response);
    return std::move(response);
  }

  GURL model_file_url_;
  GURL model_file_with_good_additional_file_url_;
  GURL model_file_with_nonexistent_additional_file_url_;
  GURL https_url_with_content_;
  GURL https_url_without_content_;
  base::test::ScopedFeatureList scoped_feature_list_;
  ScopedProfileKeepAliveIOS scoped_profile_;
  variations::test::ScopedVariationsIdsProvider scoped_variations_ids_provider_{
      variations::VariationsIdsProvider::Mode::kUseSignedInState};
  base::test::ScopedCommandLine scoped_command_line_;
  std::unique_ptr<net::EmbeddedTestServer> https_server_;
  std::unique_ptr<net::EmbeddedTestServer> models_server_;
  PredictionModelsFetcherRemoteResponseType response_type_ =
      PredictionModelsFetcherRemoteResponseType::kSuccessfulWithValidModelFile;
  network::TestURLLoaderFactory test_loader_factory_;
};

class PredictionManagerTest : public PredictionManagerTestBase {
 protected:
  void InitializeFeatureList() override {
    std::vector<base::test::FeatureRefAndParams> enabled_features = {
        {optimization_guide::features::kOptimizationHints, {}},
        {optimization_guide::features::kOptimizationTargetPrediction,
         {{"fetch_startup_delay_ms", "2000"}}},
    };
    scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
  }
};

TEST_F(PredictionManagerTest, ComponentUpdatesEnabledPrefDisabled) {
  ModelFileObserver model_file_observer;
  SetResponseType(PredictionModelsFetcherRemoteResponseType::kUnsuccessful);
  SetComponentUpdatesEnabled(false);
  base::HistogramTester histogram_tester;
  RegisterWithKeyedService(&model_file_observer);
  base::RunLoop().RunUntilIdle();

  // Should not have made fetch request.
  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionModelFetcher.GetModelsResponse.Status", 0);
}

TEST_F(PredictionManagerTest, ModelsAndFeaturesStoreInitialized) {
  ModelFileObserver model_file_observer;
  SetResponseType(
      PredictionModelsFetcherRemoteResponseType::kSuccessfulWithValidModelFile);
  base::HistogramTester histogram_tester;

  RegisterWithKeyedService(&model_file_observer);
  RetryForHistogramUntilCountReached(
      &histogram_tester,
      "OptimizationGuide.PredictionManager.PredictionModelsStored", 1);
  RetryForHistogramUntilCountReached(
      &histogram_tester,
      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 1);
  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionManager.PredictionModelsStored", true, 1);
  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelUpdateVersion.PainfulPageLoad",
      kSuccessfulModelVersion, 1);
  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad",
      kSuccessfulModelVersion, 1);
}

TEST_F(PredictionManagerTest, PredictionModelFetchFailed) {
  ModelFileObserver model_file_observer;
  SetResponseType(PredictionModelsFetcherRemoteResponseType::kUnsuccessful);
  base::HistogramTester histogram_tester;

  RegisterWithKeyedService(&model_file_observer);

  // Wait until histograms have been updated before performing checks for
  // correct behavior based on the response.
  RetryForHistogramUntilCountReached(
      &histogram_tester,
      "OptimizationGuide.PredictionModelFetcher.GetModelsResponse.Status", 1);

  histogram_tester.ExpectBucketCount(
      "OptimizationGuide.PredictionModelFetcher.GetModelsResponse.Status",
      net::HTTP_NOT_FOUND, 1);

  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionManager.PredictionModelsStored", 0);
  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionModelUpdateVersion.PainfulPageLoad", 0);
  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 0);
}

class PredictionManagerModelDownloadingBrowserTest
    : public PredictionManagerTest {
 public:
  PredictionManagerModelDownloadingBrowserTest() = default;
  ~PredictionManagerModelDownloadingBrowserTest() override = default;

  void SetUp() override {
    model_file_observer_ = std::make_unique<ModelFileObserver>();
    PredictionManagerTest::SetUp();
  }

  ModelFileObserver* model_file_observer() {
    return model_file_observer_.get();
  }

  void RegisterModelFileObserverWithProfile(ProfileIOS* profile = nullptr) {
    OptimizationGuideServiceFactory::GetForProfile(
        profile ? profile : scoped_profile_.profile())
        ->AddObserverForOptimizationTargetModel(
            optimization_guide::proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD,
            /*model_metadata=*/std::nullopt,
            base::ThreadPool::CreateSequencedTaskRunner(
                {base::MayBlock(), base::TaskPriority::BEST_EFFORT}),
            model_file_observer_.get());
  }

  // Sets up the model observer to receive valid ModelInfo.
  void SetUpValidModelInfoReceival(base::RunLoop* run_loop,
                                   const std::set<base::FilePath::StringType>&
                                       expected_additional_files = {}) {
    model_file_observer_->set_model_file_received_callback(base::BindOnce(
        [](base::RunLoop* run_loop,
           const std::set<base::FilePath::StringType>&
               expected_additional_files,
           optimization_guide::proto::OptimizationTarget optimization_target,
           base::optional_ref<const optimization_guide::ModelInfo> model_info) {
          base::ScopedAllowBlockingForTesting scoped_allow_blocking;

          EXPECT_EQ(
              optimization_target,
              optimization_guide::proto::OPTIMIZATION_TARGET_PAINFUL_PAGE_LOAD);
          EXPECT_TRUE(model_info.has_value());

          EXPECT_EQ(123, model_info->version);
          EXPECT_TRUE(model_info->model_file_path.IsAbsolute());
          EXPECT_TRUE(base::PathExists(model_info->model_file_path));

          EXPECT_EQ(expected_additional_files.size(),
                    model_info->additional_files.size());
          for (const base::FilePath& add_file : model_info->additional_files) {
            EXPECT_TRUE(add_file.IsAbsolute());
            EXPECT_TRUE(base::PathExists(add_file));
            EXPECT_TRUE(expected_additional_files.contains(
                add_file.BaseName().value()));
          }
          run_loop->Quit();
        },
        run_loop, expected_additional_files));
  }

  // Sets up the model observer to not receive any model.
  void SetUpNoModelInfoReceival() {
    model_file_observer_->set_model_file_received_callback(base::BindOnce(
        [](optimization_guide::proto::OptimizationTarget optimization_target,
           base::optional_ref<const optimization_guide::ModelInfo> model_info) {
          FAIL() << "Should not be called";
        }));
  }

 private:
  void InitializeFeatureList() override {
    std::vector<base::test::FeatureRefAndParams> enabled_features = {
        {optimization_guide::features::kOptimizationHints, {}},
        {optimization_guide::features::kOptimizationTargetPrediction,
         {{"fetch_startup_delay_ms", "2000"}}},
    };
    scoped_feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
  }

 protected:
  std::unique_ptr<ModelFileObserver> model_file_observer_;
};

TEST_F(PredictionManagerModelDownloadingBrowserTest,
       TestDownloadUrlAcceptedByDownloadServiceButInvalid) {
  base::HistogramTester histogram_tester;

  SetResponseType(PredictionModelsFetcherRemoteResponseType::
                      kSuccessfulWithInvalidModelFile);

  // Registering should initiate the fetch and receive a response with a model
  // containing a download URL and then subsequently downloaded.
  RegisterModelFileObserverWithProfile();

  RetryForHistogramUntilCountReached(
      &histogram_tester,
      "OptimizationGuide.PredictionModelDownloadManager.DownloadStatus", 1);

  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelDownloadManager.DownloadStatus",
      optimization_guide::PredictionModelDownloadStatus::kFailedCrxVerification,
      1);
  // An unverified file should not notify us that it's ready.
  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionModelUpdateVersion.PainfulPageLoad", 0);
}

TEST_F(PredictionManagerModelDownloadingBrowserTest,
       TestSuccessfulModelFileFlow) {
  base::HistogramTester histogram_tester;

  SetResponseType(
      PredictionModelsFetcherRemoteResponseType::kSuccessfulWithValidModelFile);

  std::unique_ptr<base::RunLoop> run_loop = std::make_unique<base::RunLoop>();
  SetUpValidModelInfoReceival(run_loop.get());

  // Registering should initiate the fetch and receive a response with a model
  // containing a download URL and then subsequently downloaded.
  RegisterModelFileObserverWithProfile();

  // Wait until the observer receives the file. We increase the timeout to 60
  // seconds here since the file is on the larger side.
  {
    base::test::ScopedRunLoopTimeout file_download_timeout(FROM_HERE,
                                                           base::Seconds(60));
    run_loop->Run();
  }

  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelDownloadManager.DownloadStatus",
      optimization_guide::PredictionModelDownloadStatus::kSuccess, 1);

  // No error when moving the file so there will be no record.
  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionModelDownloadManager.ReplaceFileError", 0);
  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelUpdateVersion.PainfulPageLoad",
      kSuccessfulModelVersion, 1);
  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad",
      kSuccessfulModelVersion, 1);
}

TEST_F(PredictionManagerModelDownloadingBrowserTest,
       TestSuccessfulModelFileFlowWithAdditionalFile) {
  base::HistogramTester histogram_tester;

  SetResponseType(PredictionModelsFetcherRemoteResponseType::
                      kSuccessfulWithValidModelFileAndValidAdditionalFiles);

  std::unique_ptr<base::RunLoop> run_loop = std::make_unique<base::RunLoop>();
  SetUpValidModelInfoReceival(run_loop.get(),
                              {FILE_PATH_LITERAL("good_additional_file.txt")});

  // Registering should initiate the fetch and receive a response with a model
  // containing a download URL and then subsequently downloaded.
  RegisterModelFileObserverWithProfile();

  // Wait until the observer receives the file. We increase the timeout to 60
  // seconds here since the file is on the larger side.
  {
    base::test::ScopedRunLoopTimeout file_download_timeout(FROM_HERE,
                                                           base::Seconds(60));
    run_loop->Run();
  }

  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelDownloadManager.DownloadStatus",
      optimization_guide::PredictionModelDownloadStatus::kSuccess, 1);

  // No error when moving the file so there will be no record.
  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionModelDownloadManager.ReplaceFileError", 0);
  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelUpdateVersion.PainfulPageLoad",
      kSuccessfulModelVersion, 1);
  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad",
      kSuccessfulModelVersion, 1);
}

TEST_F(PredictionManagerModelDownloadingBrowserTest,
       TestSuccessfulModelFileFlowWithInvalidAdditionalFile) {
  base::HistogramTester histogram_tester;

  SetResponseType(PredictionModelsFetcherRemoteResponseType::
                      kSuccessfulWithValidModelFileAndInvalidAdditionalFiles);
  SetUpNoModelInfoReceival();

  // Registering should initiate the fetch and receive a response with a model
  // containing a download URL and then subsequently downloaded. But the model
  // with invalid additional file, will not be delivered to observers.
  RegisterModelFileObserverWithProfile();

  RetryForHistogramUntilCountReached(
      &histogram_tester,
      "OptimizationGuide.PredictionModelDownloadManager.DownloadStatus", 1);
  base::RunLoop().RunUntilIdle();

  // The additional file does not exist.
  histogram_tester.ExpectUniqueSample(
      "OptimizationGuide.PredictionModelDownloadManager.DownloadStatus",
      optimization_guide::PredictionModelDownloadStatus::
          kFailedInvalidAdditionalFile,
      1);

  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionModelUpdateVersion.PainfulPageLoad", 0);
  histogram_tester.ExpectTotalCount(
      "OptimizationGuide.PredictionModelLoadedVersion.PainfulPageLoad", 0);
}
