// 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.

#include "ui/native_theme/native_theme_fluent.h"

#include <algorithm>
#include <array>
#include <cmath>
#include <memory>
#include <string_view>
#include <utility>

#include "base/strings/strcat.h"
#include "cc/paint/paint_op.h"
#include "cc/paint/paint_record.h"
#include "cc/paint/record_paint_canvas.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_utils.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/native_theme/native_theme.h"

namespace ui {

class NativeThemeFluentTest : public ::testing::Test,
                              public ::testing::WithParamInterface<float> {
 protected:
  const NativeThemeFluent& theme() const { return theme_; }

  // Mocks the availability of the font for drawing arrow icons.
  void SetArrowIconsAvailable(bool available) {
    theme_.SetArrowIconsAvailableForTesting(available);
    EXPECT_EQ(theme().GetArrowIconsAvailable(), available);
  }

  void VerifyArrowRect() const {
    SCOPED_TRACE(::testing::Message() << "Scale: " << ScaleFromDIP());
    for (const auto& parts_elem :
         std::to_array<std::pair<NativeTheme::Part, std::string_view>>(
             {{NativeTheme::kScrollbarDownArrow, "down"},
              {NativeTheme::kScrollbarLeftArrow, "left"},
              {NativeTheme::kScrollbarRightArrow, "right"},
              {NativeTheme::kScrollbarUpArrow, "up"}})) {
      const NativeTheme::Part part = parts_elem.first;
      SCOPED_TRACE(base::StrCat({"Arrow direction: ", parts_elem.second}));
      const gfx::RectF button_rect = ButtonRect(part);
      for (const auto& states_elem :
           std::to_array<std::pair<NativeTheme::State, std::string_view>>(
               {{NativeTheme::kDisabled, "disabled"},
                {NativeTheme::kHovered, "hovered"},
                {NativeTheme::kNormal, "normal"},
                {NativeTheme::kPressed, "pressed"}})) {
        const NativeTheme::State state = states_elem.first;
        SCOPED_TRACE(base::StrCat({"Arrow state: ", states_elem.second}));
        const gfx::RectF arrow_rect =
            theme().GetArrowRect(gfx::ToNearestRect(button_rect), part, state);
        VerifyArrowRectCommonDimensions(arrow_rect);
        if (!theme().GetArrowIconsAvailable()) {
          VerifyArrowRectIsIntRect(arrow_rect);
        }
        VerifyArrowRectIsCentered(button_rect, arrow_rect, part);
        VerifyArrowRectLengthRatio(button_rect, arrow_rect, state);
      }
    }
  }

  void PaintScrollbarThumb(cc::PaintCanvas* canvas) const {
    ColorProvider color_provider;
    theme_.PaintScrollbarThumb(canvas, &color_provider,
                               NativeTheme::kScrollbarVerticalThumb,
                               NativeTheme::kNormal, gfx::Rect(15, 100), {}
                            #if BUILDFLAG(ARKWEB_UNITTESTS)
                               , SK_ColorLTGRAY
                            #endif
                              );
  }

 private:
  static float ScaleFromDIP() { return GetParam(); }

  static void VerifyArrowRectCommonDimensions(const gfx::RectF& arrow_rect) {
    EXPECT_FALSE(arrow_rect.IsEmpty());
    EXPECT_EQ(arrow_rect.width(), arrow_rect.height());
    EXPECT_EQ(arrow_rect.width(), std::floor(arrow_rect.width()));
  }

  void VerifyArrowRectIsCentered(const gfx::RectF& button_rect,
                                 const gfx::RectF& arrow_rect,
                                 NativeTheme::Part part) const {
    const gfx::PointF button_center = button_rect.CenterPoint();
    const gfx::PointF arrow_center = arrow_rect.CenterPoint();
    // The arrow is shifted away from center along the length axis by one dp,
    // rounded to integral px.
    float expected_shift = std::round(ScaleFromDIP());
    if (!theme().GetArrowIconsAvailable()) {
      // For triangular arrows, rect coordinates are snapped to integers, which
      // may introduce an additional half pixel shift.
      expected_shift += 0.5f;
    }
    if (part == NativeTheme::kScrollbarUpArrow ||
        part == NativeTheme::kScrollbarDownArrow) {
      EXPECT_EQ(button_center.x(), arrow_center.x());
      EXPECT_NEAR(button_center.y(), arrow_center.y(), expected_shift);
    } else {
      EXPECT_NEAR(button_center.x(), arrow_center.x(), expected_shift);
      EXPECT_EQ(button_center.y(), arrow_center.y());
    }
  }

  static void VerifyArrowRectIsIntRect(const gfx::RectF& arrow_rect) {
    EXPECT_TRUE(gfx::IsNearestRectWithinDistance(arrow_rect, 0.01f));
  }

  static void VerifyArrowRectLengthRatio(const gfx::RectF& button_rect,
                                         const gfx::RectF& arrow_rect,
                                         NativeTheme::State state) {
    const float thickness = std::min(button_rect.width(), button_rect.height());
    const float arrow_side = arrow_rect.width();  // The arrow is square.
    if (state == NativeTheme::kPressed) {
      // Pressed icons are ~0.5 times as thick as the button (precise value
      // depends on zoom and whether arrow icons are available).
      EXPECT_GT(arrow_side, thickness / 3.0f);
      EXPECT_LT(arrow_side, thickness / 1.5f);
    } else {
      // Non-pressed arrows are ~0.6 times as thick as the button.
      EXPECT_GT(arrow_side, thickness / 2.0f);
      EXPECT_LT(arrow_side, thickness);
    }
  }

  gfx::RectF ButtonRect(NativeTheme::Part part) const {
    gfx::Rect rect({}, theme().GetVerticalScrollbarButtonSize());
    if (part == NativeTheme::kScrollbarLeftArrow ||
        part == NativeTheme::kScrollbarRightArrow) {
      rect.Transpose();
    }
    return gfx::RectF(gfx::ScaleToEnclosedRect(rect, ScaleFromDIP()));
  }

 private:
  NativeThemeFluent theme_;
};

// Verify the dimensions of an arrow rect with triangular arrows for a given
// button rect depending on the arrow direction and state.
TEST_P(NativeThemeFluentTest, VerifyArrowRectWithTriangularArrows) {
  SetArrowIconsAvailable(false);
  VerifyArrowRect();
}

// Verify the dimensions of an arrow rect with arrow icons for a given button
// rect depending on the arrow direction and state.
TEST_P(NativeThemeFluentTest, VerifyArrowRectWithArrowIcons) {
  SetArrowIconsAvailable(true);
  VerifyArrowRect();
}

// Verify that the thumb paint function draws a round rectangle. Generally,
// `NativeThemeFluent::Paint*()` functions are covered by Blink's web tests; but
// in web tests we render the thumbs as squares instead of pill-shaped. This
// test ensures we don't lose coverage on the PaintOp called to draw the thumb.
TEST_F(NativeThemeFluentTest, PaintThumbRoundedCorners) {
  cc::RecordPaintCanvas canvas;
  PaintScrollbarThumb(&canvas);
  EXPECT_EQ(canvas.TotalOpCount(), 1u);
  EXPECT_EQ(canvas.ReleaseAsRecord().GetFirstOp().GetType(),
            cc::PaintOpType::kDrawRRect);
}

// Verify that GetThumbColor returns the correct color given the scrollbar state
// and extra params.
TEST_F(NativeThemeFluentTest, GetThumbColor) {
  const std::unique_ptr<ColorProvider> color_provider =
      CreateDefaultColorProviderForBlink(/*dark_mode=*/false);

  // When there are no extra params set, the colors should be the ones that
  // correspond to the ColorId.
  EXPECT_EQ(color_provider->GetColor(kColorWebNativeControlScrollbarThumb),
            theme().GetScrollbarThumbColor(color_provider.get(),
                                           NativeTheme::kNormal, {}));
  const auto hovered_thumb_color =
      color_provider->GetColor(kColorWebNativeControlScrollbarThumbHovered);
  EXPECT_EQ(hovered_thumb_color,
            theme().GetScrollbarThumbColor(color_provider.get(),
                                           NativeTheme::kHovered, {}));
  const auto pressed_thumb_color =
      color_provider->GetColor(kColorWebNativeControlScrollbarThumbPressed);
  EXPECT_EQ(pressed_thumb_color,
            theme().GetScrollbarThumbColor(color_provider.get(),
                                           NativeTheme::kPressed, {}));

  // When the thumb is being painted in minimal mode, the normal state should
  // return the minimal mode's transparent color while the other states remain
  // unaffected.
  static constexpr NativeTheme::ScrollbarThumbExtraParams kMinimalParams = {
      .is_thumb_minimal_mode = true};
  EXPECT_EQ(color_provider->GetColor(
                kColorWebNativeControlScrollbarThumbOverlayMinimalMode),
            theme().GetScrollbarThumbColor(
                color_provider.get(), NativeTheme::kNormal, kMinimalParams));
  EXPECT_EQ(hovered_thumb_color,
            theme().GetScrollbarThumbColor(
                color_provider.get(), NativeTheme::kHovered, kMinimalParams));
  EXPECT_EQ(pressed_thumb_color,
            theme().GetScrollbarThumbColor(
                color_provider.get(), NativeTheme::kPressed, kMinimalParams));

  // When there is a css color set in the extra params, we modify the color
  // when it is hovered or pressed to signal the change in state.
  static constexpr auto kCssColor = SK_ColorGREEN;
  static constexpr NativeTheme::ScrollbarThumbExtraParams kColorParams = {
      .thumb_color = kCssColor};
  EXPECT_EQ(kCssColor,
            theme().GetScrollbarThumbColor(color_provider.get(),
                                           NativeTheme::kNormal, kColorParams));
  EXPECT_NE(kCssColor,
            theme().GetScrollbarThumbColor(
                color_provider.get(), NativeTheme::kHovered, kColorParams));
  EXPECT_NE(kCssColor,
            theme().GetScrollbarThumbColor(
                color_provider.get(), NativeTheme::kPressed, kColorParams));
}

INSTANTIATE_TEST_SUITE_P(All,
                         NativeThemeFluentTest,
                         ::testing::Values(1.0f, 1.25f, 1.5f, 1.75f, 2.0f));

}  // namespace ui