// Copyright 2019 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/accessibility/platform/ax_platform_node_textprovider_win.h"

#include <optional>
#include <vector>

#include "base/strings/escape.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_bstr.h"
#include "base/win/scoped_safearray.h"
#include "base/win/scoped_variant.h"
#include "content/browser/accessibility/accessibility_test_helpers.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/test/accessibility_notification_waiter.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/scoped_accessibility_mode_override.h"
#include "content/shell/browser/shell.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h"
#include "ui/accessibility/platform/browser_accessibility.h"
#include "ui/accessibility/platform/browser_accessibility_com_win.h"
#include "ui/accessibility/platform/browser_accessibility_manager.h"

using Microsoft::WRL::ComPtr;

namespace content {

#define EXPECT_UIA_TEXTRANGE_EQ(provider, expected_content) \
  {                                                         \
    base::win::ScopedBstr provider_content;                 \
    ASSERT_HRESULT_SUCCEEDED(                               \
        provider->GetText(-1, provider_content.Receive())); \
    EXPECT_STREQ(expected_content, provider_content.Get()); \
  }

class AXPlatformNodeTextProviderWinBrowserTest : public ContentBrowserTest {
 protected:
  void SetUpOnMainThread() override {
    accessibility_mode_.emplace(ui::kAXModeDefaultForTests);
  }

  void TearDownOnMainThread() override { accessibility_mode_.reset(); }

  void LoadInitialAccessibilityTreeFromUrl(const GURL& url) {
    AccessibilityNotificationWaiter waiter(shell()->web_contents(),
                                           ax::mojom::Event::kLoadComplete);
    EXPECT_TRUE(NavigateToURL(shell(), url));
    ASSERT_TRUE(waiter.WaitForNotification());
  }

  void LoadInitialAccessibilityTreeFromHtmlFilePath(
      const std::string& html_file_path) {
    if (!embedded_test_server()->Started()) {
      ASSERT_TRUE(embedded_test_server()->Start());
    }
    ASSERT_TRUE(embedded_test_server()->Started());
    LoadInitialAccessibilityTreeFromUrl(
        embedded_test_server()->GetURL(html_file_path));
  }

  void LoadInitialAccessibilityTreeFromHtml(const std::string& html) {
    LoadInitialAccessibilityTreeFromUrl(
        GURL("data:text/html," + base::EscapeQueryParamValue(html, false)));
  }

  ui::BrowserAccessibilityManager* GetManagerAndAssertNonNull() {
    auto GetManagerAndAssertNonNull =
        [this](ui::BrowserAccessibilityManager** result) {
          WebContentsImpl* web_contents_impl =
              static_cast<WebContentsImpl*>(shell()->web_contents());
          ASSERT_NE(nullptr, web_contents_impl);
          ui::BrowserAccessibilityManager* browser_accessibility_manager =
              web_contents_impl->GetRootBrowserAccessibilityManager();
          ASSERT_NE(nullptr, browser_accessibility_manager);
          *result = browser_accessibility_manager;
        };

    ui::BrowserAccessibilityManager* browser_accessibility_manager;
    GetManagerAndAssertNonNull(&browser_accessibility_manager);
    return browser_accessibility_manager;
  }

  ui::BrowserAccessibility* GetRootAndAssertNonNull() {
    auto GetRootAndAssertNonNull = [this](ui::BrowserAccessibility** result) {
      ui::BrowserAccessibility* root_browser_accessibility =
          GetManagerAndAssertNonNull()->GetBrowserAccessibilityRoot();
      ASSERT_NE(nullptr, result);
      *result = root_browser_accessibility;
    };

    ui::BrowserAccessibility* root_browser_accessibility;
    GetRootAndAssertNonNull(&root_browser_accessibility);
    return root_browser_accessibility;
  }

  ui::BrowserAccessibility* FindNode(ax::mojom::Role role,
                                     const std::string& name_or_value) {
    return FindFirstAccessibilityNodeWithRoleAndNameOrValue(
        *GetRootAndAssertNonNull(), role, name_or_value);
  }

  void GetTextProviderFromTextNode(
      ComPtr<ITextProvider>& text_provider,
      ui::BrowserAccessibility* target_browser_accessibility) {
    auto* provider_simple =
        ToBrowserAccessibilityWin(target_browser_accessibility)->GetCOM();
    ASSERT_NE(nullptr, provider_simple);

    EXPECT_HRESULT_SUCCEEDED(
        provider_simple->GetPatternProvider(UIA_TextPatternId, &text_provider));
    ASSERT_NE(nullptr, text_provider.Get());
  }

