// Copyright 2024 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/screen_ai/public/optical_character_recognizer.h"

#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/scoped_observation.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/test_future.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/screen_ai/screen_ai_install_state.h"
#include "chrome/browser/screen_ai/screen_ai_service_router.h"
#include "chrome/browser/screen_ai/screen_ai_service_router_factory.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/metrics/content/subprocess_metrics_provider.h"
#include "content/public/test/browser_test.h"
#include "services/screen_ai/buildflags/buildflags.h"
#include "services/screen_ai/public/cpp/utilities.h"
#include "services/screen_ai/public/mojom/screen_ai_service.mojom.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkMallocPixelRef.h"
#include "third_party/skia/include/core/SkPixelRef.h"
#include "ui/accessibility/accessibility_features.h"
#include "ui/accessibility/accessibility_switches.h"
#include "ui/accessibility/ax_features.mojom-features.h"
#include "ui/gfx/codec/png_codec.h"

namespace {

// LINT.IfChange(kServiceIdleCheckingDelay)
constexpr base::TimeDelta kServiceIdleCheckingDelay = base::Seconds(3);
// LINT.ThenChange(//services/screen_ai/screen_ai_service_impl.cc:kIdleCheckingDelay)

#if BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS) && !BUILDFLAG(USE_FAKE_SCREEN_AI)
// LINT.IfChange(kResourceMeasurementInterval)
constexpr base::TimeDelta kResourceMeasurementInterval = base::Seconds(1);
// LINT.ThenChange(//chrome/browser/screen_ai/resource_monitor.cc:kSampleInterval)
#endif

using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::IsEmpty;
using ::testing::Pointee;

// Load image from test data directory.
SkBitmap LoadImageFromTestFile(
    const base::FilePath& relative_path_from_chrome_data) {
  base::ScopedAllowBlockingForTesting allow_blocking;

  base::FilePath chrome_src_dir;
  EXPECT_TRUE(
      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &chrome_src_dir));

  base::FilePath image_path =
      chrome_src_dir.Append(FILE_PATH_LITERAL("chrome/test/data"))
          .Append(relative_path_from_chrome_data);
  EXPECT_TRUE(base::PathExists(image_path)) << image_path;

  std::optional<std::vector<uint8_t>> image_data =
      base::ReadFileToBytes(image_path);

  SkBitmap image = gfx::PNGCodec::Decode(image_data.value());
  EXPECT_FALSE(image.isNull());
  return image;
}

void WaitForStatus(scoped_refptr<screen_ai::OpticalCharacterRecognizer> ocr,
                   base::OnceClosure callback,
                   int remaining_tries) {
  if (ocr->StatusAvailableForTesting() || !remaining_tries) {
    std::move(callback).Run();
    return;
  }

  // Wait more...
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&WaitForStatus, ocr, std::move(callback),
                     remaining_tries - 1),
      base::Milliseconds(200));
}

void WaitForDisconnecting(screen_ai::ScreenAIServiceRouter* router,
                          base::OnceCallback<void()> callback,
                          int remaining_tries) {
  if (!router->IsProcessRunningForTesting(
          screen_ai::ScreenAIServiceRouter::Service::kOCR) ||
      !remaining_tries) {
    std::move(callback).Run();
    return;
  }

  // Wait more...
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&WaitForDisconnecting, router, std::move(callback),
                     remaining_tries - 1),
      kServiceIdleCheckingDelay);
}

// bool: PDF OCR service enabled.
// bool: ScreenAI library available.
using OpticalCharacterRecognizerTestParams = std::tuple<bool, bool>;

struct OpticalCharacterRecognizerTestParamsToString {
  std::string operator()(
      const ::testing::TestParamInfo<OpticalCharacterRecognizerTestParams>&
          info) const {
    return base::StringPrintf(
        "OCR_%s_Library_%s", std::get<0>(info.param) ? "Enabled" : "Disabled",
        std::get<1>(info.param) ? "Available" : "Unavailable");
  }
};

#if BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS) && !BUILDFLAG(USE_FAKE_SCREEN_AI)

// Name of files which have results.
const int kTestFilenamesCount = 7;
std::string_view kTestFilenames[kTestFilenamesCount] = {
    "simple_text_only_sample",
    "chinese",
    "farsi",
    "hindi",
    "japanese",
    "korean",
    "russian"};

// Computes string match based on the edit distance of the two strings.
// Returns 0 for totally different strings and 1 for totally matching. See
// `StringMatchTest` for some examples.
double StringMatch(std::string_view expected, std::string_view extracted) {
  unsigned extracted_size = extracted.size();
  unsigned expected_size = expected.size();
  if (!expected_size || !extracted_size) {
    return (expected_size || extracted_size) ? 0 : 1;
  }

  // d[i][j] is the best match (shortest edit distance) until expected[i] and
  // extracted[j].
  std::vector<std::vector<int>> d(expected_size,
                                  std::vector<int>(extracted_size));

  for (unsigned i = 0; i < expected_size; i++) {
    for (unsigned j = 0; j < extracted_size; j++) {
      int local_match = (expected[i] == extracted[j]) ? 0 : 1;
      if (!i && !j) {
        d[i][j] = local_match;
        continue;
      }

      int best_match = expected_size + extracted_size;
      // Insert
      if (i) {
        best_match = std::min(best_match, d[i - 1][j] + 1);
      }
      // Delete
      if (j) {
        best_match = std::min(best_match, d[i][j - 1] + 1);
      }
      // Replace/Accept
      if (i && j) {
        best_match = std::min(best_match, d[i - 1][j - 1] + local_match);
      }
      d[i][j] = best_match;
    }
  }
  return 1.0f -
         (d[expected_size - 1][extracted_size - 1]) /
             static_cast<double>(std::max(expected_size, extracted_size));
}

