// 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 "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/display_cutout/display_cutout_constants.h"
#include "content/browser/frame_host/frame_tree_node.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_frame_navigation_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/content_browser_test_utils_internal.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/associated_interfaces/associated_interface_provider.h"
#include "third_party/blink/public/mojom/page/display_cutout.mojom.h"

namespace content {

namespace {

#if defined(OS_ANDROID)

// These inset and flags simulate when we are not extending into the cutout.
const gfx::Insets kNoCutoutInsets = gfx::Insets();
const int kNoCutoutInsetsExpectedFlags = DisplayCutoutSafeArea::kEmpty;

// These inset and flags simulate when the we are extending into the cutout.
const gfx::Insets kCutoutInsets = gfx::Insets(1, 0, 1, 0);
const int kCutoutInsetsExpectedFlags =
    DisplayCutoutSafeArea::kTop | DisplayCutoutSafeArea::kBottom;

// These inset and flags simulate when we are extending into the cutout and have
// rotated the device so that the cutout is on the other sides.
const gfx::Insets kRotatedCutoutInsets = gfx::Insets(0, 1, 0, 1);
const int kRotatedCutoutInsetsExpectedFlags =
    DisplayCutoutSafeArea::kLeft | DisplayCutoutSafeArea::kRight;

#endif

class TestWebContentsObserver : public WebContentsObserver {
 public:
  explicit TestWebContentsObserver(content::WebContents* web_contents)
      : WebContentsObserver(web_contents) {}

  // WebContentsObserver override.
  void ViewportFitChanged(blink::mojom::ViewportFit value) override {
    value_ = value;

    if (value_ == wanted_value_)
      run_loop_.Quit();
  }

  bool has_value() const { return value_.has_value(); }

  void WaitForWantedValue(blink::mojom::ViewportFit wanted_value) {
    if (value_.has_value()) {
      EXPECT_EQ(wanted_value, value_);
      return;
    }

    wanted_value_ = wanted_value;
    run_loop_.Run();
  }

 private:
  base::RunLoop run_loop_;
  base::Optional<blink::mojom::ViewportFit> value_;
  blink::mojom::ViewportFit wanted_value_ = blink::mojom::ViewportFit::kAuto;

  DISALLOW_COPY_AND_ASSIGN(TestWebContentsObserver);
};

// Used for forcing a specific |blink::WebDisplayMode| during a test.
class DisplayCutoutWebContentsDelegate : public WebContentsDelegate {
 public:
  blink::WebDisplayMode GetDisplayMode(
      const WebContents* web_contents) const override {
    return display_mode_;
  }

  void SetDisplayMode(blink::WebDisplayMode display_mode) {
    display_mode_ = display_mode;
  }

