// Copyright 2017 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 "content/browser/screen_orientation/screen_orientation_provider.h"

#include "base/optional.h"
#include "base/run_loop.h"
#include "content/common/frame_messages.h"
#include "content/public/browser/screen_orientation_delegate.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "third_party/blink/public/common/screen_orientation/web_screen_orientation_lock_type.h"

namespace content {

using device::mojom::ScreenOrientationLockResult;

namespace {

class FakeScreenOrientationDelegate : public ScreenOrientationDelegate {
 public:
  FakeScreenOrientationDelegate(bool supported, bool full_screen_required)
      : supported_(supported), full_screen_required_(full_screen_required) {
    ScreenOrientationProvider::SetDelegate(this);
  }

  ~FakeScreenOrientationDelegate() override = default;

  bool FullScreenRequired(WebContents* web_contents) override {
    return full_screen_required_;
  }

  bool ScreenOrientationProviderSupported() override { return supported_; }

  void Lock(WebContents* web_contents,
            blink::WebScreenOrientationLockType lock_orientation) override {
    lock_count_++;
  }

  void Unlock(WebContents* web_contents) override { unlock_count_++; }

  int lock_count() const { return lock_count_; }
  int unlock_count() const { return unlock_count_; }

 private:
  bool supported_ = true;
  bool full_screen_required_ = false;

  int lock_count_ = 0;
  int unlock_count_ = 0;

  DISALLOW_COPY_AND_ASSIGN(FakeScreenOrientationDelegate);
};

class FakeWebContentsDelegate : public WebContentsDelegate {
 public:
  FakeWebContentsDelegate() = default;
  ~FakeWebContentsDelegate() override = default;

  void EnterFullscreenModeForTab(
      WebContents* web_contents,
      const GURL& origin,
      const blink::WebFullscreenOptions& options) override {
    fullscreened_contents_ = web_contents;
  }

  void ExitFullscreenModeForTab(WebContents* web_contents) override {
    fullscreened_contents_ = nullptr;
  }

  bool IsFullscreenForTabOrPending(
      const WebContents* web_contents) const override {
    return fullscreened_contents_ && web_contents == fullscreened_contents_;
  }

 private:
  WebContents* fullscreened_contents_ = nullptr;

  DISALLOW_COPY_AND_ASSIGN(FakeWebContentsDelegate);
};

void LockResultCallback(base::Optional<ScreenOrientationLockResult>* out_result,
                        ScreenOrientationLockResult result) {
  *out_result = result;
}

}  // namespace

class ScreenOrientationProviderTest : public RenderViewHostImplTestHarness {
 public:
  ScreenOrientationProviderTest() = default;

  void SetUp() override {
    content::RenderViewHostImplTestHarness::SetUp();

    contents()->SetDelegate(&wc_delegate_);
  }

  // Helpers for testing ScreenOrientationProvider methods.
  void CallLockAndGetResult(
      blink::WebScreenOrientationLockType orientation,
      base::Optional<ScreenOrientationLockResult>* out_result) {
    contents()->GetScreenOrientationProviderForTesting()->LockOrientation(
        orientation, base::BindOnce(&LockResultCallback, out_result));

    base::RunLoop().RunUntilIdle();
  }

  void CallUnlock() {
    contents()->GetScreenOrientationProviderForTesting()->UnlockOrientation();
  }

 private:
  FakeWebContentsDelegate wc_delegate_;
};

// Lock operation is not available.
TEST_F(ScreenOrientationProviderTest, DelegateNotAvailableLockOnce) {
  // No ScreenOrientationDelegate.
  base::Optional<ScreenOrientationLockResult> result_1;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result_1);
  EXPECT_EQ(ScreenOrientationLockResult::
                SCREEN_ORIENTATION_LOCK_RESULT_ERROR_NOT_AVAILABLE,
            *result_1);

  // ScreenOrientationDelegate not supported.
  FakeScreenOrientationDelegate delegate(false, false);
  base::Optional<ScreenOrientationLockResult> result_2;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result_2);
  EXPECT_EQ(ScreenOrientationLockResult::
                SCREEN_ORIENTATION_LOCK_RESULT_ERROR_NOT_AVAILABLE,
            *result_2);
}