#endif  // BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS) &&
        // !BUILDFLAG(USE_FAKE_SCREEN_AI)

}  // namespace

namespace screen_ai {

class OpticalCharacterRecognizerTestBase : public InProcessBrowserTest {
 public:
  OpticalCharacterRecognizerTestBase() = default;
  ~OpticalCharacterRecognizerTestBase() override = default;

  // Returns true if ocr initialization was successful.
  bool CreateAndInitOCR(
      mojom::OcrClientType client_type = mojom::OcrClientType::kTest) {
    base::test::TestFuture<bool> init_future;
    ocr_ = OpticalCharacterRecognizer::CreateWithStatusCallback(
        browser()->GetProfile(), client_type, init_future.GetCallback());
    EXPECT_TRUE(init_future.Wait());
    return init_future.Get<bool>();
  }

  scoped_refptr<OpticalCharacterRecognizer> ocr() { return ocr_; }

  mojom::VisualAnnotationPtr PerformOCR(const SkBitmap& bitmap) {
    base::test::TestFuture<mojom::VisualAnnotationPtr> perform_future;
    ocr_->PerformOCR(bitmap, perform_future.GetCallback());
    return WaitForPerformOCR(std::move(perform_future));
  }

  mojom::VisualAnnotationPtr WaitForPerformOCR(
      base::test::TestFuture<mojom::VisualAnnotationPtr> perform_ocr_future) {
    if (perform_ocr_future.IsReady()) {
      return perform_ocr_future.Take();
    }

    screen_ai::ScreenAIServiceRouter* router =
        ScreenAIServiceRouterFactory::GetForBrowserContext(
            browser()->GetProfile());

    // If OCR service crashes while performing OCR, the callback will not be
    // called. A timer is used to check the connection state and stop the
    // waiting if connection state changes. This is done to reduce test time
    // when the test is bound to fail due to timeout.
    base::RepeatingTimer disconnect_timer;
    if (router->IsProcessRunningForTesting(
            screen_ai::ScreenAIServiceRouter::Service::kOCR)) {
      disconnect_timer.Start(
          FROM_HERE, base::Milliseconds(100),
          base::BindRepeating(
              [](screen_ai::ScreenAIServiceRouter* router,
                 base::test::TestFuture<mojom::VisualAnnotationPtr>* future,
                 base::RepeatingTimer* timer) {
                if (!router->IsProcessRunningForTesting(
                        screen_ai::ScreenAIServiceRouter::Service::kOCR)) {
                  future->SetValue(mojom::VisualAnnotation::New());
                  timer->Stop();
                  ADD_FAILURE()
                      << "OCR service disconnected while performing OCR.";
                }
              },
              base::Unretained(router), base::Unretained(&perform_ocr_future),
              base::Unretained(&disconnect_timer)));
    } else {
      perform_ocr_future.SetValue(mojom::VisualAnnotation::New());
      ADD_FAILURE() << "OCR service is not running.";
    }

    EXPECT_TRUE(perform_ocr_future.Wait());
    disconnect_timer.Stop();
    return perform_ocr_future.Take();
  }

 protected:
  scoped_refptr<OpticalCharacterRecognizer> ocr_;
};

// This test fixture tests different running configurations of the OCR service,
// the availability of the ScreenAI library, and usage pattern of the OCR
// service API. It is NOT focused on the OCR results.
// Params: (OCR service enabled, ScreenAI library available)
class OpticalCharacterRecognizerTest
    : public OpticalCharacterRecognizerTestBase,
      public ScreenAIInstallState::Observer,
      public ::testing::WithParamInterface<
          OpticalCharacterRecognizerTestParams> {
 public:
  OpticalCharacterRecognizerTest() {
    std::vector<base::test::FeatureRef> enabled_features;
    std::vector<base::test::FeatureRef> disabled_features;

    if (IsOcrServiceEnabled()) {
      enabled_features.push_back(ax::mojom::features::kScreenAIOCREnabled);
    } else {
      disabled_features.push_back(ax::mojom::features::kScreenAIOCREnabled);
    }

    if (IsLibraryAvailable()) {
      enabled_features.push_back(::features::kScreenAITestMode);
    }

    feature_list_.InitWithFeatures(enabled_features, disabled_features);
  }

  ~OpticalCharacterRecognizerTest() override = default;

  bool IsOcrServiceEnabled() const { return std::get<0>(GetParam()); }
  bool IsLibraryAvailable() const {
#if BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS)
    return std::get<1>(GetParam()) &&
           ScreenAIInstallState::IsDeviceCompatible();
#else
    return false;
#endif
  }

  bool IsOcrAvailable() const {
    return IsOcrServiceEnabled() && IsLibraryAvailable();
  }

  // InProcessBrowserTest:
  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();

    if (IsLibraryAvailable()) {
#if BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS)
      ScreenAIInstallState::GetInstance()->SetComponentFolder(
          GetComponentBinaryPathForTests().DirName());
#else
      NOTREACHED() << "Test library is used on a not-suppported platform.";
#endif
    } else {
      // Set an observer to reply download failed, when download requested.
      component_download_observer_.Observe(ScreenAIInstallState::GetInstance());
    }
  }

