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

#include <set>
#include <string>
#include <vector>

#include "base/logging.h"
#include "base/strings/string16.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "content/browser/accessibility/browser_accessibility_android.h"
#include "content/browser/accessibility/browser_accessibility_manager_android.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/shell/browser/shell.h"
#include "content/test/accessibility_browser_test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace content {

const int GRANULARITY_CHARACTER =
    ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_CHARACTER;
const int GRANULARITY_WORD =
    ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_WORD;
const int GRANULARITY_LINE =
    ANDROID_ACCESSIBILITY_NODE_INFO_MOVEMENT_GRANULARITY_LINE;

class AndroidGranularityMovementBrowserTest : public ContentBrowserTest {
 public:
  AndroidGranularityMovementBrowserTest() {}
  ~AndroidGranularityMovementBrowserTest() override {}

  BrowserAccessibility* LoadUrlAndGetAccessibilityRoot(const GURL& url) {
    NavigateToURL(shell(), GURL(url::kAboutBlankURL));

    // Load the page.
    AccessibilityNotificationWaiter waiter(
        shell(), AccessibilityModeComplete,
        ui::AX_EVENT_LOAD_COMPLETE);
    NavigateToURL(shell(), url);
    waiter.WaitForNotification();

    // Get the BrowserAccessibilityManager.
    WebContentsImpl* web_contents =
        static_cast<WebContentsImpl*>(shell()->web_contents());
    return web_contents->GetRootBrowserAccessibilityManager()->GetRoot();
  }

  // First, set accessibility focus to a node and wait for the update that
  // loads inline text boxes for that node. (We load inline text boxes
  // asynchronously on Android since we only ever need them for the node
  // with accessibility focus.)
  //
  // Then call NextAtGranularity repeatedly and return a string that
  // concatenates all of the text of the returned text ranges.
  //
  // As an example, if the node's text is "cat dog" and you traverse by
  // word, this returns "'cat', 'dog'".
  //
  // Also calls PreviousAtGranularity from the end back to the beginning
  // and fails (by logging an error and returning the empty string) if
  // the result when traversing backwards is not the same
  // (but in reverse order).
  base::string16 TraverseNodeAtGranularity(
      BrowserAccessibility* node,
      int granularity) {
    AccessibilityNotificationWaiter waiter(
        shell(), AccessibilityModeComplete,
        ui::AX_EVENT_TREE_CHANGED);
    node->manager()->delegate()->AccessibilitySetAccessibilityFocus(
        node->GetId());
    waiter.WaitForNotification();

    int start_index = -1;
    int end_index = -1;
    BrowserAccessibilityAndroid* android_node =
        static_cast<BrowserAccessibilityAndroid*>(node);
    BrowserAccessibilityManagerAndroid* manager =
        static_cast<BrowserAccessibilityManagerAndroid*>(node->manager());
    base::string16 text = android_node->GetText();
    base::string16 concatenated;
    int previous_end_index = -1;
    while (manager->NextAtGranularity(
               granularity, end_index, android_node,
               &start_index, &end_index)) {
      int len = (granularity == GRANULARITY_CHARACTER) ?
          1 : end_index - start_index;
      base::string16 selection = text.substr(start_index, len);
      if (base::EndsWith(selection, base::ASCIIToUTF16("\n"),
                         base::CompareCase::INSENSITIVE_ASCII))
        selection.erase(selection.size() - 1);
      if (!selection.empty()) {
        if (!concatenated.empty())
          concatenated += base::ASCIIToUTF16(", ");
        concatenated += base::ASCIIToUTF16("'") + selection +
            base::ASCIIToUTF16("'");
      }

      // Prevent an endless loop.
      if (end_index == previous_end_index) {
        LOG(ERROR) << "Bailing from loop, NextAtGranularity didn't advance";
        break;
      }
      previous_end_index = end_index;
    }

    base::string16 reverse;
    previous_end_index = -1;
    start_index = end_index;
    while (manager->PreviousAtGranularity(
        granularity, start_index, android_node, &start_index, &end_index)) {
      int len = (granularity == GRANULARITY_CHARACTER) ?
          1 : end_index - start_index;
      base::string16 selection = text.substr(start_index, len);
      if (base::EndsWith(selection, base::ASCIIToUTF16("\n"),
                         base::CompareCase::INSENSITIVE_ASCII))
        selection = selection.substr(0, selection.size() - 1);
      if (!reverse.empty())
        reverse = base::ASCIIToUTF16(", ") + reverse;
      reverse = base::ASCIIToUTF16("'") + selection +
          base::ASCIIToUTF16("'") + reverse;

      // Prevent an endless loop.
      if (end_index == previous_end_index) {
        LOG(ERROR) << "Bailing from loop, PreviousAtGranularity didn't advance";
        break;
      }
      previous_end_index = end_index;
    }

    if (concatenated != reverse) {
      LOG(ERROR) << "Did not get the same sequence in the forwards and "
                 << "reverse directions!";
      LOG(ERROR) << "Forwards: " << concatenated;
      LOG(ERROR) << "Backwards " << reverse;
      return base::string16();
    }

    return concatenated;
  }
};

