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

#include <array>
#include <cstdint>
#include <optional>
#include <queue>
#include <string>
#include <string_view>
#include <vector>

#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/logging.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/escape.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/fuzzing/geolocation_element_css_fuzzer_grammar.h"
#include "chrome/test/fuzzing/geolocation_element_css_fuzzer_grammar.pb.h"
#include "chrome/test/fuzzing/in_process_fuzzer.h"
#include "components/viz/common/frame_sinks/copy_output_result.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/libfuzzer/proto/lpm_interface.h"
#include "testing/libfuzzer/research/domatolpm/domatolpm.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/gfx/geometry/rect.h"

namespace {

// The amount of a pixel's color is allowed to diverge from the target color
// (calculated as Euclidean distance in the RGB space). This value was arrived
// at experimentally.
constexpr int kColorTolerance = 100;

// The amount of expected text/background islands that the element screenshot is
// expected to have. Since the text and its font is hard-set, this values should
// not vary.

// There are 14 text islands, 11 for every letter in "Use location", 1 for the
// "i" dot, and 2 for the element's location pin icon.
constexpr int kExpectedTextIslands = 14;

// There are 6 background islands, 1 for the large island around the text, 1 for
// the location pin icon, and 4 for every letter in "Use location" with a hole.
constexpr int kExpectedBgIslands = 6;

// Helper function to parse rgb(r, g, b) or rgba(r, g, b, a) strings
std::optional<SkColor> ParseRgbString(std::string_view color_str) {
  if (color_str.empty()) {
    return std::nullopt;
  }

  size_t start = color_str.find('(');
  size_t end = color_str.find(')');
  if (start == std::string::npos || end == std::string::npos || end <= start) {
    return std::nullopt;
  }

  std::string_view values_str = color_str.substr(start + 1, end - start - 1);

  std::vector<std::string_view> parts = base::SplitStringPiece(
      values_str, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);

  if (parts.size() < 3) {
    return std::nullopt;
  }

  std::vector<int> components;
  for (int i = 0; i < 3; ++i) {
    int component;
    if (!base::StringToInt(parts[i], &component) || component < 0 ||
        component > 255) {
      return std::nullopt;
    }
    components.push_back(component);
  }

  return SkColorSetRGB(components[0], components[1], components[2]);
}

// Helper function to check if colors are similar within a tolerance
bool AreColorsSimilar(SkColor c1, SkColor c2, int tolerance) {
  int r_diff =
      static_cast<int>(SkColorGetR(c1)) - static_cast<int>(SkColorGetR(c2));
  int g_diff =
      static_cast<int>(SkColorGetG(c1)) - static_cast<int>(SkColorGetG(c2));
  int b_diff =
      static_cast<int>(SkColorGetB(c1)) - static_cast<int>(SkColorGetB(c2));
  return (r_diff * r_diff + g_diff * g_diff + b_diff * b_diff) <=
         (tolerance * tolerance);
}

// Helper function to count color islands
int CountColorIslands(const SkBitmap& bitmap,
                      SkColor target_color,
                      int tolerance) {
  // Direction vectors for visiting neighbors.
  std::array<int, 8> di = {-1, -1, -1, 0, 0, 1, 1, 1};
  std::array<int, 8> dj = {-1, 0, 1, -1, 1, -1, 0, 1};

  int width = bitmap.width();
  int height = bitmap.height();
  std::vector<std::vector<bool>> visited(height,
                                         std::vector<bool>(width, false));
  int island_count = 0;

  for (int i = 0; i < height; ++i) {
    for (int j = 0; j < width; ++j) {
      if (visited[i][j]) {
        continue;
      }
      if (!AreColorsSimilar(bitmap.getColor(j, i), target_color, tolerance)) {
        visited[i][j] = true;
        continue;
      }

      std::queue<std::pair<int, int>> q;

      q.emplace(i, j);
      visited[i][j] = true;
      island_count++;

      while (!q.empty()) {
        std::pair<int, int> curr = q.front();
        q.pop();

        for (int k = 0; k < 8; ++k) {
          int next_i = curr.first + di[k];
          int next_j = curr.second + dj[k];

          if (next_i < 0 || next_i >= height || next_j < 0 || next_j >= width) {
            continue;
          }
          if (visited[next_i][next_j]) {
            continue;
          }

          visited[next_i][next_j] = true;
          if (!AreColorsSimilar(bitmap.getColor(next_j, next_i), target_color,
                                tolerance)) {
            continue;
          }
          q.emplace(next_i, next_j);
        }
      }
    }
  }
  return island_count;
}

}  // namespace

// This fuzzer uses DomatoLPM to generate CSS to style a geolocation element.
class GeolocationElementCssFuzzer : public InProcessFuzzer {
 public:
  using FuzzCase =
      domatolpm::generated::geolocation_element_css_fuzzer_grammar::fuzzcase;
  GeolocationElementCssFuzzer() = default;

  int Fuzz(const uint8_t* data, size_t size) override;

 private:
  void OnScreenshotCaptured(base::OnceClosure quit_closure,
                            SkColor text_color,
                            SkColor bg_color,
                            const content::CopyFromSurfaceResult& result);
};

DEFINE_CUSTOM_PROTO_MUTATOR_IMPL(true, GeolocationElementCssFuzzer::FuzzCase)
DEFINE_CUSTOM_PROTO_CROSSOVER_IMPL(true, GeolocationElementCssFuzzer::FuzzCase)
DEFINE_POST_PROCESS_PROTO_MUTATION_IMPL(GeolocationElementCssFuzzer::FuzzCase)