  // InProcessBrowserTest:
  void TearDownOnMainThread() override {
    // The Observer should be removed before browser shut down and destruction
    // of the ScreenAIInstallState.
    component_download_observer_.Reset();
  }

  // ScreenAIInstallState::Observer:
  void StateChanged(ScreenAIInstallState::State state) override {
    if (state == ScreenAIInstallState::State::kDownloading &&
        !IsLibraryAvailable()) {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE, base::BindOnce([]() {
            ScreenAIInstallState::GetInstance()->SetState(
                ScreenAIInstallState::State::kDownloadFailed);
          }));
    }
  }

 private:
  base::ScopedObservation<ScreenAIInstallState, ScreenAIInstallState::Observer>
      component_download_observer_{this};
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest, Create) {
  scoped_refptr<screen_ai::OpticalCharacterRecognizer> ocr =
      screen_ai::OpticalCharacterRecognizer::Create(
          browser()->GetProfile(), mojom::OcrClientType::kTest);
  base::test::TestFuture<void> future;
  // This step can be slow.
  WaitForStatus(ocr, future.GetCallback(), /*remaining_tries=*/25);
  ASSERT_TRUE(future.Wait());

  EXPECT_TRUE(ocr->StatusAvailableForTesting());
  EXPECT_EQ(ocr->is_ready(), IsOcrAvailable());

  base::test::TestFuture<uint32_t> max_dimension_future;
  ocr->GetMaxImageDimension(max_dimension_future.GetCallback());
  ASSERT_TRUE(max_dimension_future.Wait());
  ASSERT_EQ(max_dimension_future.Get<uint32_t>(),
            IsOcrAvailable() ? screen_ai::GetMaxDimensionForOCR() : 0);
}

IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest,
                       CreateWithStatusCallback) {
  base::test::TestFuture<bool> future;
  scoped_refptr<OpticalCharacterRecognizer> ocr =
      OpticalCharacterRecognizer::CreateWithStatusCallback(
          browser()->GetProfile(), mojom::OcrClientType::kTest,
          future.GetCallback());

  ASSERT_TRUE(future.Wait());
  ASSERT_EQ(future.Get<bool>(), IsOcrAvailable());
  ASSERT_TRUE(ocr);

  base::test::TestFuture<uint32_t> max_dimension_future;
  ocr->GetMaxImageDimension(max_dimension_future.GetCallback());
  ASSERT_TRUE(max_dimension_future.Wait());
  ASSERT_EQ(max_dimension_future.Get<uint32_t>(),
            IsOcrAvailable() ? screen_ai::GetMaxDimensionForOCR() : 0);
}

IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest,
                       CreateWithStatusCallbackWithReturnedPtrDestructed) {
  base::test::TestFuture<bool> future;

  // Create an `OpticalCharacterRecognizer` scoped_refptr and then immediately
  // discard the result.
  OpticalCharacterRecognizer::CreateWithStatusCallback(
      browser()->GetProfile(), mojom::OcrClientType::kTest,
      future.GetCallback());

  // The status callback should still be run without crashing even though the
  // created scoped_refptr was destroyed.
  EXPECT_TRUE(future.Wait());
}

// Test OCR on an image that draws nothing.
// Note: We cannot have a separate test for a pure drawsNothing() image (where
// drawsNothing() is true but empty() is false). Such a bitmap must have
// isNull() == true in Skia, which is rejected by Mojo's serialization
// validation on the client side before the message is sent. Hence, we use a
// non-null 1x0 empty image to cover the drawsNothing() || empty() check in the
// service.
IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest,
                       PerformOCR_DrawsNothing) {
  ASSERT_EQ(CreateAndInitOCR(mojom::OcrClientType::kTest), IsOcrAvailable());

  SkBitmap bitmap;
  SkImageInfo info = SkImageInfo::MakeN32Premul(1, 0);
  bitmap.setInfo(info);
  sk_sp<SkPixelRef> pr = SkMallocPixelRef::MakeAllocate(info, 0);
  ASSERT_TRUE(pr);
  bitmap.setPixelRef(std::move(pr), 0, 0);

  ASSERT_TRUE(bitmap.drawsNothing());
  ASSERT_TRUE(bitmap.empty());
  ASSERT_FALSE(bitmap.isNull());

  mojom::VisualAnnotationPtr results = PerformOCR(bitmap);
  ASSERT_TRUE(results->lines.empty());
}

// Test OCR on an blank white image with no text.
IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest, PerformOCR_Empty) {
  ASSERT_EQ(CreateAndInitOCR(mojom::OcrClientType::kTest), IsOcrAvailable());

  SkBitmap bitmap =
      LoadImageFromTestFile(base::FilePath(FILE_PATH_LITERAL("ocr/empty.png")));
  mojom::VisualAnnotationPtr results = PerformOCR(bitmap);
  ASSERT_TRUE(results->lines.empty());
}

