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

#include "services/device/geolocation/geolocation_provider_impl.h"

#include <memory>
#include <string>

#include "base/at_exit.h"
#include "base/barrier_closure.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/mock_callback.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "base/test/with_feature_override.h"
#include "base/threading/thread_checker.h"
#include "base/time/time.h"
#include "components/content_settings/core/common/features.h"
#include "services/device/geolocation/fake_location_provider.h"
#include "services/device/public/cpp/device_features.h"
#include "services/device/public/cpp/geolocation/location_system_permission_status.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

#if BUILDFLAG(OS_LEVEL_GEOLOCATION_PERMISSION_SUPPORTED)
#include "services/device/public/cpp/test/fake_geolocation_system_permission_manager.h"
#endif

namespace device {
namespace {

using ::base::test::TestFuture;
using ::device::LocationSystemPermissionStatus;

}  // namespace

class GeolocationProviderTest : public testing::Test {
 public:
  void SetUp() override {
#if BUILDFLAG(OS_LEVEL_GEOLOCATION_PERMISSION_SUPPORTED)
    if (features::IsOsLevelGeolocationPermissionSupportEnabled()) {
      fake_geolocation_system_permission_manager_ =
          std::make_unique<FakeGeolocationSystemPermissionManager>();
      GeolocationProviderImpl::SetGeolocationSystemPermissionManagerForTesting(
          static_cast<GeolocationSystemPermissionManager*>(
              fake_geolocation_system_permission_manager_.get()));
    }
#endif
  }

 protected:
  GeolocationProviderTest()
      : task_environment_(
            base::test::SingleThreadTaskEnvironment::MainThreadType::UI,
            base::test::SingleThreadTaskEnvironment::TimeSource::MOCK_TIME) {
    mojom::Geoposition& position1 = *position_result1_->get_position();
    position1.latitude = 12;
    position1.longitude = 34;
    position1.accuracy = 56;
    position1.timestamp = base::Time::Now();

    mojom::Geoposition& position2 = *position_result2_->get_position();
    position2.latitude = 13;
    position2.longitude = 34;
    position2.accuracy = 56;
    position2.timestamp = base::Time::Now();
  }

  GeolocationProviderTest(const GeolocationProviderTest&) = delete;
  GeolocationProviderTest& operator=(const GeolocationProviderTest&) = delete;

  ~GeolocationProviderTest() override = default;

  GeolocationProviderImpl* provider() {
    return GeolocationProviderImpl::GetInstance();
  }

  FakeLocationProvider* location_provider_manager() {
    return location_provider_manager_;
  }

  void SetSystemPermission(LocationSystemPermissionStatus status) {
#if BUILDFLAG(OS_LEVEL_GEOLOCATION_PERMISSION_SUPPORTED)
    fake_geolocation_system_permission_manager_->SetSystemPermission(status);
    RunUntilIdle();
#endif
  }
  void RunUntilIdle() { task_environment_.RunUntilIdle(); }

  // Called on test thread.
  void SetFakeLocationProviderManager();
  bool ProvidersStarted();
  void SendMockLocation(const mojom::GeopositionResult& result);

  bool GetIsRunningPrecise() {
    return provider()->is_running_precise_for_testing();
  }

  device::mojom::GeopositionResultPtr position_result1_ =
      mojom::GeopositionResult::NewPosition(mojom::Geoposition::New());

  device::mojom::GeopositionResultPtr position_result2_ =
      mojom::GeopositionResult::NewPosition(mojom::Geoposition::New());

  device::mojom::GeopositionResultPtr error_result_ =
      mojom::GeopositionResult::NewError(mojom::GeopositionError::New(
          mojom::GeopositionErrorCode::kPermissionDenied,
          GeolocationProviderImpl::kSystemPermissionDeniedErrorMessage,
          GeolocationProviderImpl::kSystemPermissionDeniedErrorTechnical));

  base::test::SingleThreadTaskEnvironment task_environment_;

 private:
  // Called on provider thread.
  bool GetProvidersStarted();

#if BUILDFLAG(OS_LEVEL_GEOLOCATION_PERMISSION_SUPPORTED)
  std::unique_ptr<FakeGeolocationSystemPermissionManager>
      fake_geolocation_system_permission_manager_;
#endif
  // |at_exit| must be initialized before all other variables so that it is
  // available to register with Singletons and can handle tear down when the
  // test completes.
  base::ShadowingAtExitManager at_exit_;

  base::ThreadChecker thread_checker_;

