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

#include "chrome/browser/ui/views/accessibility/accessibility_focus_highlight.h"

#include <math.h>

#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "cc/test/pixel_comparator.h"
#include "cc/test/pixel_test_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/prefs/pref_service.h"
#include "components/viz/common/frame_sinks/copy_output_request.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/public/browser/focused_node_details.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/focus_changed_observer.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/image/image.h"
#include "ui/snapshot/snapshot.h"
#include "ui/views/widget/widget.h"

#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif

// To rebaseline this test on all platforms:
// 1. Run a CQ+1 dry run.
// 2. Click the failing bots for android, windows, mac, and linux.
// 3. Find the failing browser_tests step.
// 4. Click the "Deterministic failure" link for the failing test case.
// 5. Copy the "Actual pixels" data url and paste into browser.
// 6. Save the image into your chromium checkout in
//    chrome/test/data/accessibility.

class AccessibilityFocusHighlightBrowserTest : public InProcessBrowserTest {
 public:
  AccessibilityFocusHighlightBrowserTest() = default;
  ~AccessibilityFocusHighlightBrowserTest() override = default;
  AccessibilityFocusHighlightBrowserTest(
      const AccessibilityFocusHighlightBrowserTest&) = delete;
  AccessibilityFocusHighlightBrowserTest& operator=(
      const AccessibilityFocusHighlightBrowserTest&) = delete;

  // InProcessBrowserTest overrides:
  void SetUp() override {
    EnablePixelOutput();
    InProcessBrowserTest::SetUp();
  }

  bool ColorsApproximatelyEqual(SkColor color1, SkColor color2) {
    return abs(static_cast<int>(SkColorGetR(color1)) -
               static_cast<int>(SkColorGetR(color2))) < 50 &&
           abs(static_cast<int>(SkColorGetG(color1)) -
               static_cast<int>(SkColorGetG(color2))) < 50 &&
           abs(static_cast<int>(SkColorGetB(color1)) -
               static_cast<int>(SkColorGetB(color2))) < 50;
  }

  float CountPercentPixelsWithColor(const gfx::Image& image, SkColor color) {
    SkBitmap bitmap = image.AsBitmap();
    int count = 0;
    for (int x = 0; x < bitmap.width(); ++x) {
      for (int y = 0; y < bitmap.height(); ++y) {
        if (ColorsApproximatelyEqual(color, bitmap.getColor(x, y))) {
          count++;
        }
      }
    }
    return count * 100.0f / (bitmap.width() * bitmap.height());
  }

  gfx::Image CaptureWindowContents() {
    BrowserView* browser_view =
        BrowserView::GetBrowserViewForBrowser(browser());
    views::Widget* widget = browser_view->GetWidget();
    gfx::Rect bounds = widget->GetWindowBoundsInScreen();
    bounds.Offset(-bounds.OffsetFromOrigin());
    gfx::NativeView native_view = widget->GetNativeView();

    // Keep trying until we get a successful capture.
    while (true) {
      base::test::TestFuture<gfx::Image> future;
      ui::GrabViewSnapshot(native_view, bounds, future.GetCallback());
      gfx::Image result_image = future.Take();

      if (result_image.Size().IsEmpty()) {
        LOG(INFO) << "Bitmap not correct size, trying to capture again";
        continue;
      }

      // Skip through every 16th pixel (just for speed, no need to check
      // every single one). If we find at least one opaque pixel then we
      // assume we got a valid image. If the capture fails we sometimes get
      // an all transparent image, but when it succeeds there can be a
      // transparent edge.
      bool found_opaque_pixel = false;
      for (int x = 0; x < bounds.width() && !found_opaque_pixel; x += 16) {
        for (int y = 0; y < bounds.height() && !found_opaque_pixel; y += 16) {
          if (SkColorGetA(result_image.AsBitmap().getColor(x, y)) ==
              SK_AlphaOPAQUE) {
            found_opaque_pixel = true;
          }
        }
      }
      if (!found_opaque_pixel) {
        LOG(INFO) << "Bitmap not opaque, trying to capture again";
        continue;
      }

      return result_image;
    }
  }
};

// Smoke test that ensures that when a node gets focus, the layer with the
// focus highlight actually gets drawn.
//
// Flaky on all platforms. TODO(crbug.com/40692704): Enable this test.
IN_PROC_BROWSER_TEST_F(AccessibilityFocusHighlightBrowserTest,
                       DISABLED_DrawsHighlight) {
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), GURL("data:text/html,"
                      "<body style='background-color: rgb(204, 255, 255);'>"
                      "<div tabindex=0 id='div'>Focusable div</div>")));

  AccessibilityFocusHighlight::SetNoFadeForTesting();
  AccessibilityFocusHighlight::SkipActivationCheckForTesting();
  AccessibilityFocusHighlight::UseDefaultColorForTesting();

  browser()->GetProfile()->GetPrefs()->SetBoolean(
      prefs::kAccessibilityFocusHighlightEnabled, true);

  // The web page has a background with a specific color. Keep looping until we
  // capture an image of the page that's more than 90% that color.
  gfx::Image image;
  do {
    base::RunLoop().RunUntilIdle();
    image = CaptureWindowContents();
  } while (CountPercentPixelsWithColor(image, SkColorSetRGB(204, 255, 255)) <
           90.0f);

  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  SkColor highlight_color =
      browser_view->GetColorProvider()->GetColor(kColorFocusHighlightDefault);

  // Initially less than 0.05% of the image should be the focus ring's highlight
  // color.
  ASSERT_LT(CountPercentPixelsWithColor(image, highlight_color), 0.05f);

  // Focus something.
  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  std::string script("document.getElementById('div').focus();");
  EXPECT_TRUE(content::ExecJs(web_contents, script));

  // Now wait until at least 0.1% of the image has the focus ring's highlight
  // color. If it never does, the test will time out.
  do {
    base::RunLoop().RunUntilIdle();
    image = CaptureWindowContents();
  } while (CountPercentPixelsWithColor(image, highlight_color) < 0.1f);
}