// The image used in this test is very simple to reduce the possibility of
// failure due to library changes.
// If this test fails after updating the library, there is a high probability
// that the new library has some sort of incompatibility with Chromium.
IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest, PerformOCR_Simple) {
  base::HistogramTester histograms;

  ASSERT_EQ(CreateAndInitOCR(mojom::OcrClientType::kTest), IsOcrAvailable());

  SkBitmap bitmap = LoadImageFromTestFile(
      base::FilePath(FILE_PATH_LITERAL("ocr/just_one_letter.png")));
  mojom::VisualAnnotationPtr results = PerformOCR(bitmap);

// Fake library always returns empty.
#if BUILDFLAG(USE_FAKE_SCREEN_AI)
  bool expected_call_success = false;
#else
  bool expected_call_success = true;
#endif
  unsigned expected_lines_count =
      (expected_call_success && IsOcrAvailable()) ? 1 : 0;
  ASSERT_EQ(expected_lines_count, results->lines.size());
  if (results->lines.size()) {
    ASSERT_EQ(results->lines[0]->text_line, "A");
  }

  metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();

  unsigned expected_calls = IsOcrAvailable() ? 1 : 0;
  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.ClientType",
                              expected_calls);
  histograms.ExpectBucketCount("Accessibility.ScreenAI.OCR.ClientType",
                               mojom::OcrClientType::kTest, expected_calls);

  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.Successful",
                              expected_calls);
  histograms.ExpectBucketCount("Accessibility.ScreenAI.OCR.Successful",
                               expected_call_success, expected_calls);

  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.LinesCount",
                              expected_calls);
  histograms.ExpectBucketCount("Accessibility.ScreenAI.OCR.LinesCount",
                               expected_lines_count, expected_calls);

  // Expect measured latency, but we don't know how long it taskes to process.
  // So we just check the total count of the expected bucket determined by the
  // image dimensions, with threshold 2048 for each dimension.
  EXPECT_GE(2048, bitmap.width());
  EXPECT_GE(2048, bitmap.height());
  histograms.ExpectTotalCount(
      "Accessibility.ScreenAI.OCR.Latency.NotDownsampled", expected_calls);
  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.Latency.Downsampled",
                              0);
  histograms.ExpectTotalCount(
      "Accessibility.ScreenAI.OCR.Downsampled.ClientType", 0);

  // PDF Specific metrics should not be recorded as the client type is test.
  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.LinesCount.PDF", 0);
  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.Time.PDF", 0);
  histograms.ExpectTotalCount(
      "Accessibility.ScreenAI.OCR.ImageSize.PDF.WithText", 0);
  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.ImageSize.PDF.NoText",
                              0);
  histograms.ExpectTotalCount(
      "Accessibility.ScreenAI.OCR.MostDetectedLanguage.PDF", 0);
}

IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest, PerformOCR_PdfMetrics) {
  base::HistogramTester histograms;

  ASSERT_EQ(CreateAndInitOCR(mojom::OcrClientType::kPdfViewer),
            IsOcrAvailable());

  SkBitmap bitmap = LoadImageFromTestFile(
      base::FilePath(FILE_PATH_LITERAL("ocr/just_one_letter.png")));
  mojom::VisualAnnotationPtr results = PerformOCR(bitmap);

// Fake library always returns empty.
#if BUILDFLAG(USE_FAKE_SCREEN_AI)
  bool expected_call_success = false;
#else
  bool expected_call_success = true;
#endif

  metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();

  unsigned expected_calls = IsOcrAvailable() ? 1 : 0;
  unsigned expected_lines_count =
      (expected_call_success && IsOcrAvailable()) ? 1 : 0;

  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.LinesCount.PDF",
                              expected_calls);
  histograms.ExpectBucketCount("Accessibility.ScreenAI.OCR.LinesCount.PDF",
                               expected_lines_count, expected_calls);

  // Since the text in the image is just one letter, language is not detected.
  histograms.ExpectTotalCount(
      "Accessibility.ScreenAI.OCR.MostDetectedLanguage.PDF",0);

  // Expect measured latency and image size, but we don't know how long it
  // taskes to process and how large the image is.
  // So we just check the total count of the expected bucket.
  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.Time.PDF",
                              expected_calls);
  histograms.ExpectTotalCount(
      "Accessibility.ScreenAI.OCR.ImageSize.PDF.WithText",
      expected_lines_count);

  // If OCR is not available, the metric is not recorded at all. But when it is
  // available, the expectation is the opposite of the above metrics.
  unsigned expected_no_text_calls =
      IsOcrAvailable() ? (1 - expected_lines_count) : 0;
  histograms.ExpectTotalCount("Accessibility.ScreenAI.OCR.ImageSize.PDF.NoText",
                              expected_no_text_calls);
}

IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest,
                       PerformOCR_AfterServiceRevive) {
  if (!IsOcrAvailable()) {
    GTEST_SKIP() << "This test is only available when service is available";
  }

  screen_ai::ScreenAIServiceRouter* router =
      ScreenAIServiceRouterFactory::GetForBrowserContext(
          browser()->GetProfile());

  // Init OCR once and verify service availability.
  ASSERT_TRUE(CreateAndInitOCR(mojom::OcrClientType::kTest));
  ASSERT_TRUE(router->IsProcessRunningForTesting(
      screen_ai::ScreenAIServiceRouter::Service::kOCR));

  // Release it and wait for shutdown due to being idle.
  ocr_.reset();
  base::test::TestFuture<void> future;
  WaitForDisconnecting(router, future.GetCallback(), /*remaining_tries=*/3);
  ASSERT_TRUE(future.Wait());
  ASSERT_FALSE(router->IsProcessRunningForTesting(
      screen_ai::ScreenAIServiceRouter::Service::kOCR));

  // Init OCR again.
  ASSERT_TRUE(CreateAndInitOCR(mojom::OcrClientType::kTest));
  ASSERT_TRUE(router->IsProcessRunningForTesting(
      screen_ai::ScreenAIServiceRouter::Service::kOCR));

  // Perform OCR.
  SkBitmap bitmap = LoadImageFromTestFile(
      base::FilePath(FILE_PATH_LITERAL("ocr/just_one_letter.png")));
  mojom::VisualAnnotationPtr results = PerformOCR(bitmap);

  // Fake library always returns empty results.
#if !BUILDFLAG(USE_FAKE_SCREEN_AI)
  ASSERT_FALSE(results->lines.empty());
#endif
}

IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerTest,
                       PerformOCR_AfterDisconnect) {
  if (!IsOcrAvailable()) {
    GTEST_SKIP() << "This test is only available when service is available";
  }

  ASSERT_TRUE(CreateAndInitOCR(mojom::OcrClientType::kTest));
  ocr()->DisconnectAnnotator();

  // Perform OCR and get VisualAnnotation.
  SkBitmap bitmap = LoadImageFromTestFile(
      base::FilePath(FILE_PATH_LITERAL("ocr/just_one_letter.png")));
  mojom::VisualAnnotationPtr results = PerformOCR(bitmap);

  // Fake library always returns empty results.
#if !BUILDFLAG(USE_FAKE_SCREEN_AI)
  ASSERT_FALSE(results->lines.empty());
#endif

  ocr()->DisconnectAnnotator();

  // Perform OCR and get VisualAnnotation.
  mojom::VisualAnnotationPtr results2 = PerformOCR(bitmap);

  // Fake library always returns empty results.
#if !BUILDFLAG(USE_FAKE_SCREEN_AI)
  ASSERT_FALSE(results2->lines.empty());
#endif
}

INSTANTIATE_TEST_SUITE_P(All,
                         OpticalCharacterRecognizerTest,
                         ::testing::Combine(testing::Bool(), testing::Bool()),
                         OpticalCharacterRecognizerTestParamsToString());

#if BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS) && !BUILDFLAG(USE_FAKE_SCREEN_AI)

TEST(OpticalCharacterRecognizer, StringMatchTest) {
  ASSERT_EQ(StringMatch("ABC", ""), 0);
  ASSERT_EQ(StringMatch("", "ABC"), 0);
  ASSERT_EQ(StringMatch("ABC", "ABC"), 1);
  ASSERT_EQ(StringMatch("ABC", "DEF"), 0);
  ASSERT_LE(StringMatch("ABCD", "ABC"), 0.75);
  ASSERT_LE(StringMatch("ABCD", "ABXD"), 0.75);
}

// This test fixture is used for testing the OCR results on different
// languages and image conditions and covers OCR accuracy.
// Param: Test name.
class OpticalCharacterRecognizerResultsTest
    : public OpticalCharacterRecognizerTestBase,
      public ::testing::WithParamInterface<std::string_view> {
 public:
  OpticalCharacterRecognizerResultsTest() {
    feature_list_.InitWithFeatures({ax::mojom::features::kScreenAIOCREnabled,
                                    ::features::kScreenAITestMode},
                                   {});
  }

  // InProcessBrowserTest:
  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    if (!ScreenAIInstallState::IsDeviceCompatible()) {
      GTEST_SKIP()
          << "ScreenAI library is not available / compatible on this device.";
    }
    ScreenAIInstallState::GetInstance()->SetComponentFolder(
        GetComponentBinaryPathForTests().DirName());
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

// If this test fails after updating the library, the failure can be related to
// minor changes in recognition results of the library. The failed cases can be
// checked and if the new result is acceptable. For each test case, the last
// line in the .txt file is the minimum acceptable match and it can be updated.
IN_PROC_BROWSER_TEST_P(OpticalCharacterRecognizerResultsTest, PerformOCR) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  ASSERT_TRUE(CreateAndInitOCR());

  // Perform OCR.
  base::FilePath image_path =
      base::FilePath(FILE_PATH_LITERAL("ocr"))
          .AppendASCII(base::StringPrintf("%s.png", GetParam()));
  SkBitmap bitmap = LoadImageFromTestFile(image_path);
  mojom::VisualAnnotationPtr results = PerformOCR(bitmap);

  // Load expectations.
  std::string expetations;
  base::FilePath expectation_path;
  ASSERT_TRUE(
      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &expectation_path));
  expectation_path =
      expectation_path.Append(FILE_PATH_LITERAL("chrome/test/data/ocr"))
          .AppendASCII(base::StringPrintf("%s.txt", GetParam()));
  ASSERT_TRUE(ReadFileToString(expectation_path, &expetations))
      << expectation_path << " not found.";
  std::vector<std::string> expected_lines = base::SplitString(
      expetations, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);

  // The last line of expectations is the minimum expected match.
  ASSERT_NE(expected_lines.size(), 0u);
  double expected_match =
      atof(expected_lines[expected_lines.size() - 1].c_str());
  expected_lines.pop_back();

  ASSERT_EQ(results->lines.size(), expected_lines.size());
  for (unsigned i = 0; i < results->lines.size(); i++) {
    const std::string extracted_text = results->lines[i]->text_line;
    const std::string expected_text = expected_lines[i];

    double match = StringMatch(expected_text, extracted_text);
    ASSERT_GE(match, expected_match) << "Extracted text: " << extracted_text;
  }
}