  // Owned by the GeolocationProviderImpl class.
  raw_ptr<FakeLocationProvider> location_provider_manager_ = nullptr;

  // True if |location_provider_manager_| is started.
  bool is_started_;
};

class GeolocationProviderApproxGeoTest : public GeolocationProviderTest {
 protected:
  GeolocationProviderApproxGeoTest() {
    feature_list_.InitWithFeatures(
        /*enabled_features=*/{content_settings::features::
                                  kApproximateGeolocationPermission},
        /*disabled_features=*/{});

    // Create a precise position result.
    precise_position_result_ =
        mojom::GeopositionResult::NewPosition(mojom::Geoposition::New());
    precise_position_result_->get_position()->latitude = 1.0;
    precise_position_result_->get_position()->longitude = 2.0;
    precise_position_result_->get_position()->accuracy = 10.0;
    precise_position_result_->get_position()->is_precise = true;
    precise_position_result_->get_position()->timestamp = base::Time::Now();

    // Create an approximate position result.
    approximate_position_result_ =
        mojom::GeopositionResult::NewPosition(mojom::Geoposition::New());
    approximate_position_result_->get_position()->latitude = 3.0;
    approximate_position_result_->get_position()->longitude = 4.0;
    approximate_position_result_->get_position()->accuracy = 100.0;
    approximate_position_result_->get_position()->is_precise = false;
    approximate_position_result_->get_position()->timestamp = base::Time::Now();
  }

  device::mojom::GeopositionResultPtr precise_position_result_;
  device::mojom::GeopositionResultPtr approximate_position_result_;

 private:
  base::test::ScopedFeatureList feature_list_;
};

void GeolocationProviderTest::SetFakeLocationProviderManager() {
  ASSERT_FALSE(location_provider_manager_);
  auto location_provider_manager = std::make_unique<FakeLocationProvider>();
  location_provider_manager_ = location_provider_manager.get();
  provider()->SetLocationProviderManagerForTesting(
      std::move(location_provider_manager));
}

bool GeolocationProviderTest::ProvidersStarted() {
  DCHECK(provider()->IsRunning());
  DCHECK(thread_checker_.CalledOnValidThread());

  TestFuture<bool> future;
  provider()->task_runner()->PostTaskAndReplyWithResult(
      FROM_HERE,
      base::BindOnce(&GeolocationProviderTest::GetProvidersStarted,
                     base::Unretained(this)),
      future.GetCallback());
  return future.Get();
}

bool GeolocationProviderTest::GetProvidersStarted() {
  DCHECK(provider()->task_runner()->BelongsToCurrentThread());
  is_started_ = location_provider_manager()->state() !=
                mojom::GeolocationDiagnostics::ProviderState::kStopped;
  return is_started_;
}

void GeolocationProviderTest::SendMockLocation(
    const mojom::GeopositionResult& result) {
  DCHECK(provider()->IsRunning());
  DCHECK(thread_checker_.CalledOnValidThread());
  provider()->task_runner()->PostTask(
      FROM_HERE, base::BindOnce(&GeolocationProviderImpl::OnLocationUpdate,
                                base::Unretained(provider()),
                                location_provider_manager_, result.Clone()));
}

// Regression test for http://crbug.com/59377
TEST_F(GeolocationProviderTest, OnPermissionGrantedWithoutObservers) {
  EXPECT_FALSE(provider()->user_did_opt_into_location_services_for_testing());
  provider()->UserDidOptIntoLocationServices();
  EXPECT_TRUE(provider()->user_did_opt_into_location_services_for_testing());
  provider()->clear_user_did_opt_into_location_services_for_testing();
}

TEST_F(GeolocationProviderTest, StartStop) {
  SetFakeLocationProviderManager();
  EXPECT_FALSE(provider()->IsRunning());
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);
  base::CallbackListSubscription subscription =
      provider()->AddLocationUpdateCallback(base::DoNothing(),
                                            /*enable_high_accuracy=*/false);
  EXPECT_TRUE(provider()->IsRunning());
  EXPECT_TRUE(ProvidersStarted());

  subscription = {};

  EXPECT_FALSE(ProvidersStarted());
  EXPECT_TRUE(provider()->IsRunning());
}

class GeolocationProviderTestWithApproxLocation
    : public base::test::WithFeatureOverride,
      public GeolocationProviderTest {
 public:
  GeolocationProviderTestWithApproxLocation()
      : base::test::WithFeatureOverride(
            content_settings::features::kApproximateGeolocationPermission) {}
};

INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(
    GeolocationProviderTestWithApproxLocation);