// Full screen is not required by delegate, normally lock once.
TEST_F(ScreenOrientationProviderTest, DelegateLockOnce) {
  // ScreenOrientationDelegate does not require full screen.
  FakeScreenOrientationDelegate delegate(true, false);

  // Navigate to a site.
  const GURL url("http://www.google.com");
  controller().LoadURL(url, Referrer(), ui::PAGE_TRANSITION_TYPED,
                       std::string());

  base::Optional<ScreenOrientationLockResult> result_1;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result_1);
  // Lock request is pending.
  EXPECT_FALSE(result_1.has_value());
  // Delegate did apply lock once.
  EXPECT_EQ(1, delegate.lock_count());
}

// Full screen is required by delegate.
TEST_F(ScreenOrientationProviderTest, DelegateRequireFullScreenLockOnce) {
  // ScreenOrientationDelegate requires full screen.
  FakeScreenOrientationDelegate delegate(true, true);

  // Navigate to a site.
  const GURL url("http://www.google.com");
  controller().LoadURL(url, Referrer(), ui::PAGE_TRANSITION_TYPED,
                       std::string());

  // Current web contents is not in full screen.
  ASSERT_FALSE(contents()->IsFullscreenForCurrentTab());
  base::Optional<ScreenOrientationLockResult> result_1;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result_1);
  EXPECT_EQ(ScreenOrientationLockResult::
                SCREEN_ORIENTATION_LOCK_RESULT_ERROR_FULLSCREEN_REQUIRED,
            *result_1);
  // Delegate did not apply any lock.
  EXPECT_EQ(0, delegate.lock_count());

  // Simulates entering full screen.
  main_test_rfh()->OnMessageReceived(FrameHostMsg_EnterFullscreen(
      main_test_rfh()->GetRoutingID(), blink::WebFullscreenOptions()));
  ASSERT_TRUE(contents()->IsFullscreenForCurrentTab());

  base::Optional<ScreenOrientationLockResult> result_2;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result_2);
  // Lock request is pending.
  EXPECT_FALSE(result_2.has_value());
  // Delegate did apply lock once.
  EXPECT_EQ(1, delegate.lock_count());
}

// Lock once, then unlock once, the pending lock request will be cancelled.
TEST_F(ScreenOrientationProviderTest, DelegateLockThenUnlock) {
  FakeScreenOrientationDelegate delegate(true, false);

  // Navigate to a site.
  const GURL url("http://www.google.com");
  controller().LoadURL(url, Referrer(), ui::PAGE_TRANSITION_TYPED,
                       std::string());

  base::Optional<ScreenOrientationLockResult> result_1;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result_1);
  // The lock request will be pending.
  EXPECT_FALSE(result_1.has_value());
  // Delegate did apply lock once.
  EXPECT_EQ(1, delegate.lock_count());
  EXPECT_EQ(0, delegate.unlock_count());

  CallUnlock();
  // The pending lock request is cancelled.
  EXPECT_EQ(ScreenOrientationLockResult::
                SCREEN_ORIENTATION_LOCK_RESULT_ERROR_CANCELED,
            result_1);
  // Delegate did apply unlock once.
  EXPECT_EQ(1, delegate.unlock_count());
}

// Lock twice, the first lock request will be cancelled by the second one.
TEST_F(ScreenOrientationProviderTest, DelegateLockThenLock) {
  FakeScreenOrientationDelegate delegate(true, false);

  // Navigate to a site.
  const GURL url("http://www.google.com");
  controller().LoadURL(url, Referrer(), ui::PAGE_TRANSITION_TYPED,
                       std::string());

  base::Optional<ScreenOrientationLockResult> result_1;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result_1);
  // The lock request will be pending.
  EXPECT_FALSE(result_1.has_value());
  // Delegate did apply lock once.
  EXPECT_EQ(1, delegate.lock_count());
  EXPECT_EQ(0, delegate.unlock_count());

  base::Optional<ScreenOrientationLockResult> result_2;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result_2);
  // The pending lock request is cancelled.
  EXPECT_EQ(ScreenOrientationLockResult::
                SCREEN_ORIENTATION_LOCK_RESULT_ERROR_CANCELED,
            result_1);
  // The second one became pending.
  EXPECT_FALSE(result_2.has_value());
  // Delegate did apply lock once more.
  EXPECT_EQ(2, delegate.lock_count());
  EXPECT_EQ(0, delegate.unlock_count());
}