IN_PROC_BROWSER_TEST_F(AccessibilityFocusHighlightBrowserTest,
                       FocusBoundsIncludeImages) {
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), GURL("data:text/html,"
                      "<a id='link' href=''>"
                      "<img id='image' width='220' height='147' "
                      "style='vertical-align: middle;'>"
                      "</a>")));

  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  content::FocusChangedObserver observer(web_contents);
  std::string script("document.getElementById('link').focus();");
  ASSERT_TRUE(content::ExecJs(web_contents, script));
  auto details = observer.Wait();

  gfx::Rect bounds = details.node_bounds_in_screen;
  EXPECT_EQ(220, bounds.width());

  // Not sure where the extra px of height are coming from...
  EXPECT_EQ(147, bounds.height());
}

class ReadbackHolder : public base::RefCountedThreadSafe<ReadbackHolder> {
 public:
  ReadbackHolder() : run_loop_(std::make_unique<base::RunLoop>()) {}

  void OutputRequestCallback(std::unique_ptr<viz::CopyOutputResult> result) {
    if (result->IsEmpty()) {
      result_.reset();
    } else {
      auto scoped_sk_bitmap = result->ScopedAccessSkBitmap();
      result_ =
          std::make_unique<SkBitmap>(scoped_sk_bitmap.GetOutScopedBitmap());
      EXPECT_TRUE(result_->readyToDraw());
    }
    run_loop_->Quit();
  }

  void WaitForReadback() { run_loop_->Run(); }

  const SkBitmap& result() const { return *result_; }

 private:
  friend class base::RefCountedThreadSafe<ReadbackHolder>;

  virtual ~ReadbackHolder() = default;

  std::unique_ptr<SkBitmap> result_;
  std::unique_ptr<base::RunLoop> run_loop_;
};

const cc::ExactPixelComparator pixel_comparator;

// TODO(crbug.com/40924319): Fix flaky test on Mac.
// TODO(crbug.com/373535999): Fix flaky test on Windows.
// TODO(crbug.com/446071321): Fix flaky test on Linux.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
#define MAYBE_FocusAppearance DISABLED_FocusAppearance
#else
#define MAYBE_FocusAppearance FocusAppearance
#endif

IN_PROC_BROWSER_TEST_F(AccessibilityFocusHighlightBrowserTest,
                       MAYBE_FocusAppearance) {
  base::ScopedAllowBlockingForTesting allow_blocking;

  AccessibilityFocusHighlight::SetNoFadeForTesting();
  AccessibilityFocusHighlight::SkipActivationCheckForTesting();

  browser()->GetProfile()->GetPrefs()->SetBoolean(
      prefs::kAccessibilityFocusHighlightEnabled, true);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(),
                                           GURL("data:text/html,"
                                                "<a id='link' href=''>"
                                                "<img width='10' height='10'>"
                                                "</a>")));

  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  content::FocusChangedObserver observer(web_contents);
  std::string script("document.getElementById('link').focus();");
  ASSERT_TRUE(content::ExecJs(web_contents, script));
  observer.Wait();
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  AccessibilityFocusHighlight* highlight =
      browser_view->GetAccessibilityFocusHighlightForTesting();
  ui::Layer* layer = highlight->GetLayerForTesting();

  gfx::Rect source_rect = gfx::Rect(layer->size());
  scoped_refptr<ReadbackHolder> holder(new ReadbackHolder);
  std::unique_ptr<viz::CopyOutputRequest> request =
      std::make_unique<viz::CopyOutputRequest>(
          viz::CopyOutputRequest::ResultFormat::RGBA,
          viz::CopyOutputRequest::ResultDestination::kSystemMemory,
          base::BindOnce(&ReadbackHolder::OutputRequestCallback, holder));
  request->set_area(source_rect);
  layer->RequestCopyOfOutput(std::move(request));
  holder->WaitForReadback();
  SkBitmap bitmap(holder->result());
  ASSERT_FALSE(bitmap.drawsNothing());

  base::FilePath dir_test_data;
  ASSERT_TRUE(base::PathService::Get(chrome::DIR_TEST_DATA, &dir_test_data));

  std::string screenshot_filename = "focus_highlight_appearance";
  std::string platform_suffix;
#if BUILDFLAG(IS_MAC)
  platform_suffix = "_mac";
#elif BUILDFLAG(IS_WIN)
  platform_suffix = "_win";
#endif

  base::FilePath golden_filepath =
      dir_test_data.AppendASCII("accessibility")
          .AppendASCII(screenshot_filename + ".png");
  base::FilePath golden_filepath_platform =
      golden_filepath.InsertBeforeExtensionASCII(platform_suffix);
  if (base::PathExists(golden_filepath_platform)) {
    golden_filepath = golden_filepath_platform;
  }

  bool snapshot_matches =
      cc::MatchesPNGFile(bitmap, golden_filepath, pixel_comparator);
  EXPECT_TRUE(snapshot_matches);
}