TEST_P(GeolocationProviderTestWithApproxLocation, StalePositionNotSent) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  {
    TestFuture<mojom::GeopositionResultPtr> future1;

    base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
        mock_callback1;
    EXPECT_CALL(mock_callback1, Run)
        .WillOnce([&](const mojom::GeopositionResult& result) {
          future1.SetValue(result.Clone());
        });

    base::CallbackListSubscription subscription =
        provider()->AddLocationUpdateCallback(mock_callback1.Get(),
                                              /*enable_high_accuracy=*/true);
    SendMockLocation(*position_result1_);
    EXPECT_EQ(future1.Get()->get_position(), position_result1_->get_position());
    subscription = {};
  }

  {
    base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
        mock_callback2;

    // After adding a second callback, check that no unexpected position update
    // is sent.
    EXPECT_CALL(mock_callback2, Run).Times(0);
    base::CallbackListSubscription subscription2 =
        provider()->AddLocationUpdateCallback(mock_callback2.Get(),
                                              /*enable_high_accuracy=*/true);
    base::RunLoop().RunUntilIdle();

    // The second callback should receive the new position now.
    TestFuture<mojom::GeopositionResultPtr> future2;
    EXPECT_CALL(mock_callback2, Run)
        .WillOnce([&](const mojom::GeopositionResult& result) {
          future2.SetValue(result.Clone());
        });
    SendMockLocation(*position_result2_);
    EXPECT_EQ(future2.Get()->get_position(), position_result2_->get_position());
    subscription2 = {};
  }

  EXPECT_FALSE(ProvidersStarted());
}

TEST_F(GeolocationProviderTest, OverrideLocationForTesting) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  provider()->OverrideLocationForTesting(error_result_->Clone());
  // Adding a callback when the location is overridden should synchronously
  // invoke the callback with our overridden position.
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      mock_callback;
  EXPECT_CALL(mock_callback, Run);
  base::CallbackListSubscription subscription =
      provider()->AddLocationUpdateCallback(mock_callback.Get(), false);
  subscription = {};
  // Wait for the providers to be stopped now that all clients are gone.
  EXPECT_FALSE(ProvidersStarted());
}

namespace {

class MockGeolocationInternalsObserver
    : public mojom::GeolocationInternalsObserver {
 public:
  MOCK_METHOD(void,
              OnDiagnosticsChanged,
              (mojom::GeolocationDiagnosticsPtr),
              (override));
  void OnNetworkLocationRequested(
      std::vector<mojom::AccessPointDataPtr> request) override {}
  void OnNetworkLocationReceived(
      mojom::NetworkLocationResponsePtr response) override {}

  void Bind(
      mojo::PendingReceiver<mojom::GeolocationInternalsObserver> receiver) {
    receiver_.Bind(std::move(receiver));
  }
  void Disconnect() { receiver_.reset(); }

 private:
  mojo::Receiver<mojom::GeolocationInternalsObserver> receiver_{this};
};

}  // namespace

TEST_F(GeolocationProviderTest, InitializeWhileObservingDiagnostics) {
  // Add the observer and wait for the initial update.
  MockGeolocationInternalsObserver observer;
  mojo::PendingRemote<mojom::GeolocationInternalsObserver> remote;
  observer.Bind(remote.InitWithNewPipeAndPassReceiver());
  TestFuture<mojom::GeolocationDiagnosticsPtr> add_observer_future;
  provider()->AddInternalsObserver(std::move(remote),
                                   add_observer_future.GetCallback());
  // AddInternalsObserver invokes the callback with nullptr if the API
  // implementation is not yet initialized.
  EXPECT_FALSE(add_observer_future.Get());
  EXPECT_FALSE(provider()->IsRunning());

  // Add a subscription so the provider will be initialized and started.
  TestFuture<mojom::GeolocationDiagnosticsPtr> provider_started_future;
  EXPECT_CALL(observer, OnDiagnosticsChanged).WillOnce([&](auto diagnostics) {
    provider_started_future.SetValue(std::move(diagnostics));
  });
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);
  base::CallbackListSubscription subscription =
      provider()->AddLocationUpdateCallback(base::DoNothing(),
                                            /*enable_high_accuracy=*/false);
  EXPECT_TRUE(ProvidersStarted());

  // Starting the provider updates diagnostics.
  ASSERT_TRUE(provider_started_future.Get());
  EXPECT_EQ(provider_started_future.Get()->provider_state,
            mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy);

  // Calling OnInternalsUpdated updates diagnostics.
  TestFuture<mojom::GeolocationDiagnosticsPtr> internals_updated_future;
  EXPECT_CALL(observer, OnDiagnosticsChanged).WillOnce([&](auto diagnostics) {
    internals_updated_future.SetValue(std::move(diagnostics));
  });
  provider()->SimulateInternalsUpdatedForTesting();
  ASSERT_TRUE(internals_updated_future.Get());
  EXPECT_EQ(internals_updated_future.Get()->provider_state,
            mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy);

  // Stopping the provider updates diagnostics.
  TestFuture<mojom::GeolocationDiagnosticsPtr> provider_stopped_future;
  EXPECT_CALL(observer, OnDiagnosticsChanged).WillOnce([&](auto diagnostics) {
    provider_stopped_future.SetValue(std::move(diagnostics));
  });
  subscription = {};
  EXPECT_FALSE(ProvidersStarted());
  ASSERT_TRUE(provider_stopped_future.Get());
  EXPECT_EQ(provider_stopped_future.Get()->provider_state,
            mojom::GeolocationDiagnostics::ProviderState::kStopped);
}