// Unlock won't be applied if no lock has been applied previously.
TEST_F(ScreenOrientationProviderTest, NoUnlockWithoutLock) {
  FakeScreenOrientationDelegate delegate(true, false);

  // Navigate to a site.
  const GURL url("http://www.google.com");
  controller().LoadURL(url, Referrer(), ui::PAGE_TRANSITION_TYPED,
                       std::string());

  CallUnlock();
  // Delegate did not apply any unlock.
  EXPECT_EQ(0, delegate.unlock_count());
}

// If lock already applied once in full screen, then unlock should be triggered
// once automatically when exiting full screen, and previous pending lock
// request will be cancelled.
TEST_F(ScreenOrientationProviderTest, UnlockWhenExitingFullScreen) {
  // ScreenOrientationDelegate requires full screen.
  FakeScreenOrientationDelegate delegate(true, true);

  // Navigate to a site.
  const GURL url("http://www.google.com");
  controller().LoadURL(url, Referrer(), ui::PAGE_TRANSITION_TYPED,
                       std::string());

  // Simulates entering full screen.
  main_test_rfh()->OnMessageReceived(FrameHostMsg_EnterFullscreen(
      main_test_rfh()->GetRoutingID(), blink::WebFullscreenOptions()));
  ASSERT_TRUE(contents()->IsFullscreenForCurrentTab());

  base::Optional<ScreenOrientationLockResult> result;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result);
  // The lock request will be pending.
  EXPECT_FALSE(result.has_value());
  // Delegate did apply lock once.
  EXPECT_EQ(1, delegate.lock_count());
  EXPECT_EQ(0, delegate.unlock_count());

  // Simulates exiting full screen.
  main_test_rfh()->OnMessageReceived(
      FrameHostMsg_ExitFullscreen(main_test_rfh()->GetRoutingID()));
  ASSERT_FALSE(contents()->IsFullscreenForCurrentTab());
  // The pending lock request is cancelled.
  EXPECT_EQ(ScreenOrientationLockResult::
                SCREEN_ORIENTATION_LOCK_RESULT_ERROR_CANCELED,
            result);
  // Delegate did apply unlock once.
  EXPECT_EQ(1, delegate.unlock_count());
}

// If lock already applied once, then unlock should be triggered once
// automatically when navigating to other web page, and previous pending lock
// request will be cancelled.
TEST_F(ScreenOrientationProviderTest, UnlockWhenNavigation) {
  FakeScreenOrientationDelegate delegate(true, false);

  // Navigate to a site.
  const GURL url("http://www.google.com");
  controller().LoadURL(url, Referrer(), ui::PAGE_TRANSITION_TYPED,
                       std::string());

  base::Optional<ScreenOrientationLockResult> result;
  CallLockAndGetResult(blink::WebScreenOrientationLockType::
                           kWebScreenOrientationLockLandscapeSecondary,
                       &result);
  // The lock request will be pending.
  EXPECT_FALSE(result.has_value());
  // Delegate did apply lock once.
  EXPECT_EQ(1, delegate.lock_count());
  EXPECT_EQ(0, delegate.unlock_count());

  // Navigate to another site.
  const GURL another_url("http://www.google.com/abc.html");
  contents()->NavigateAndCommit(another_url);
  // The pending lock request is cancelled.
  EXPECT_EQ(ScreenOrientationLockResult::
                SCREEN_ORIENTATION_LOCK_RESULT_ERROR_CANCELED,
            result);
  // Delegate did apply unlock once.
  EXPECT_EQ(1, delegate.unlock_count());
}

}  // namespace content