IN_PROC_BROWSER_TEST_F(AndroidGranularityMovementBrowserTest,
                       NavigateByCharacters) {
  GURL url("data:text/html,"
           "<body>"
           "<p>One, two, three!</p>"
           "<button aria-label='Seven, eight, nine!'>Four, five, six!</button>"
           "</body></html>");
  BrowserAccessibility* root = LoadUrlAndGetAccessibilityRoot(url);
  ASSERT_EQ(2U, root->PlatformChildCount());
  BrowserAccessibility* para = root->PlatformGetChild(0);
  ASSERT_EQ(0U, para->PlatformChildCount());
  BrowserAccessibility* button_container = root->PlatformGetChild(1);
  ASSERT_EQ(1U, button_container->PlatformChildCount());
  BrowserAccessibility* button = button_container->PlatformGetChild(0);
  ASSERT_EQ(0U, button->PlatformChildCount());

  ASSERT_EQ(
      base::ASCIIToUTF16("'O', 'n', 'e', ',', ' ', 't', 'w', 'o', "
                         "',', ' ', 't', 'h', 'r', 'e', 'e', '!'"),
      TraverseNodeAtGranularity(para, GRANULARITY_CHARACTER));
  ASSERT_EQ(
      base::ASCIIToUTF16("'S', 'e', 'v', 'e', 'n', ',', ' ', 'e', 'i', 'g', "
                         "'h', 't', ',', ' ', 'n', 'i', 'n', 'e', '!'"),
      TraverseNodeAtGranularity(button, GRANULARITY_CHARACTER));
}

IN_PROC_BROWSER_TEST_F(AndroidGranularityMovementBrowserTest,
                       NavigateByWords) {
  GURL url("data:text/html,"
           "<body>"
           "<p>One, two, three!</p>"
           "<button aria-label='Seven, eight, nine!'>Four, five, six!</button>"
           "</body></html>");
  BrowserAccessibility* root = LoadUrlAndGetAccessibilityRoot(url);
  ASSERT_EQ(2U, root->PlatformChildCount());
  BrowserAccessibility* para = root->PlatformGetChild(0);
  ASSERT_EQ(0U, para->PlatformChildCount());
  BrowserAccessibility* button_container = root->PlatformGetChild(1);
  ASSERT_EQ(1U, button_container->PlatformChildCount());
  BrowserAccessibility* button = button_container->PlatformGetChild(0);
  ASSERT_EQ(0U, button->PlatformChildCount());

  ASSERT_EQ(base::ASCIIToUTF16("'One', 'two', 'three'"),
            TraverseNodeAtGranularity(para, GRANULARITY_WORD));
  ASSERT_EQ(base::ASCIIToUTF16("'Seven', 'eight', 'nine'"),
            TraverseNodeAtGranularity(button, GRANULARITY_WORD));
}

IN_PROC_BROWSER_TEST_F(AndroidGranularityMovementBrowserTest,
                       NavigateByLine) {
  GURL url("data:text/html,"
           "<body>"
           "<pre>One,%0dtwo,%0dthree!</pre>"
           "</body>");
  BrowserAccessibility* root = LoadUrlAndGetAccessibilityRoot(url);
  ASSERT_EQ(1U, root->PlatformChildCount());
  BrowserAccessibility* pre = root->PlatformGetChild(0);
  ASSERT_EQ(0U, pre->PlatformChildCount());

  ASSERT_EQ(base::ASCIIToUTF16("'One,', 'two,', 'three!'"),
            TraverseNodeAtGranularity(pre, GRANULARITY_LINE));
}

}  // namespace content