TEST_F(GeolocationProviderTest, MultipleDiagnosticsObservers) {
  // Add a subscription so the provider will be started.
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);
  base::CallbackListSubscription subscription =
      provider()->AddLocationUpdateCallback(base::DoNothing(),
                                            /*enable_high_accuracy=*/true);
  EXPECT_TRUE(ProvidersStarted());

  // Add two internals observers. Both receive diagnostics indicating the
  // provider is started.
  MockGeolocationInternalsObserver observer1;
  {
    mojo::PendingRemote<mojom::GeolocationInternalsObserver> remote;
    observer1.Bind(remote.InitWithNewPipeAndPassReceiver());
    TestFuture<mojom::GeolocationDiagnosticsPtr> future;
    provider()->AddInternalsObserver(std::move(remote), future.GetCallback());
    ASSERT_TRUE(future.Get());
    EXPECT_EQ(future.Get()->provider_state,
              mojom::GeolocationDiagnostics::ProviderState::kHighAccuracy);
  }
  MockGeolocationInternalsObserver observer2;
  {
    mojo::PendingRemote<mojom::GeolocationInternalsObserver> remote;
    observer2.Bind(remote.InitWithNewPipeAndPassReceiver());
    TestFuture<mojom::GeolocationDiagnosticsPtr> future;
    provider()->AddInternalsObserver(std::move(remote), future.GetCallback());
    ASSERT_TRUE(future.Get());
    EXPECT_EQ(future.Get()->provider_state,
              mojom::GeolocationDiagnostics::ProviderState::kHighAccuracy);
  }

  // Call OnInternalsUpdated. Both observers are notified.
  {
    base::RunLoop loop;
    auto barrier_closure = base::BarrierClosure(2, loop.QuitClosure());
    mojom::GeolocationDiagnosticsPtr diagnostics1;
    EXPECT_CALL(observer1, OnDiagnosticsChanged)
        .WillOnce([&](auto diagnostics) {
          diagnostics1 = std::move(diagnostics);
          barrier_closure.Run();
        });
    mojom::GeolocationDiagnosticsPtr diagnostics2;
    EXPECT_CALL(observer2, OnDiagnosticsChanged)
        .WillOnce([&](auto diagnostics) {
          diagnostics2 = std::move(diagnostics);
          barrier_closure.Run();
        });
    provider()->SimulateInternalsUpdatedForTesting();
    loop.Run();
    ASSERT_TRUE(diagnostics1);
    EXPECT_EQ(diagnostics1->provider_state,
              mojom::GeolocationDiagnostics::ProviderState::kHighAccuracy);
    ASSERT_TRUE(diagnostics2);
    EXPECT_EQ(diagnostics2->provider_state,
              mojom::GeolocationDiagnostics::ProviderState::kHighAccuracy);
  }

  // Disconnect observer1.
  observer1.Disconnect();

  // Call OnInternalsUpdated. Only observer2 is notified.
  {
    TestFuture<mojom::GeolocationDiagnosticsPtr> future;
    EXPECT_CALL(observer2, OnDiagnosticsChanged)
        .WillOnce(
            [&](auto diagnostics) { future.SetValue(std::move(diagnostics)); });
    provider()->SimulateInternalsUpdatedForTesting();
    ASSERT_TRUE(future.Get());
    EXPECT_EQ(future.Get()->provider_state,
              mojom::GeolocationDiagnostics::ProviderState::kHighAccuracy);
  }
}