INSTANTIATE_TEST_SUITE_P(All,
                         OpticalCharacterRecognizerResultsTest,
                         testing::ValuesIn(kTestFilenames));

// This test is slow and most probably failing on debug builds and ASAN builds
// which are slower than the other tests.
// TODO(crbug.com/509294498): Re-enable this test on Linux and ASan/Debug.
#if BUILDFLAG(IS_LINUX) || !defined(NDEBUG) || defined(ADDRESS_SANITIZER)
#define MAYBE_PerformOCRLargeImage DISABLED_PerformOCRLargeImage
#else
#define MAYBE_PerformOCRLargeImage PerformOCRLargeImage
#endif
IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
                       MAYBE_PerformOCRLargeImage) {
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kForceRendererAccessibility)) {
    // crbug.com/417534818
    GTEST_SKIP()
        << "This test is slow and flaky when accessibility is enabled.";
  }
  base::HistogramTester histograms;

  // Since this test processes a huge image, it can be slow and overrun the
  // timeout.
  base::test::ScopedDisableRunLoopTimeout disable_timeout;
  base::ScopedAllowBlockingForTesting allow_blocking;

  ASSERT_TRUE(CreateAndInitOCR());

  base::FilePath image_path =
      base::FilePath(FILE_PATH_LITERAL("ocr"))
          .Append(FILE_PATH_LITERAL("building_chromium_long.png"));
  SkBitmap bitmap = LoadImageFromTestFile(image_path);
  mojom::VisualAnnotationPtr results = PerformOCR(bitmap);

  // Since OCR downsamples large images, the content of this image becomes quite
  // small and unreadable, hence nothing is recognized.
  EXPECT_FALSE(results->lines.size());

  metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
  histograms.ExpectTotalCount(
      "Accessibility.ScreenAI.OCR.Downsampled.ClientType", 1);
}

IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
                       PerformOCRMultipleFilesOneByOne) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  ASSERT_TRUE(CreateAndInitOCR());

  for (std::string_view& file_name : kTestFilenames) {
    base::FilePath image_path =
        base::FilePath(FILE_PATH_LITERAL("ocr"))
            .AppendASCII(base::StringPrintf("%s.png", file_name));
    SkBitmap bitmap = LoadImageFromTestFile(image_path);
    mojom::VisualAnnotationPtr results = PerformOCR(bitmap);
    EXPECT_TRUE(results->lines.size());
  }
}

IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
                       PerformOCRMultipleFilesNoWaitBetween) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  ASSERT_TRUE(CreateAndInitOCR());

  SkBitmap bitmaps[kTestFilenamesCount];
  {
    auto* bitmap = std::begin(bitmaps);
    for (std::string_view& file_name : kTestFilenames) {
      base::FilePath image_path =
          base::FilePath(FILE_PATH_LITERAL("ocr"))
              .AppendASCII(base::StringPrintf("%s.png", file_name));
      *bitmap = LoadImageFromTestFile(image_path);
      bitmap = std::next(bitmap);
    }
  }

  base::test::TestFuture<mojom::VisualAnnotationPtr>
      futures[kTestFilenamesCount];
  {
    auto* future = std::begin(futures);
    for (SkBitmap& bitmap : bitmaps) {
      ocr()->PerformOCR(bitmap, future->GetCallback());
      future = std::next(future);
    }
  }

  // Verify that OCR is not considered busy as there is only one connected
  // client.
  {
    base::test::TestFuture<bool> future;

    ocr()->IsOCRBusy(future.GetCallback());
    EXPECT_TRUE(future.Wait());
    EXPECT_FALSE(future.Get<bool>());
  }

  for (auto& future : futures) {
    mojom::VisualAnnotationPtr results = WaitForPerformOCR(std::move(future));
    EXPECT_TRUE(results->lines.size());
  }
}

IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
                       PerformOCRMultipleClientsNoWaitBetween) {
  base::HistogramTester histograms;
  base::ScopedAllowBlockingForTesting allow_blocking;

  base::TimeTicks start_time = base::TimeTicks::Now();

  // Create multiple OCR clients.
  scoped_refptr<OpticalCharacterRecognizer> ocr_clients[kTestFilenamesCount];
  {
    base::test::TestFuture<bool> futures[kTestFilenamesCount];
    {
      auto* future = std::begin(futures);
      auto* ocr = std::begin(ocr_clients);
      for (int i = 0; i < kTestFilenamesCount; i++) {
        (*ocr) = OpticalCharacterRecognizer::CreateWithStatusCallback(
            browser()->GetProfile(), mojom::OcrClientType::kTest,
            future->GetCallback());
        future = std::next(future);
        ocr = std::next(ocr);
      }
    }

    for (auto& future : futures) {
      ASSERT_TRUE(future.Wait());
      ASSERT_TRUE(future.Get<bool>());
    }
  }

  // Load files.
  SkBitmap bitmaps[kTestFilenamesCount];
  {
    auto* bitmap = std::begin(bitmaps);
    for (std::string_view& file_name : kTestFilenames) {
      base::FilePath image_path =
          base::FilePath(FILE_PATH_LITERAL("ocr"))
              .AppendASCII(base::StringPrintf("%s.png", file_name));
      *bitmap = LoadImageFromTestFile(image_path);
      bitmap = std::next(bitmap);
    }
  }

  // Perform OCR on all client without waiting.
  base::test::TestFuture<mojom::VisualAnnotationPtr>
      futures[kTestFilenamesCount];
  {
    auto* future = std::begin(futures);
    auto* ocr = std::begin(ocr_clients);
    for (SkBitmap& bitmap : bitmaps) {
      (*ocr)->PerformOCR(bitmap, future->GetCallback());
      future = std::next(future);
      ocr = std::next(ocr);
    }
  }

  // Verify all got results.
  for (auto& future : futures) {
    ASSERT_TRUE(future.Wait());
    auto& results = future.Get<mojom::VisualAnnotationPtr>();
    EXPECT_TRUE(results->lines.size());
  }

  // Disconnect all clients.
  for (auto& ocr : ocr_clients) {
    ocr.reset();
  }

  // Wait for the service to shutdown and store metrics.
  screen_ai::ScreenAIServiceRouter* router =
      ScreenAIServiceRouterFactory::GetForBrowserContext(
          browser()->GetProfile());
  base::test::TestFuture<void> future;
  WaitForDisconnecting(router, future.GetCallback(), /*remaining_tries=*/3);
  ASSERT_TRUE(future.Wait());
  ASSERT_FALSE(router->IsProcessRunningForTesting(
      screen_ai::ScreenAIServiceRouter::Service::kOCR));

  metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();

  // Memory use and lifetime measurements should be recorded only once since all
  // tasks were done with the same process.
  histograms.ExpectTotalCount("Accessibility.OCR.Service.LifeTime", 1);

  // Since this metric is recorded at long intervals, ensure the test has been
  // running long enough to record it.
  if (base::TimeTicks::Now() - start_time > kResourceMeasurementInterval * 2) {
    histograms.ExpectTotalCount("Accessibility.OCR.Service.MaxMemoryLoad", 1);
  }

  // Expect no OCR mode change.
  histograms.ExpectUniqueSample("Accessibility.ScreenAI.OCR.ModeSwitch", 0, 1);
}

IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
                       PerformOCRLightModeEnglish) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  SkBitmap bitmap =
      LoadImageFromTestFile(base::FilePath(FILE_PATH_LITERAL("ocr"))
                                .AppendASCII("simple_text_only_sample.png"));

  ASSERT_TRUE(CreateAndInitOCR());

  // Recognize in normal mode and store data.
  mojom::VisualAnnotationPtr results1 = PerformOCR(bitmap);
  int lines_count = results1->lines.size();
  ASSERT_GT(lines_count, 0);
  std::string first_line_text = results1->lines[0]->text_line;

  // Set Light mode.
  ocr()->SetOCRLightMode(true);

  // Recognize in light mode, ensure no change.
  mojom::VisualAnnotationPtr results2 = PerformOCR(bitmap);
  ASSERT_EQ(lines_count, results2->lines.size());
  EXPECT_EQ(first_line_text, results2->lines[0]->text_line);

  // Back to normal mode and still no change.
  ocr()->SetOCRLightMode(false);
  mojom::VisualAnnotationPtr results3 = PerformOCR(bitmap);
  ASSERT_EQ(lines_count, results3->lines.size());
  EXPECT_EQ(first_line_text, results3->lines[0]->text_line);
}

IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
                       PerformOCRLightModeChinese) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  SkBitmap bitmap = LoadImageFromTestFile(
      base::FilePath(FILE_PATH_LITERAL("ocr")).AppendASCII("chinese.png"));

  ASSERT_TRUE(CreateAndInitOCR());

  // Recognize in normal mode and store data.
  mojom::VisualAnnotationPtr results1 = PerformOCR(bitmap);
  int line_count1 = results1->lines.size();
  ASSERT_GT(line_count1, 0);
  std::string first_line_text1 = results1->lines[0]->text_line;

  // Set Light mode.
  ocr()->SetOCRLightMode(true);

  // Recognize in light mode. Chinese text is not recognized, however since OCR
  // only has Latin recognizer, it tries to recognize the text using that and
  // does not return empty.
  mojom::VisualAnnotationPtr results2 = PerformOCR(bitmap);
  ASSERT_EQ(line_count1, results2->lines.size());
  EXPECT_NE(first_line_text1, results2->lines[0]->text_line);

  // Back to normal mode and correct recognition.
  ocr()->SetOCRLightMode(false);
  mojom::VisualAnnotationPtr results3 = PerformOCR(bitmap);
  ASSERT_EQ(line_count1, results3->lines.size());
  EXPECT_EQ(first_line_text1, results3->lines[0]->text_line);
}

