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

#include "third_party/blink/renderer/core/dom/static_range.h"

#include "base/memory/scoped_refptr.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_static_range_init.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/node_list.h"
#include "third_party/blink/renderer/core/dom/range.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/html/html_body_element.h"
#include "third_party/blink/renderer/core/html/html_document.h"
#include "third_party/blink/renderer/core/html/html_element.h"
#include "third_party/blink/renderer/core/html/html_html_element.h"
#include "third_party/blink/renderer/core/testing/null_execution_context.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"

namespace blink {

class StaticRangeTest : public testing::Test {
 protected:
  void SetUp() override;

  HTMLDocument& GetDocument() const;

 private:
  test::TaskEnvironment task_environment_;
  ScopedNullExecutionContext execution_context_;
  Persistent<HTMLDocument> document_;
};

void StaticRangeTest::SetUp() {
  document_ =
      HTMLDocument::CreateForTest(execution_context_.GetExecutionContext());
  auto* html = MakeGarbageCollected<HTMLHtmlElement>(*document_);
  html->AppendChild(MakeGarbageCollected<HTMLBodyElement>(*document_));
  document_->AppendChild(html);
}

HTMLDocument& StaticRangeTest::GetDocument() const {
  return *document_;
}

TEST_F(StaticRangeTest, SplitTextNodeRangeWithinText) {
  V8TestingScope scope;
  GetDocument().body()->SetInnerHTMLWithoutTrustedTypes("1234");
  auto* old_text = To<Text>(GetDocument().body()->firstChild());

  auto* static_range04 =
      MakeGarbageCollected<StaticRange>(old_text, 0u, old_text, 4u);
  auto* static_range02 =
      MakeGarbageCollected<StaticRange>(old_text, 0u, old_text, 2u);
  auto* static_range22 =
      MakeGarbageCollected<StaticRange>(old_text, 2u, old_text, 2u);
  auto* static_range24 =
      MakeGarbageCollected<StaticRange>(old_text, 2u, old_text, 4u);

  Range* range04 = static_range04->toRange(ASSERT_NO_EXCEPTION);
  Range* range02 = static_range02->toRange(ASSERT_NO_EXCEPTION);
  Range* range22 = static_range22->toRange(ASSERT_NO_EXCEPTION);
  Range* range24 = static_range24->toRange(ASSERT_NO_EXCEPTION);

  old_text->splitText(2, ASSERT_NO_EXCEPTION);
  auto* new_text = To<Text>(old_text->nextSibling());

  // Range should mutate.
  EXPECT_TRUE(range04->BoundaryPointsValid());
  EXPECT_EQ(old_text, range04->startContainer());
  EXPECT_EQ(0u, range04->startOffset());
  EXPECT_EQ(new_text, range04->endContainer());
  EXPECT_EQ(2u, range04->endOffset());

  EXPECT_TRUE(range02->BoundaryPointsValid());
  EXPECT_EQ(old_text, range02->startContainer());
  EXPECT_EQ(0u, range02->startOffset());
  EXPECT_EQ(old_text, range02->endContainer());
  EXPECT_EQ(2u, range02->endOffset());

  // Our implementation always moves the boundary point at the separation point
  // to the end of the original text node.
  EXPECT_TRUE(range22->BoundaryPointsValid());
  EXPECT_EQ(old_text, range22->startContainer());
  EXPECT_EQ(2u, range22->startOffset());
  EXPECT_EQ(old_text, range22->endContainer());
  EXPECT_EQ(2u, range22->endOffset());

  EXPECT_TRUE(range24->BoundaryPointsValid());
  EXPECT_EQ(old_text, range24->startContainer());
  EXPECT_EQ(2u, range24->startOffset());
  EXPECT_EQ(new_text, range24->endContainer());
  EXPECT_EQ(2u, range24->endOffset());

  // StaticRange shouldn't mutate.
  EXPECT_EQ(old_text, static_range04->startContainer());
  EXPECT_EQ(0u, static_range04->startOffset());
  EXPECT_EQ(old_text, static_range04->endContainer());
  EXPECT_EQ(4u, static_range04->endOffset());

  EXPECT_EQ(old_text, static_range02->startContainer());
  EXPECT_EQ(0u, static_range02->startOffset());
  EXPECT_EQ(old_text, static_range02->endContainer());
  EXPECT_EQ(2u, static_range02->endOffset());

  EXPECT_EQ(old_text, static_range22->startContainer());
  EXPECT_EQ(2u, static_range22->startOffset());
  EXPECT_EQ(old_text, static_range22->endContainer());
  EXPECT_EQ(2u, static_range22->endOffset());

  EXPECT_EQ(old_text, static_range24->startContainer());
  EXPECT_EQ(2u, static_range24->startOffset());
  EXPECT_EQ(old_text, static_range24->endContainer());
  EXPECT_EQ(4u, static_range24->endOffset());
}

TEST_F(StaticRangeTest, SplitTextNodeRangeOutsideText) {
  V8TestingScope scope;
  GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(
      "<span id=\"outer\">0<span id=\"inner-left\">1</span>SPLITME<span "
      "id=\"inner-right\">2</span>3</span>");

  Element* outer =
      GetDocument().getElementById(AtomicString::FromUtf8("outer"));
  Element* inner_left =
      GetDocument().getElementById(AtomicString::FromUtf8("inner-left"));
  Element* inner_right =
      GetDocument().getElementById(AtomicString::FromUtf8("inner-right"));
  auto* old_text = To<Text>(outer->childNodes()->item(2));

  auto* static_range_outer_outside =
      MakeGarbageCollected<StaticRange>(outer, 0u, outer, 5u);
  auto* static_range_outer_inside =
      MakeGarbageCollected<StaticRange>(outer, 1u, outer, 4u);
  auto* static_range_outer_surrounding_text =
      MakeGarbageCollected<StaticRange>(outer, 2u, outer, 3u);
  auto* static_range_inner_left =
      MakeGarbageCollected<StaticRange>(inner_left, 0u, inner_left, 1u);
  auto* static_range_inner_right =
      MakeGarbageCollected<StaticRange>(inner_right, 0u, inner_right, 1u);
  auto* static_range_from_text_to_middle_of_element =
      MakeGarbageCollected<StaticRange>(old_text, 6u, outer, 3u);

  Range* range_outer_outside =
      static_range_outer_outside->toRange(ASSERT_NO_EXCEPTION);
  Range* range_outer_inside =
      static_range_outer_inside->toRange(ASSERT_NO_EXCEPTION);
  Range* range_outer_surrounding_text =
      static_range_outer_surrounding_text->toRange(ASSERT_NO_EXCEPTION);
  Range* range_inner_left =
      static_range_inner_left->toRange(ASSERT_NO_EXCEPTION);
  Range* range_inner_right =
      static_range_inner_right->toRange(ASSERT_NO_EXCEPTION);
  Range* range_from_text_to_middle_of_element =
      static_range_from_text_to_middle_of_element->toRange(ASSERT_NO_EXCEPTION);

  old_text->splitText(3, ASSERT_NO_EXCEPTION);
  auto* new_text = To<Text>(old_text->nextSibling());

  // Range should mutate.
  EXPECT_TRUE(range_outer_outside->BoundaryPointsValid());
  EXPECT_EQ(outer, range_outer_outside->startContainer());
  EXPECT_EQ(0u, range_outer_outside->startOffset());
  EXPECT_EQ(outer, range_outer_outside->endContainer());
  EXPECT_EQ(6u,
            range_outer_outside
                ->endOffset());  // Increased by 1 since a new node is inserted.

  EXPECT_TRUE(range_outer_inside->BoundaryPointsValid());
  EXPECT_EQ(outer, range_outer_inside->startContainer());
  EXPECT_EQ(1u, range_outer_inside->startOffset());
  EXPECT_EQ(outer, range_outer_inside->endContainer());
  EXPECT_EQ(5u, range_outer_inside->endOffset());

  EXPECT_TRUE(range_outer_surrounding_text->BoundaryPointsValid());
  EXPECT_EQ(outer, range_outer_surrounding_text->startContainer());
  EXPECT_EQ(2u, range_outer_surrounding_text->startOffset());
  EXPECT_EQ(outer, range_outer_surrounding_text->endContainer());
  EXPECT_EQ(4u, range_outer_surrounding_text->endOffset());

  EXPECT_TRUE(range_inner_left->BoundaryPointsValid());
  EXPECT_EQ(inner_left, range_inner_left->startContainer());
  EXPECT_EQ(0u, range_inner_left->startOffset());
  EXPECT_EQ(inner_left, range_inner_left->endContainer());
  EXPECT_EQ(1u, range_inner_left->endOffset());

  EXPECT_TRUE(range_inner_right->BoundaryPointsValid());
  EXPECT_EQ(inner_right, range_inner_right->startContainer());
  EXPECT_EQ(0u, range_inner_right->startOffset());
  EXPECT_EQ(inner_right, range_inner_right->endContainer());
  EXPECT_EQ(1u, range_inner_right->endOffset());

  EXPECT_TRUE(range_from_text_to_middle_of_element->BoundaryPointsValid());
  EXPECT_EQ(new_text, range_from_text_to_middle_of_element->startContainer());
  EXPECT_EQ(3u, range_from_text_to_middle_of_element->startOffset());
  EXPECT_EQ(outer, range_from_text_to_middle_of_element->endContainer());
  EXPECT_EQ(4u, range_from_text_to_middle_of_element->endOffset());

  // StaticRange shouldn't mutate.
  EXPECT_EQ(outer, static_range_outer_outside->startContainer());
  EXPECT_EQ(0u, static_range_outer_outside->startOffset());
  EXPECT_EQ(outer, static_range_outer_outside->endContainer());
  EXPECT_EQ(5u, static_range_outer_outside->endOffset());

  EXPECT_EQ(outer, static_range_outer_inside->startContainer());
  EXPECT_EQ(1u, static_range_outer_inside->startOffset());
  EXPECT_EQ(outer, static_range_outer_inside->endContainer());
  EXPECT_EQ(4u, static_range_outer_inside->endOffset());

  EXPECT_EQ(outer, static_range_outer_surrounding_text->startContainer());
  EXPECT_EQ(2u, static_range_outer_surrounding_text->startOffset());
  EXPECT_EQ(outer, static_range_outer_surrounding_text->endContainer());
  EXPECT_EQ(3u, static_range_outer_surrounding_text->endOffset());

  EXPECT_EQ(inner_left, static_range_inner_left->startContainer());
  EXPECT_EQ(0u, static_range_inner_left->startOffset());
  EXPECT_EQ(inner_left, static_range_inner_left->endContainer());
  EXPECT_EQ(1u, static_range_inner_left->endOffset());

  EXPECT_EQ(inner_right, static_range_inner_right->startContainer());
  EXPECT_EQ(0u, static_range_inner_right->startOffset());
  EXPECT_EQ(inner_right, static_range_inner_right->endContainer());
  EXPECT_EQ(1u, static_range_inner_right->endOffset());

  EXPECT_EQ(old_text,
            static_range_from_text_to_middle_of_element->startContainer());
  EXPECT_EQ(6u, static_range_from_text_to_middle_of_element->startOffset());
  EXPECT_EQ(outer, static_range_from_text_to_middle_of_element->endContainer());
  EXPECT_EQ(3u, static_range_from_text_to_middle_of_element->endOffset());
}

TEST_F(StaticRangeTest, InvalidToRange) {
  V8TestingScope scope;
  GetDocument().body()->SetInnerHTMLWithoutTrustedTypes("1234");
  auto* old_text = To<Text>(GetDocument().body()->firstChild());

  auto* static_range04 =
      MakeGarbageCollected<StaticRange>(old_text, 0u, old_text, 4u);

  // Valid StaticRange.
  static_range04->toRange(ASSERT_NO_EXCEPTION);

  old_text->splitText(2, ASSERT_NO_EXCEPTION);
  // StaticRange shouldn't mutate, endOffset() become invalid after splitText().
  EXPECT_EQ(old_text, static_range04->startContainer());
  EXPECT_EQ(0u, static_range04->startOffset());
  EXPECT_EQ(old_text, static_range04->endContainer());
  EXPECT_EQ(4u, static_range04->endOffset());

  // Invalid StaticRange.
  DummyExceptionStateForTesting exception_state;
  static_range04->toRange(exception_state);
  EXPECT_TRUE(exception_state.HadException());
}

TEST_F(StaticRangeTest, OwnerDocumentMatchesStartContainerDocument) {
  V8TestingScope scope;
  // StaticRange has no specification-defined owner document. Blink derives
  // OwnerDocument() from the current start container so cross-realm consumers
  // such as |HighlightRegistry::ValidateHighlightMarkers| do not reject a
  // range after its endpoints move to the document being painted.
  // See https://issues.chromium.org/issues/407812149.
  ScopedNullExecutionContext other_execution_context;
  Persistent<HTMLDocument> other_document = HTMLDocument::CreateForTest(
      other_execution_context.GetExecutionContext());
  auto* other_html = MakeGarbageCollected<HTMLHtmlElement>(*other_document);
  other_html->AppendChild(
      MakeGarbageCollected<HTMLBodyElement>(*other_document));
  other_document->AppendChild(other_html);
  other_document->body()->SetInnerHTMLWithoutTrustedTypes("hello");
  auto* other_text = To<Text>(other_document->body()->firstChild());

  auto* init = MakeGarbageCollected<StaticRangeInit>();
  init->setStartContainer(other_text);
  init->setStartOffset(0u);
  init->setEndContainer(other_text);
  init->setEndOffset(5u);

  auto* static_range = StaticRange::Create(init, ASSERT_NO_EXCEPTION);
  ASSERT_TRUE(static_range);
  EXPECT_EQ(&static_range->OwnerDocument(), other_document.Get())
      << "StaticRange::OwnerDocument() must follow the start container's "
         "document, not the document associated with the calling realm.";
  EXPECT_TRUE(static_range->IsValid());

  // Sanity: the start/end containers and offsets round-trip unchanged.
  EXPECT_EQ(other_text, static_range->startContainer());
  EXPECT_EQ(0u, static_range->startOffset());
  EXPECT_EQ(other_text, static_range->endContainer());
  EXPECT_EQ(5u, static_range->endOffset());
}

TEST_F(StaticRangeTest,
       IsInvalidWhenStartAndEndContainersAreInDifferentDocuments) {
  V8TestingScope scope;
  // A StaticRange whose start and end containers live in different documents
  // is constructible, but |IsValid()| must return false because the two
  // containers have different roots. Downstream consumers such as
  // |HighlightRegistry::ValidateHighlightMarkers| check |IsValid()| via
  // |IsAbstractRangePaintable| and silently drop such ranges, so we lock the
  // behavior in here.
  GetDocument().body()->SetInnerHTMLWithoutTrustedTypes("hello");
  auto* main_text = To<Text>(GetDocument().body()->firstChild());

  ScopedNullExecutionContext other_execution_context;
  Persistent<HTMLDocument> other_document = HTMLDocument::CreateForTest(
      other_execution_context.GetExecutionContext());
  auto* other_html = MakeGarbageCollected<HTMLHtmlElement>(*other_document);
  other_html->AppendChild(
      MakeGarbageCollected<HTMLBodyElement>(*other_document));
  other_document->AppendChild(other_html);
  other_document->body()->SetInnerHTMLWithoutTrustedTypes("world");
  auto* other_text = To<Text>(other_document->body()->firstChild());

  auto* init = MakeGarbageCollected<StaticRangeInit>();
  init->setStartContainer(main_text);
  init->setStartOffset(0u);
  init->setEndContainer(other_text);
  init->setEndOffset(5u);

  auto* static_range = StaticRange::Create(init, ASSERT_NO_EXCEPTION);
  ASSERT_TRUE(static_range);
  // The owner document follows the start container per the IDL contract.
  EXPECT_EQ(&static_range->OwnerDocument(), &GetDocument());
  EXPECT_FALSE(static_range->IsValid())
      << "A StaticRange whose endpoints are in different documents must "
         "report IsValid() == false.";
}

TEST_F(StaticRangeTest, RevalidatesAfterEndpointAdoptionAndMutation) {
  GetDocument().body()->SetInnerHTMLWithoutTrustedTypes(
      "<span id='span'>temporary</span>");
  auto* span = GetDocument().getElementById(AtomicString("span"));
  auto* text = To<Text>(span->firstChild());
  auto* static_range = MakeGarbageCollected<StaticRange>(text, 0u, text, 9u);
  ASSERT_TRUE(static_range->IsValid());

  ScopedNullExecutionContext other_execution_context;
  Persistent<HTMLDocument> other_document = HTMLDocument::CreateForTest(
      other_execution_context.GetExecutionContext());
  auto* other_html = MakeGarbageCollected<HTMLHtmlElement>(*other_document);
  other_html->AppendChild(
      MakeGarbageCollected<HTMLBodyElement>(*other_document));
  other_document->AppendChild(other_html);
  other_document->adoptNode(span, ASSERT_NO_EXCEPTION);
  other_document->body()->AppendChild(span);

  EXPECT_EQ(&static_range->OwnerDocument(), other_document.Get());
  // Prime validity after adoption, when the original offsets are still valid.
  ASSERT_TRUE(static_range->IsValid());
  text->setData("x");
  EXPECT_FALSE(static_range->IsValid());
}

}  // namespace blink