TEST_F(GeolocationProviderApproxGeoTest, PreciseOnlyMode) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  TestFuture<mojom::GeopositionResultPtr> high_accuracy_future;
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      high_accuracy_callback;
  EXPECT_CALL(high_accuracy_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        high_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription high_accuracy_subscription =
      provider()->AddLocationUpdateCallback(high_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/true);

  EXPECT_TRUE(GetIsRunningPrecise());

  SendMockLocation(*precise_position_result_);
  EXPECT_TRUE(high_accuracy_future.Get()->is_position());
  EXPECT_EQ(high_accuracy_future.Get()->get_position(),
            precise_position_result_->get_position());
}

TEST_F(GeolocationProviderApproxGeoTest, ApproximateOnlyMode) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  TestFuture<mojom::GeopositionResultPtr> low_accuracy_future;
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      low_accuracy_callback;
  EXPECT_CALL(low_accuracy_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        low_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription low_accuracy_subscription =
      provider()->AddLocationUpdateCallback(low_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/false);

  EXPECT_FALSE(GetIsRunningPrecise());

  SendMockLocation(*approximate_position_result_);
  EXPECT_TRUE(low_accuracy_future.Get()->is_position());
  EXPECT_EQ(low_accuracy_future.Get()->get_position(),
            approximate_position_result_->get_position());
}