IN_PROC_BROWSER_TEST_F(OpticalCharacterRecognizerResultsTest,
                       PerformOCRMultipleClientsLightMode) {
  base::HistogramTester histograms;
  base::ScopedAllowBlockingForTesting allow_blocking;

  // Create two OCR clients.
  scoped_refptr<OpticalCharacterRecognizer> ocr_clients[2];
  {
    base::test::TestFuture<bool> futures[2];

    ocr_clients[0] = OpticalCharacterRecognizer::CreateWithStatusCallback(
        browser()->GetProfile(), mojom::OcrClientType::kTest,
        futures[0].GetCallback());
    ocr_clients[1] = OpticalCharacterRecognizer::CreateWithStatusCallback(
        browser()->GetProfile(), mojom::OcrClientType::kTest,
        futures[1].GetCallback());

    ASSERT_TRUE(futures[0].Wait());
    ASSERT_TRUE(futures[1].Wait());
    ASSERT_TRUE(futures[0].Get<bool>());
    ASSERT_TRUE(futures[1].Get<bool>());
  }

  // Load files.
  SkBitmap chinese_bitmap = LoadImageFromTestFile(
      base::FilePath(FILE_PATH_LITERAL("ocr")).AppendASCII("chinese.png"));
  SkBitmap english_bitmap =
      LoadImageFromTestFile(base::FilePath(FILE_PATH_LITERAL("ocr"))
                                .AppendASCII("simple_text_only_sample.png"));

  // Set Light mode on the second client.
  ocr_clients[1]->SetOCRLightMode(true);

  // Send both bitmaps from both clients and wait for completion.
  {
    base::test::TestFuture<mojom::VisualAnnotationPtr> result_futures[4];

    ocr_clients[0]->PerformOCR(english_bitmap, result_futures[0].GetCallback());
    ocr_clients[1]->PerformOCR(english_bitmap, result_futures[1].GetCallback());
    ocr_clients[0]->PerformOCR(chinese_bitmap, result_futures[2].GetCallback());
    ocr_clients[1]->PerformOCR(chinese_bitmap, result_futures[3].GetCallback());

    mojom::VisualAnnotationPtr english_0 =
        WaitForPerformOCR(std::move(result_futures[0]));
    mojom::VisualAnnotationPtr english_1 =
        WaitForPerformOCR(std::move(result_futures[1]));
    mojom::VisualAnnotationPtr chinese_0 =
        WaitForPerformOCR(std::move(result_futures[2]));
    mojom::VisualAnnotationPtr chinese_1 =
        WaitForPerformOCR(std::move(result_futures[3]));

    // Light mode should not affect English text.
    EXPECT_EQ(english_0->lines.size(), english_1->lines.size());
    for (unsigned i = 0; i < english_0->lines.size(); i++) {
      EXPECT_EQ(english_0->lines[i]->text_line, english_1->lines[i]->text_line);
    }

    // Chinese on client 1 is not recognized, however since OCR only has Latin
    // recognizer, it tries to recognize the text using that and does not return
    // empty.
    EXPECT_EQ(chinese_0->lines.size(), chinese_1->lines.size());
    for (unsigned i = 0; i < chinese_0->lines.size(); i++) {
      EXPECT_NE(chinese_0->lines[i]->text_line, chinese_1->lines[i]->text_line);
    }
  }

  // Verify that the both clients consider OCR busy as there is another
  // connected client.
  {
    base::test::TestFuture<bool> features[2];

    ocr_clients[0]->IsOCRBusy(features[0].GetCallback());
    ocr_clients[1]->IsOCRBusy(features[1].GetCallback());

    EXPECT_TRUE(features[0].Wait());
    EXPECT_TRUE(features[1].Wait());
    EXPECT_TRUE(features[0].Get<bool>());
    EXPECT_TRUE(features[1].Get<bool>());
  }

  // Disconnect clients and wait for the service to shutdown and store metrics.
  ocr_clients[0].reset();
  ocr_clients[1].reset();
  screen_ai::ScreenAIServiceRouter* router =
      ScreenAIServiceRouterFactory::GetForBrowserContext(
          browser()->GetProfile());
  base::test::TestFuture<void> future;
  WaitForDisconnecting(router, future.GetCallback(), /*remaining_tries=*/3);
  ASSERT_TRUE(future.Wait());
  ASSERT_FALSE(router->IsProcessRunningForTesting(
      screen_ai::ScreenAIServiceRouter::Service::kOCR));

  metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();

  // Verify there has been three switches between light and normal mode. The
  // actual number may differ based on mojo queue handling, but at least one
  // should be recorded.
  histograms.ExpectBucketCount("Accessibility.ScreenAI.OCR.ModeSwitch", 0, 0);
  EXPECT_GT(
      histograms.GetAllSamples("Accessibility.ScreenAI.OCR.ModeSwitch").size(),
      0);
}

#endif  // BUILDFLAG(ENABLE_SCREEN_AI_BROWSERTESTS) && !
        // BUILDFLAG(USE_FAKE_SCREEN_AI)
}  // namespace screen_ai