  std::vector<ComPtr<ITextRangeProvider>> GetTextRangeProvidersFromTextProvider(
      ComPtr<ITextProvider>& text_provider) {
    base::win::ScopedSafearray text_provider_ranges;
    EXPECT_HRESULT_SUCCEEDED(
        text_provider->GetVisibleRanges(text_provider_ranges.Receive()));

    auto lock_scope =
        text_provider_ranges.CreateLockScope<VT_UNKNOWN>().value();

    std::vector<ComPtr<ITextRangeProvider>> text_range_providers;
    for (IUnknown* unknown : lock_scope) {
      ComPtr<ITextRangeProvider> text_range_provider;
      EXPECT_HRESULT_SUCCEEDED(
          unknown->QueryInterface(IID_PPV_ARGS(&text_range_provider)));
      text_range_providers.push_back(text_range_provider);
    }
    return text_range_providers;
  }

 private:
  std::optional<ScopedAccessibilityModeOverride> accessibility_mode_;
};

IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
                       GetVisibleBounds) {
  LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
      <!DOCTYPE html>
      <html>
        <body>
          <div style='overflow: hidden; width: 10em; height: 2.1em;'>
            <span style='white-space: pre-line;'>AAA BBB
              CCCCCC
              DDDDDD</span>
          </div>
        </body>
      </html>
  )HTML"));

  auto* node =
      FindNode(ax::mojom::Role::kStaticText, "AAA BBB\nCCCCCC\nDDDDDD");
  ASSERT_NE(nullptr, node);
  EXPECT_TRUE(node->IsLeaf());
  EXPECT_EQ(0u, node->PlatformChildCount());

  ComPtr<ITextProvider> text_provider;
  GetTextProviderFromTextNode(text_provider, node);

  std::vector<ComPtr<ITextRangeProvider>> text_range_providers =
      GetTextRangeProvidersFromTextProvider(text_provider);
  ASSERT_EQ(2u, text_range_providers.size());

  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[0].Get(), L"AAA BBB");
  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[1].Get(), L"CCCCCC");
}

IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
                       GetVisibleRangesPositionsOnLeafNodes) {
  LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
      <!DOCTYPE html>
      <html>
        <body>
          <div contenteditable="true" role="textbox" aria-label="text">
            <div><span>one two</span></div>
            <div><span>three four</span></div>
            <div><span>five six</span></div>
          </div>
        </body>
      </html>
  )HTML"));

  auto* node = FindNode(ax::mojom::Role::kTextField, "text");
  ASSERT_NE(nullptr, node);

  ComPtr<ITextProvider> text_provider;
  GetTextProviderFromTextNode(text_provider, node);

  std::vector<ComPtr<ITextRangeProvider>> text_range_providers =
      GetTextRangeProvidersFromTextProvider(text_provider);
  ASSERT_EQ(3u, text_range_providers.size());

  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[0].Get(), L"one two");
  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[1].Get(), L"three four");
  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[2].Get(), L"five six");
}

IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
                       FindTextOnRangesReturnedByGetVisibleRanges) {
  LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
      <!DOCTYPE html>
      <html>
        <body>
          <div contenteditable="true" role="textbox" aria-label="text">
            <div><span>one two</span></div>
            <div><span>three four</span></div>
            <div><span>five six</span></div>
          </div>
        </body>
      </html>
  )HTML"));

  auto* node = FindNode(ax::mojom::Role::kTextField, "text");
  ASSERT_NE(nullptr, node);

  ComPtr<ITextProvider> text_provider;
  GetTextProviderFromTextNode(text_provider, node);

  std::vector<ComPtr<ITextRangeProvider>> text_range_providers =
      GetTextRangeProvidersFromTextProvider(text_provider);
  ASSERT_EQ(3u, text_range_providers.size());

  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[0].Get(), L"one two");
  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[1].Get(), L"three four");
  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[2].Get(), L"five six");

  {
    base::win::ScopedBstr find_string(L"two");
    Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found;
    EXPECT_HRESULT_SUCCEEDED(text_range_providers[0]->FindText(
        find_string.Get(), false, false, &text_range_provider_found));
    ASSERT_TRUE(text_range_provider_found.Get());
  }
  {
    base::win::ScopedBstr find_string(L"three");
    Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found;
    EXPECT_HRESULT_SUCCEEDED(text_range_providers[1]->FindText(
        find_string.Get(), false, false, &text_range_provider_found));
    ASSERT_TRUE(text_range_provider_found.Get());
  }
  {
    base::win::ScopedBstr find_string(L"five six");
    Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found;
    EXPECT_HRESULT_SUCCEEDED(text_range_providers[2]->FindText(
        find_string.Get(), false, false, &text_range_provider_found));
    ASSERT_TRUE(text_range_provider_found.Get());
  }
}

IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
                       GetVisibleRangesInContentEditable) {
  LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
      <!DOCTYPE html>
      <html>
        <body>
          <div contenteditable="true">
            <p>hello</p>
          </div>
        </body>
      </html>
  )HTML"));

  auto* gc_node = FindNode(ax::mojom::Role::kGenericContainer, "hello");

  ASSERT_NE(nullptr, gc_node);
  EXPECT_EQ(1u, gc_node->PlatformChildCount());

  ComPtr<ITextProvider> text_provider;
  GetTextProviderFromTextNode(text_provider, gc_node);

  std::vector<ComPtr<ITextRangeProvider>> text_range_providers =
      GetTextRangeProvidersFromTextProvider(text_provider);
  ASSERT_EQ(1u, text_range_providers.size());

  // If the `embedded_object_character` was being exposed, the search for this
  // string would fail.
  // We have to use `FindText` instead of the `EXPECT_UIA_TEXTRANGE_EQ` macro
  // since that macro uses `GetText` API which hardcodes the
  // `AXEmbeddedObjectCharacter` to be exposed, which then in this case would
  // mess up the text range. Filing a bug for `GetText`. CRBug: 1445692
  base::win::ScopedBstr find_string(L"hello");
  Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider_found;
  EXPECT_HRESULT_SUCCEEDED(text_range_providers[0]->FindText(
      find_string.Get(), false, false, &text_range_provider_found));
  ASSERT_TRUE(text_range_provider_found.Get());
}

IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
                       GetVisibleRangesForTextSlightlyOutsideContainer) {
  LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
      <!DOCTYPE html>
      <html>
        <body>
          <div role='textbox' contenteditable="true" style='height: 10px;'>
            <span style='height:20px; display:inline-block'>hello</span>
          </div>
        </body>
      </html>
  )HTML"));

  auto* gc_node = FindNode(ax::mojom::Role::kTextField, "hello");

  ASSERT_NE(nullptr, gc_node);
  EXPECT_EQ(1u, gc_node->PlatformChildCount());

  ComPtr<ITextProvider> text_provider;
  GetTextProviderFromTextNode(text_provider, gc_node);

  std::vector<ComPtr<ITextRangeProvider>> text_range_providers =
      GetTextRangeProvidersFromTextProvider(text_provider);
  ASSERT_EQ(1u, text_range_providers.size());

  EXPECT_UIA_TEXTRANGE_EQ(text_range_providers[0].Get(), L"hello");
}

IN_PROC_BROWSER_TEST_F(AXPlatformNodeTextProviderWinBrowserTest,
                       GetVisibleRangesRefCount) {
  LoadInitialAccessibilityTreeFromHtml(std::string(R"HTML(
      <!DOCTYPE html>
      <html>
        <body>
            hello
        </body>
      </html>
  )HTML"));

  auto* text_node = FindNode(ax::mojom::Role::kStaticText, "hello");
  ASSERT_NE(nullptr, text_node);

  ComPtr<ITextProvider> text_provider;
  GetTextProviderFromTextNode(text_provider, text_node);

  base::win::ScopedSafearray visible_ranges;
  EXPECT_HRESULT_SUCCEEDED(
      text_provider->GetVisibleRanges(visible_ranges.Receive()));
  ASSERT_EQ(1u, visible_ranges.GetCount());

  LONG index = 0;
  ComPtr<ITextRangeProvider> text_range_provider;
  EXPECT_HRESULT_SUCCEEDED(SafeArrayGetElement(
      visible_ranges.Get(), &index, static_cast<void**>(&text_range_provider)));

  // Validate that there was only one reference to the `text_range_provider`.
  ASSERT_EQ(1U, text_range_provider->Release());

  // This is needed to avoid calling SafeArrayDestroy from SafeArray's dtor when
  // exiting the scope, which would crash trying to release the already
  // destroyed `text_range_provider`.
  visible_ranges.Release();
}

}  // namespace content