TEST_F(GeolocationProviderApproxGeoTest, ErrorBroadcast) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  TestFuture<mojom::GeopositionResultPtr> high_accuracy_future;
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      high_accuracy_callback;
  EXPECT_CALL(high_accuracy_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        high_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription high_accuracy_subscription =
      provider()->AddLocationUpdateCallback(high_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/true);

  TestFuture<mojom::GeopositionResultPtr> low_accuracy_future;
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      low_accuracy_callback;
  EXPECT_CALL(low_accuracy_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        low_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription low_accuracy_subscription =
      provider()->AddLocationUpdateCallback(low_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/false);

  // When there are both high and low accuracy callbacks, it should use
  // approximate accuracy.
  EXPECT_FALSE(GetIsRunningPrecise());

  SendMockLocation(*error_result_);
  EXPECT_EQ(high_accuracy_future.Get()->get_error(),
            error_result_->get_error());
  EXPECT_EQ(low_accuracy_future.Get()->get_error(), error_result_->get_error());
}

TEST_F(GeolocationProviderApproxGeoTest, ConcurrentMode) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  // Add a high accuracy client.
  TestFuture<mojom::GeopositionResultPtr> high_accuracy_future;
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      high_accuracy_callback;
  EXPECT_CALL(high_accuracy_callback, Run)
      .WillRepeatedly([&](const mojom::GeopositionResult& result) {
        high_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription high_accuracy_subscription =
      provider()->AddLocationUpdateCallback(high_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/true);
  EXPECT_TRUE(GetIsRunningPrecise());

  // Add a low accuracy client.
  TestFuture<mojom::GeopositionResultPtr> low_accuracy_future;
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      low_accuracy_callback;
  EXPECT_CALL(low_accuracy_callback, Run)
      .WillRepeatedly([&](const mojom::GeopositionResult& result) {
        low_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription low_accuracy_subscription =
      provider()->AddLocationUpdateCallback(low_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/false);

  // Ensure that we are running approximate accuracy in concurrent mode.
  EXPECT_FALSE(GetIsRunningPrecise());

  // Send an approximate location update. Both low-accuracy and high-accuracy
  // clients should receive it.
  SendMockLocation(*approximate_position_result_);
  EXPECT_EQ(low_accuracy_future.Get()->get_position()->latitude,
            approximate_position_result_->get_position()->latitude);
  EXPECT_EQ(high_accuracy_future.Get()->get_position()->latitude,
            approximate_position_result_->get_position()->latitude);
}

TEST_F(GeolocationProviderApproxGeoTest,
       AddHighAccuracyCallbackWithLowAccuracyCache) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  // 1. Send an approximate location update to populate the cache.
  TestFuture<mojom::GeopositionResultPtr> low_accuracy_future;
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      low_accuracy_callback;
  EXPECT_CALL(low_accuracy_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        low_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription low_accuracy_subscription =
      provider()->AddLocationUpdateCallback(low_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/false);
  SendMockLocation(*approximate_position_result_);
  EXPECT_TRUE(low_accuracy_future.Wait());

  // 2. Add a high accuracy callback. It should receive the cached approximate
  // location result immediately.
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      high_accuracy_callback;
  TestFuture<mojom::GeopositionResultPtr> high_accuracy_future;
  EXPECT_CALL(high_accuracy_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        high_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription high_accuracy_subscription =
      provider()->AddLocationUpdateCallback(high_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/true);
  EXPECT_EQ(high_accuracy_future.Get()->get_position()->latitude,
            approximate_position_result_->get_position()->latitude);
}

TEST_F(GeolocationProviderApproxGeoTest, ThrottlingApproximateUpdates) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  base::test::TestFuture<mojom::GeopositionResultPtr> low_accuracy_future;

  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      low_accuracy_callback;
  EXPECT_CALL(low_accuracy_callback, Run)
      .WillRepeatedly([&](const mojom::GeopositionResult& result) {
        low_accuracy_future.SetValue(result.Clone());
      });
  base::CallbackListSubscription low_accuracy_subscription =
      provider()->AddLocationUpdateCallback(low_accuracy_callback.Get(),
                                            /*enable_high_accuracy=*/false);

  // 1. First approximate update should be delivered.
  SendMockLocation(*approximate_position_result_);
  auto result1 = low_accuracy_future.Take();
  EXPECT_TRUE(result1->is_position());
  EXPECT_EQ(result1->get_position()->latitude,
            approximate_position_result_->get_position()->latitude);

  // 2. Subsequent update within 15 minutes should be ignored.
  auto second_approx_result = approximate_position_result_.Clone();
  second_approx_result->get_position()->latitude = 5.0;
  SendMockLocation(*second_approx_result);

  // Deterministically flush the Geolocation thread and then the Main thread.
  // Since SendMockLocation posts to Geolocation, which then posts back to Main,
  // a PostTaskAndReply to Geolocation will arrive at Main AFTER the
  // location update has been processed.
  base::test::TestFuture<void> flush_future;
  provider()->task_runner()->PostTaskAndReply(FROM_HERE, base::DoNothing(),
                                              flush_future.GetCallback());
  EXPECT_TRUE(flush_future.Wait());
  EXPECT_FALSE(low_accuracy_future.IsReady());

  // 3. Fast-forward 14 minutes (still within 15 mins) - still ignored.
  task_environment_.FastForwardBy(base::Minutes(14));
  SendMockLocation(*second_approx_result);
  base::test::TestFuture<void> flush_future2;
  provider()->task_runner()->PostTaskAndReply(FROM_HERE, base::DoNothing(),
                                              flush_future2.GetCallback());
  EXPECT_TRUE(flush_future2.Wait());
  EXPECT_FALSE(low_accuracy_future.IsReady());

  // 4. Fast-forward past 15 minutes - next update should be accepted.
  task_environment_.FastForwardBy(base::Minutes(2));  // Total 16 mins
  SendMockLocation(*second_approx_result);
  auto result2 = low_accuracy_future.Take();
  EXPECT_TRUE(result2->is_position());
  EXPECT_EQ(result2->get_position()->latitude, 5.0);

  // 5. Error should always be delivered and reset throttling.
  SendMockLocation(*error_result_);
  auto result3 = low_accuracy_future.Take();
  EXPECT_TRUE(result3->is_error());

  // Since last successful result was just replaced by an error,
  // the next successful result should be delivered immediately.
  auto third_approx_result = approximate_position_result_.Clone();
  third_approx_result->get_position()->latitude = 7.0;
  SendMockLocation(*third_approx_result);
  auto result4 = low_accuracy_future.Take();
  EXPECT_TRUE(result4->is_position());
  EXPECT_EQ(result4->get_position()->latitude, 7.0);
}

TEST_F(GeolocationProviderApproxGeoTest, ThrottlePersistsAcrossClientChurn) {
  SetFakeLocationProviderManager();
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  // 1. Subscribe a low-accuracy client and get an initial fix.
  {
    base::test::TestFuture<mojom::GeopositionResultPtr> low_accuracy_future;
    base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
        low_accuracy_callback;
    EXPECT_CALL(low_accuracy_callback, Run)
        .WillRepeatedly([&](const mojom::GeopositionResult& result) {
          low_accuracy_future.SetValue(result.Clone());
        });
    base::CallbackListSubscription low_accuracy_subscription =
        provider()->AddLocationUpdateCallback(low_accuracy_callback.Get(),
                                              /*enable_high_accuracy=*/false);

    SendMockLocation(*approximate_position_result_);
    auto result = low_accuracy_future.Take();
    EXPECT_TRUE(result->is_position());
    EXPECT_EQ(result->get_position()->latitude,
              approximate_position_result_->get_position()->latitude);
  }
  // At this point, the subscription is destroyed, and client count drops to 0.

  // 2. Resubscribe a new low-accuracy client. It should receive the cached fix.
  {
    base::test::TestFuture<mojom::GeopositionResultPtr> low_accuracy_future;
    base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
        low_accuracy_callback;
    EXPECT_CALL(low_accuracy_callback, Run)
        .WillRepeatedly([&](const mojom::GeopositionResult& result) {
          low_accuracy_future.SetValue(result.Clone());
        });
    base::CallbackListSubscription low_accuracy_subscription =
        provider()->AddLocationUpdateCallback(low_accuracy_callback.Get(),
                                              /*enable_high_accuracy=*/false);

    auto result = low_accuracy_future.Take();
    EXPECT_TRUE(result->is_position());
    EXPECT_EQ(result->get_position()->latitude,
              approximate_position_result_->get_position()->latitude);

    // 3. Send a DIFFERENT location fix within the 15-minute window.
    // It should be throttled (NOT received).
    auto second_approx_result = approximate_position_result_.Clone();
    second_approx_result->get_position()->latitude = 5.0;
    SendMockLocation(*second_approx_result);

    base::test::TestFuture<void> flush_future;
    provider()->task_runner()->PostTaskAndReply(FROM_HERE, base::DoNothing(),
                                                flush_future.GetCallback());
    EXPECT_TRUE(flush_future.Wait());
    EXPECT_FALSE(low_accuracy_future.IsReady());

    // 4. Fast-forward past 15 minutes - next update should be accepted.
    task_environment_.FastForwardBy(base::Minutes(16));
    SendMockLocation(*second_approx_result);
    auto result2 = low_accuracy_future.Take();
    EXPECT_TRUE(result2->is_position());
    EXPECT_EQ(result2->get_position()->latitude, 5.0);
  }
}

#if BUILDFLAG(OS_LEVEL_GEOLOCATION_PERMISSION_SUPPORTED)
TEST_F(GeolocationProviderTest, StartProviderAfterSystemPermissionGranted) {
  SetFakeLocationProviderManager();

  // The default system permission state is kUndetermined. Adding a location
  // callback should not start provider and the callback should not be
  // called.
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      mock_callback;
  EXPECT_CALL(mock_callback, Run).Times(0);
  base::CallbackListSubscription subscription =
      provider()->AddLocationUpdateCallback(mock_callback.Get(),
                                            /*enable_high_accuracy=*/true);

  // Verify that the provider hasn't started yet due to permission is not
  // granted.
  EXPECT_FALSE(ProvidersStarted());

  // Simulate system permission being granted. Provider should now be active.
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);
  EXPECT_TRUE(ProvidersStarted());

  TestFuture<mojom::GeopositionResultPtr> future;
  EXPECT_CALL(mock_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        future.SetValue(result.Clone());
      });

  // Trigger a location update with the sample data.
  SendMockLocation(*position_result1_);

  // Verify that the mock observer received the correct update.
  EXPECT_EQ(future.Get()->get_position(), position_result1_->get_position());

  subscription = {};
  EXPECT_FALSE(ProvidersStarted());
}

TEST_F(GeolocationProviderTest, AddCallbackWhenSystemPermissionDenied) {
  SetFakeLocationProviderManager();

  // Set system permission state from kUndetermined to kDenied.
  SetSystemPermission(LocationSystemPermissionStatus::kDenied);

  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      mock_callback;
  TestFuture<mojom::GeopositionResultPtr> future;

  // Expect that the callback should be invoked with permission denied error
  // when subscription is created.
  EXPECT_CALL(mock_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        future.SetValue(result.Clone());
      });

  base::CallbackListSubscription subscription =
      provider()->AddLocationUpdateCallback(mock_callback.Get(),
                                            /*enable_high_accuracy=*/true);

  // Verify that callback should be invoked with permission denied error and
  // provider is not started.
  EXPECT_EQ(future.Take()->get_error(), error_result_->get_error());
  EXPECT_FALSE(ProvidersStarted());
}

TEST_F(GeolocationProviderTest, AddCallbackOnPermissionGrantAfterDenied) {
  SetFakeLocationProviderManager();

  // Initially set system permission to denied, then grant it.
  SetSystemPermission(LocationSystemPermissionStatus::kDenied);
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      mock_callback;

  // Expect that the observer should NOT be notified with permission denied
  // error because the cached |result_| should has been cleared when system
  // permission state changed from kDenied to kAllowed.
  EXPECT_CALL(mock_callback, Run).Times(0);
  base::CallbackListSubscription subscription =
      provider()->AddLocationUpdateCallback(mock_callback.Get(),
                                            /*enable_high_accuracy=*/true);
  base::RunLoop().RunUntilIdle();

  // Expect that the observer is notified now when location update is simulated.
  // TestFuture<mojom::GeopositionResultPtr> future;
  TestFuture<mojom::GeopositionResultPtr> future;
  EXPECT_CALL(mock_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        future.SetValue(result.Clone());
      });

  //  Now simulate a location update and expect that position result to be
  //  equal.
  SendMockLocation(*position_result1_);
  EXPECT_EQ(future.Get()->get_position(), position_result1_->get_position());
}