 private:
  blink::WebDisplayMode display_mode_ =
      blink::WebDisplayMode::kWebDisplayModeBrowser;
};

const char kTestHTML[] =
    "<!DOCTYPE html>"
    "<style>"
    "  #target {"
    "    margin-top: env(safe-area-inset-top);"
    "    margin-left: env(safe-area-inset-left);"
    "    margin-bottom: env(safe-area-inset-bottom);"
    "    margin-right: env(safe-area-inset-right);"
    "  }"
    "</style>"
    "<div id=target></div>";

}  // namespace

class DisplayCutoutBrowserTest : public ContentBrowserTest {
 public:
  DisplayCutoutBrowserTest() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitchASCII("enable-blink-features",
                                    "DisplayCutoutAPI");
  }

  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());

    embedded_test_server()->ServeFilesFromDirectory(temp_dir_.GetPath());
    ASSERT_TRUE(embedded_test_server()->Start());

    ContentBrowserTest::SetUp();
  }

  void LoadTestPageWithViewportFitFromMeta(const std::string& value) {
    LoadTestPageWithData(
        "<!DOCTYPE html>"
        "<meta name='viewport' content='viewport-fit=" +
        value + "'><iframe></iframe>");
  }

  void LoadSubFrameWithViewportFitMetaValue(const std::string& value) {
    const std::string data =
        "data:text/html;charset=utf-8,<!DOCTYPE html>"
        "<meta name='viewport' content='viewport-fit=" +
        value + "'>";

    FrameTreeNode* root = web_contents_impl()->GetFrameTree()->root();
    FrameTreeNode* child = root->child_at(0);

    ASSERT_TRUE(NavigateToURLFromRenderer(child, GURL(data)));
    web_contents_impl()->Focus();
  }

  bool ClearViewportFitTag() {
    return ExecuteScript(
        web_contents_impl(),
        "document.getElementsByTagName('meta')[0].content = ''");
  }

  void SendSafeAreaToFrame(int top, int left, int bottom, int right) {
    blink::mojom::DisplayCutoutClientAssociatedPtr client;
    MainFrame()->GetRemoteAssociatedInterfaces()->GetInterface(&client);
    client->SetSafeArea(
        blink::mojom::DisplayCutoutSafeArea::New(top, left, bottom, right));
  }

  std::string GetCurrentSafeAreaValue(const std::string& name) {
    std::string value;
    EXPECT_TRUE(ExecuteScriptAndExtractString(
        MainFrame(),
        "(() => {"
        "const e = document.getElementById('target');"
        "const style = window.getComputedStyle(e, null);"
        "window.domAutomationController.send("
        "  style.getPropertyValue('margin-" +
            name +
            "'));"
            "})();",
        &value));
    return value;
  }

  void LoadTestPageWithData(const std::string& data) {
    // Write |data| to a temporary file that can be later reached at
    // http://127.0.0.1/test_file_*.html.
    static int s_test_file_number = 1;
    base::FilePath file_path = temp_dir_.GetPath().AppendASCII(
        base::StringPrintf("test_file_%d.html", s_test_file_number++));
    {
      base::ScopedAllowBlockingForTesting allow_temp_file_writing;
      ASSERT_EQ(static_cast<int>(data.length()),
                base::WriteFile(file_path, data.c_str(), data.length()));
    }
    GURL url = embedded_test_server()->GetURL(
        "/" + file_path.BaseName().AsUTF8Unsafe());

    // Reset UKM and navigate to the html file created above.
    ResetUKM();
    ASSERT_TRUE(NavigateToURL(shell(), url));
  }

  void SimulateFullscreenStateChanged(RenderFrameHost* frame,
                                      bool is_fullscreen) {
    web_contents_impl()->FullscreenStateChanged(frame, is_fullscreen);
  }

  void SimulateFullscreenExit() {
    web_contents_impl()->ExitFullscreenMode(true);
  }

  RenderFrameHost* MainFrame() { return web_contents_impl()->GetMainFrame(); }

  RenderFrameHost* ChildFrame() {
    FrameTreeNode* root = web_contents_impl()->GetFrameTree()->root();
    return root->child_at(0)->current_frame_host();
  }

  WebContentsImpl* web_contents_impl() {
    return static_cast<WebContentsImpl*>(shell()->web_contents());
  }

  unsigned GetUKMEntryCount() const {
    using Entry = ukm::builders::Layout_DisplayCutout_StateChanged;
    auto ukm_entries = test_ukm_recorder_->GetEntriesByName(Entry::kEntryName);
    return ukm_entries.size();
  }

  void ExpectUKMEntry(int index,
                      ukm::SourceId source_id,
                      bool is_main_frame,
                      blink::mojom::ViewportFit applied_value,
                      blink::mojom::ViewportFit supplied_value,
                      int ignored_reason,
                      int safe_areas_present) {
    using Entry = ukm::builders::Layout_DisplayCutout_StateChanged;
    auto ukm_entries = test_ukm_recorder_->GetEntriesByName(Entry::kEntryName);

    EXPECT_EQ(source_id, ukm_entries[index]->source_id);
    EXPECT_EQ(is_main_frame, *test_ukm_recorder_->GetEntryMetric(
                                 ukm_entries[index], Entry::kIsMainFrameName));
    EXPECT_EQ(static_cast<int>(applied_value),
              *test_ukm_recorder_->GetEntryMetric(
                  ukm_entries[index], Entry::kViewportFit_AppliedName));
    EXPECT_EQ(static_cast<int>(supplied_value),
              *test_ukm_recorder_->GetEntryMetric(
                  ukm_entries[index], Entry::kViewportFit_SuppliedName));
    EXPECT_EQ(ignored_reason,
              *test_ukm_recorder_->GetEntryMetric(
                  ukm_entries[index], Entry::kViewportFit_IgnoredReasonName));
    EXPECT_EQ(safe_areas_present,
              *test_ukm_recorder_->GetEntryMetric(
                  ukm_entries[index], Entry::kSafeAreasPresentName));
  }

 private:
  void ResetUKM() {
    test_ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
  }

  base::ScopedTempDir temp_dir_;
  std::unique_ptr<ukm::TestUkmRecorder> test_ukm_recorder_;

  DISALLOW_COPY_AND_ASSIGN(DisplayCutoutBrowserTest);
};