REGISTER_IN_PROCESS_FUZZER(GeolocationElementCssFuzzer)

int GeolocationElementCssFuzzer::Fuzz(const uint8_t* data, size_t size) {
  FuzzCase fuzz_case;
  if (!protobuf_mutator::libfuzzer::LoadProtoInput(false, data, size,
                                                   &fuzz_case)) {
    LOG(ERROR) << "Protobuf mutator failed to load proto input";
    return 0;
  }

  domatolpm::Context ctx;
  CHECK(domatolpm::geolocation_element_css_fuzzer_grammar::handle_fuzzer(
      &ctx, fuzz_case));
  std::string html_content(ctx.GetBuilder()->view());

  // See
  // docs/security/url_display_guidelines/url_display_guidelines.md#url-length
  const size_t kMaxUrlLength = 2 * 1024 * 1024;
  std::string url_string = "data:text/html;charset=utf-8,";
  const bool kUsePlus = false;
  url_string.append(base::EscapeQueryParamValue(html_content, kUsePlus));
  if (url_string.length() > kMaxUrlLength) {
    return 0;
  }

  if (!ui_test_utils::NavigateToURL(browser(), GURL(url_string))) {
    NOTREACHED() << "Failed to load page";
  }
  auto* web_contents = browser()->tab_strip_model()->GetActiveWebContents();
  if (!web_contents) {
    NOTREACHED() << "WebContents is null after navigation";
  }
  if (!content::WaitForLoadStop(web_contents)) {
    NOTREACHED() << "Failed to wait for page load stop";
  }

  auto* rfh = web_contents->GetPrimaryMainFrame();
  if (!rfh || !rfh->IsRenderFrameLive()) {
    NOTREACHED() << "RenderFrameHost is not live after navigation";
  }

  // Get element bounds and styles, after the element is valid.
  std::string script = R"(waitForElementAndGetData();)";
  content::EvalJsResult result = content::EvalJs(rfh, script);
  if (!result.is_ok()) {
    NOTREACHED() << "Failed to get element data: " << result.ExtractError();
  }

  if (!result.is_dict()) {
    // Gracefully exit this fuzz case, this is an expected possibility.
    LOG(INFO) << "Element did not become valid";
    return -1;
  }

  const base::DictValue& dict = result.ExtractDict();

  const std::string* error_string = dict.FindString("error");
  if (error_string) {
    NOTREACHED() << "Javascript returned an error: " << *error_string;
  }

  const base::DictValue* element_dict = dict.FindDict("element");
  if (!element_dict) {
    NOTREACHED() << "Failed to find 'element' key in result: " << dict;
  }

  std::optional<int> x = element_dict->FindInt("x");
  std::optional<int> y = element_dict->FindInt("y");
  std::optional<int> width = element_dict->FindInt("width");
  std::optional<int> height = element_dict->FindInt("height");

  if (!x || !y || !width || !height || *width <= 0 || *height <= 0) {
    NOTREACHED() << "Invalid element bounds: " << *element_dict;
  }

  gfx::Rect element_bounds(*x, *y, *width, *height);

  const base::DictValue* style_dict = dict.FindDict("style");
  if (!style_dict) {
    NOTREACHED() << "Style dictionary is not present in response";
  }

  const std::string* text_color_str = style_dict->FindString("color");
  const std::string* bg_color_str = style_dict->FindString("backgroundColor");
  if (!text_color_str || !bg_color_str) {
    NOTREACHED() << "Colors not available in response";
  }

  std::optional<SkColor> text_color = ParseRgbString(*text_color_str);
  std::optional<SkColor> bg_color = ParseRgbString(*bg_color_str);

  if (!text_color || !bg_color) {
    NOTREACHED() << "Failed to parse colors: " << text_color_str << ", "
                 << bg_color_str;
  }

  // Take screenshot of the element to run heuristic on.
  base::RunLoop run_loop;
  web_contents->GetPrimaryMainFrame()->GetView()->CopyFromSurface(
      element_bounds, gfx::Size(), base::TimeDelta(),
      base::BindOnce(&GeolocationElementCssFuzzer::OnScreenshotCaptured,
                     base::Unretained(this), run_loop.QuitClosure(),
                     *text_color, *bg_color));
  run_loop.Run();

  return 0;
}

void GeolocationElementCssFuzzer::OnScreenshotCaptured(
    base::OnceClosure quit_closure,
    SkColor text_color,
    SkColor bg_color,
    const content::CopyFromSurfaceResult& result) {
  base::ScopedClosureRunner closer(std::move(quit_closure));

  if (!result.has_value()) {
    NOTREACHED() << "Screenshot failed";
  }

  const SkBitmap& bitmap = result->bitmap;

  if (bitmap.height() == 0 || bitmap.width() == 0) {
    NOTREACHED() << "Bitmap is empty";
  }

  // This is the main goal of the test. We use a simple island counting
  // algorithm to count the number of islands matching background/text color
  // respectively and check and it matches the expected amounts if the text was
  // fully visible. This relies on the geolocation element text and font being
  // fixed.
  int text_islands = CountColorIslands(bitmap, text_color, kColorTolerance);
  int bg_islands = CountColorIslands(bitmap, bg_color, kColorTolerance);

  CHECK_EQ(text_islands, kExpectedTextIslands);
  CHECK_EQ(bg_islands, kExpectedBgIslands);
}