TEST_F(GeolocationProviderTest,
       ReportPermissionDeniedOnSystemPermissionDenied) {
  SetFakeLocationProviderManager();

  // Set system permission state from kUndetermined to kAllowed.
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      mock_callback;
  base::CallbackListSubscription subscription =
      provider()->AddLocationUpdateCallback(mock_callback.Get(),
                                            /*enable_high_accuracy=*/true);

  // Verify that provider is started when subscription is created when system
  // permission is granted.
  EXPECT_TRUE(ProvidersStarted());

  TestFuture<mojom::GeopositionResultPtr> position_future;
  EXPECT_CALL(mock_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        position_future.SetValue(result.Clone());
      });

  // Simulate a location update and expect that position result to be equal.
  SendMockLocation(*position_result1_);
  EXPECT_EQ(position_future.Get()->get_position(),
            position_result1_->get_position());

  TestFuture<mojom::GeopositionResultPtr> error_future;

  // Set system permission state from kAllowed to kDenied. Expect that callback
  // is invoked with permission denied error.
  EXPECT_CALL(mock_callback, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        error_future.SetValue(result.Clone());
      });
  SetSystemPermission(LocationSystemPermissionStatus::kDenied);
  EXPECT_EQ(error_future.Get()->get_error(), error_result_->get_error());

  // Clear subscription and expect that provider is stopped.
  subscription = {};
  EXPECT_FALSE(ProvidersStarted());
}