// The viewport meta tag is only enabled on Android.
#if defined(OS_ANDROID)

IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, ViewportFit_Fullscreen) {
  LoadTestPageWithViewportFitFromMeta("cover");
  LoadSubFrameWithViewportFitMetaValue("contain");

  {
    TestWebContentsObserver observer(web_contents_impl());
    SimulateFullscreenStateChanged(MainFrame(), true);
    observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
    web_contents_impl()->SetDisplayCutoutSafeArea(kCutoutInsets);
  }

  {
    TestWebContentsObserver observer(web_contents_impl());
    SimulateFullscreenStateChanged(ChildFrame(), true);
    observer.WaitForWantedValue(blink::mojom::ViewportFit::kContain);
    web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
  }

  {
    TestWebContentsObserver observer(web_contents_impl());
    SimulateFullscreenStateChanged(ChildFrame(), false);
    observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);

    // This simulates the user rotating the device.
    web_contents_impl()->SetDisplayCutoutSafeArea(kCutoutInsets);
    web_contents_impl()->SetDisplayCutoutSafeArea(kRotatedCutoutInsets);
  }

  {
    TestWebContentsObserver observer(web_contents_impl());
    SimulateFullscreenStateChanged(MainFrame(), false);
    SimulateFullscreenExit();
    observer.WaitForWantedValue(blink::mojom::ViewportFit::kAuto);
    web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
  }

  // Get the source id for the page and close the |shell|. This will flush any
  // unrecorded UKM metrics.
  ukm::SourceId source_id =
      web_contents_impl()->GetUkmSourceIdForLastCommittedSource();
  shell()->Close();

  // Check UKM metrics are recorded. The first two entries are from loading the
  // frame and the subframe with a viewport fit attribute.
  EXPECT_EQ(5u, GetUKMEntryCount());
  ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
                 blink::mojom::ViewportFit::kCover,
                 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
                 kNoCutoutInsetsExpectedFlags);
  ExpectUKMEntry(1, source_id, false, blink::mojom::ViewportFit::kAuto,
                 blink::mojom::ViewportFit::kContain,
                 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
                 kNoCutoutInsetsExpectedFlags);

  // This is when we take the main frame fullscreen.
  ExpectUKMEntry(2, source_id, true, blink::mojom::ViewportFit::kCover,
                 blink::mojom::ViewportFit::kCover,
                 DisplayCutoutIgnoredReason::kAllowed,
                 kCutoutInsetsExpectedFlags);

  // This is when we take the subframe fullscreen.
  ExpectUKMEntry(3, source_id, false, blink::mojom::ViewportFit::kContain,
                 blink::mojom::ViewportFit::kContain,
                 DisplayCutoutIgnoredReason::kAllowed,
                 kNoCutoutInsetsExpectedFlags);

  // These is when the subframe exits fullscreen.
  ExpectUKMEntry(4, source_id, true, blink::mojom::ViewportFit::kCover,
                 blink::mojom::ViewportFit::kCover,
                 DisplayCutoutIgnoredReason::kAllowed,
                 kRotatedCutoutInsetsExpectedFlags);
}

IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,
                       ViewportFit_Fullscreen_Update) {
  LoadTestPageWithViewportFitFromMeta("cover");

  {
    TestWebContentsObserver observer(web_contents_impl());
    SimulateFullscreenStateChanged(MainFrame(), true);
    observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
    web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
  }

  {
    TestWebContentsObserver observer(web_contents_impl());
    EXPECT_TRUE(ClearViewportFitTag());
    observer.WaitForWantedValue(blink::mojom::ViewportFit::kAuto);
    web_contents_impl()->SetDisplayCutoutSafeArea(kNoCutoutInsets);
  }

  // Get the source id for the page and close the |shell|. This will flush any
  // unrecorded UKM metrics.
  ukm::SourceId source_id =
      web_contents_impl()->GetUkmSourceIdForLastCommittedSource();
  shell()->Close();

  // Check UKM metrics are recorded.
  EXPECT_EQ(2u, GetUKMEntryCount());
  ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
                 blink::mojom::ViewportFit::kCover,
                 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
                 kNoCutoutInsetsExpectedFlags);
  ExpectUKMEntry(1, source_id, true, blink::mojom::ViewportFit::kCover,
                 blink::mojom::ViewportFit::kCover,
                 DisplayCutoutIgnoredReason::kAllowed,
                 kNoCutoutInsetsExpectedFlags);
}

IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, ViewportFit_Noop_Navigate) {
  {
    TestWebContentsObserver observer(web_contents_impl());
    LoadTestPageWithViewportFitFromMeta("cover");
    EXPECT_FALSE(observer.has_value());
  }

  ukm::SourceId source_id =
      web_contents_impl()->GetUkmSourceIdForLastCommittedSource();
  LoadTestPageWithData("");

  // Check UKM metrics are recorded.
  EXPECT_EQ(1u, GetUKMEntryCount());
  ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
                 blink::mojom::ViewportFit::kCover,
                 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
                 kNoCutoutInsetsExpectedFlags);
}

IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest,
                       ViewportFit_Noop_WebContentsDestroyed) {
  {
    TestWebContentsObserver observer(web_contents_impl());
    LoadTestPageWithViewportFitFromMeta("cover");
    EXPECT_FALSE(observer.has_value());
  }

  ukm::SourceId source_id =
      web_contents_impl()->GetUkmSourceIdForLastCommittedSource();
  shell()->Close();

  // Check UKM metrics are recorded.
  EXPECT_EQ(1u, GetUKMEntryCount());
  ExpectUKMEntry(0, source_id, true, blink::mojom::ViewportFit::kAuto,
                 blink::mojom::ViewportFit::kCover,
                 DisplayCutoutIgnoredReason::kWebContentsNotFullscreen,
                 kNoCutoutInsetsExpectedFlags);
}

IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode) {
  // Inject the custom delegate used for this test.
  std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
      new DisplayCutoutWebContentsDelegate());
  web_contents_impl()->SetDelegate(delegate.get());
  EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());

  {
    TestWebContentsObserver observer(web_contents_impl());
    LoadTestPageWithViewportFitFromMeta("cover");
    EXPECT_FALSE(observer.has_value());
  }
}

IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode_Fullscreen) {
  // Inject the custom delegate used for this test.
  std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
      new DisplayCutoutWebContentsDelegate());
  delegate->SetDisplayMode(blink::WebDisplayMode::kWebDisplayModeFullscreen);
  web_contents_impl()->SetDelegate(delegate.get());
  EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());

  {
    TestWebContentsObserver observer(web_contents_impl());
    LoadTestPageWithViewportFitFromMeta("cover");
    observer.WaitForWantedValue(blink::mojom::ViewportFit::kCover);
  }
}

IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, WebDisplayMode_Standalone) {
  // Inject the custom delegate used for this test.
  std::unique_ptr<DisplayCutoutWebContentsDelegate> delegate(
      new DisplayCutoutWebContentsDelegate());
  delegate->SetDisplayMode(blink::WebDisplayMode::kWebDisplayModeStandalone);
  web_contents_impl()->SetDelegate(delegate.get());
  EXPECT_EQ(delegate.get(), web_contents_impl()->GetDelegate());

  {
    TestWebContentsObserver observer(web_contents_impl());
    LoadTestPageWithViewportFitFromMeta("cover");
    EXPECT_FALSE(observer.has_value());
  }
}

#endif

IN_PROC_BROWSER_TEST_F(DisplayCutoutBrowserTest, PublishSafeAreaVariables) {
  LoadTestPageWithData(kTestHTML);

  // Make sure all the safe areas are currently zero.
  EXPECT_EQ("0px", GetCurrentSafeAreaValue("top"));
  EXPECT_EQ("0px", GetCurrentSafeAreaValue("left"));
  EXPECT_EQ("0px", GetCurrentSafeAreaValue("bottom"));
  EXPECT_EQ("0px", GetCurrentSafeAreaValue("right"));

  SendSafeAreaToFrame(1, 2, 3, 4);

  // Make sure all the safe ares are correctly set.
  EXPECT_EQ("1px", GetCurrentSafeAreaValue("top"));
  EXPECT_EQ("2px", GetCurrentSafeAreaValue("left"));
  EXPECT_EQ("3px", GetCurrentSafeAreaValue("bottom"));
  EXPECT_EQ("4px", GetCurrentSafeAreaValue("right"));
}

}  //  namespace content