TEST_F(GeolocationProviderTest,
       SystemPermissionAllowedAfterSystemPermissionDenied) {
  SetFakeLocationProviderManager();

  // Set system permission state from kUndetermined to kDenied.
  SetSystemPermission(LocationSystemPermissionStatus::kDenied);

  TestFuture<mojom::GeopositionResultPtr> error_future;

  // Create 1st callback and expected to be invoked with permission
  // denied error when system permission is denied.
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      mock_callback1;
  EXPECT_CALL(mock_callback1, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        error_future.SetValue(result.Clone());
      });

  base::CallbackListSubscription subscription1 =
      provider()->AddLocationUpdateCallback(mock_callback1.Get(),
                                            /*enable_high_accuracy=*/true);
  EXPECT_EQ(error_future.Get()->get_error(), error_result_->get_error());
  subscription1 = {};
  EXPECT_FALSE(ProvidersStarted());

  // Set system permission state from kDenied to kAllowed.
  SetSystemPermission(LocationSystemPermissionStatus::kAllowed);

  // Created 2nd callback and subscription after system permission is set to
  // kAllowed.
  base::MockCallback<GeolocationProviderImpl::LocationUpdateCallback>
      mock_callback2;
  base::CallbackListSubscription subscription2 =
      provider()->AddLocationUpdateCallback(mock_callback2.Get(),
                                            /*enable_high_accuracy=*/true);

  // Verify that provider is started when subscription2 is created when system
  // permission is granted.
  EXPECT_TRUE(ProvidersStarted());

  TestFuture<mojom::GeopositionResultPtr> position_future;
  EXPECT_CALL(mock_callback2, Run)
      .WillOnce([&](const mojom::GeopositionResult& result) {
        position_future.SetValue(result.Clone());
      });

  // Simulate a location update and expect that position result to be equal.
  SendMockLocation(*position_result1_);
  EXPECT_EQ(position_future.Get()->get_position(),
            position_result1_->get_position());

  subscription2 = {};
  EXPECT_FALSE(ProvidersStarted());
}
#endif  // BUILDFLAG(IS_APPLE) ||
        // BUILDFLAG(OS_LEVEL_GEOLOCATION_PERMISSION_SUPPORTED)

}  // namespace device
