// 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 <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "base/strings/to_string.h"
#include "base/test/bind.h"
#include "base/test/gmock_expected_support.h"
#include "base/types/expected.h"
#include "services/data_decoder/public/cpp/xml_dom.h"
#include "services/data_decoder/xml_parser.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using ::testing::Eq;
using ::testing::IsEmpty;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::Pointee;
using ::testing::UnorderedElementsAre;

namespace data_decoder::xml {

namespace {

class XmlParserRsTest : public testing::Test {
 protected:
  // Most test cases should call this method, which ensures that the Rust parser
  // and the legacy libxml2-based parser produce identical results.
  //
  // This rather confusing looking signature consists of two parts:
  // 1. The outer `base::expected` represents whether or not the legacy and Rust
  //    parser interpreted the XML the same way.
  // 2. The inner `base::expected` is the result from the Rust parser; in the
  //    case of invalid XML, this will contain a string that (theoretically)
  //    provides helpful additional context.
  base::expected<base::expected<Document, std::string>, std::string> ParseXml(
      std::string_view xml) {
    base::expected<Document, std::string> rust_result = ParseWithRust(xml);
    base::expected<base::Value, std::string> libxml2_result =
        ParseWithLibxml2(xml);

    if (rust_result.has_value() != libxml2_result.has_value()) {
      if (rust_result.has_value()) {
        return base::unexpected(
            "Rust parser succeeded, but libxml2 parser failed: " +
            libxml2_result.error());
      }
      return base::unexpected(
          "libxml2 parser succeeded, but Rust parser failed: " +
          rust_result.error());
    }

    if (rust_result.has_value()) {
      base::Value rust_result_as_value = rust_result->ToValueForTesting();
      if (rust_result_as_value != libxml2_result) {
        return base::unexpected(
            base::StrCat({"Mismatch for input: ", xml,
                          "\nRust result: ", rust_result_as_value.DebugString(),
                          "\nC++ result:  ", libxml2_result->DebugString()}));
      }
    }

    return rust_result;
  }

 private:
  base::expected<Document, std::string> ParseWithRust(std::string_view xml) {
    return Document::FromUtf8(xml);
  }

  base::expected<base::Value, std::string> ParseWithLibxml2(
      std::string_view xml) {
    XmlParser parser_impl;
    // `XmlParser`'s overrides for `mojom::XmlParser` are private so cheat to
    // make them visible.
    mojom::XmlParser& parser = parser_impl;

    std::optional<base::Value> value;
    std::optional<std::string> error;
    // This call does not actually go over Mojo, so the results will be
    // available synchronously.
    parser.Parse(std::string(xml),
                 mojom::XmlParser::WhitespaceBehavior::kIgnore,
                 base::BindLambdaForTesting(
                     [&](std::optional<base::Value> maybe_value,
                         const std::optional<std::string>& maybe_error) {
                       value = std::move(maybe_value);
                       error = std::move(maybe_error);
                     }));

    // The legacy C++ parser promises to only set exactly one of `cpp_value`
    // or `cpp_error`, so make sure that's the case.
    CHECK_NE(value.has_value(), error.has_value());

    if (value) {
      return *std::move(value);
    }
    return base::unexpected(*std::move(error));
  }
};

TEST_F(XmlParserRsTest, Basic) {
  ASSERT_OK_AND_ASSIGN(
      auto doc, ParseXml("<root><child attr=\"value\">text</child></root>"));
  ASSERT_OK(doc);
  const Node* root = doc->GetRoot();

  ASSERT_EQ(root->GetType(), Node::Type::kElement);
  EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));

  const auto* children = root->GetChildren();
  ASSERT_TRUE(children);
  ASSERT_EQ(children->size(), 1u);
  const Node* child = (*children)[0].get();
  ASSERT_EQ(child->GetType(), Node::Type::kElement);
  EXPECT_THAT(child->GetName(), Pointee(Name{"child"}));
  const auto* attr = child->GetAttribute(Name{"attr"});
  ASSERT_NE(attr, nullptr);
  EXPECT_EQ(*attr, "value");

  const auto* grandchildren = child->GetChildren();
  ASSERT_TRUE(grandchildren);
  ASSERT_EQ(grandchildren->size(), 1u);
  const Node* grandchild = (*grandchildren)[0].get();
  ASSERT_EQ(grandchild->GetType(), Node::Type::kText);
  EXPECT_EQ(grandchild->GetTextContent(), "text");
}

TEST_F(XmlParserRsTest, MultipleAttributes) {
  ASSERT_OK_AND_ASSIGN(auto doc,
                       ParseXml("<root a=\"1\" b=\"\" c=\"3\"></root>"));
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  EXPECT_THAT(
      root->GetAttributes(),
      Pointee(UnorderedElementsAre(Pair(Name{"a"}, "1"), Pair(Name{"b"}, ""),
                                   Pair(Name{"c"}, "3"))));
}

TEST_F(XmlParserRsTest, DuplicateAttributes) {
  // The same attribute may not be specified multiple times on the same
  // element.
  ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<root attr='1' attr='2'></root>"));
  EXPECT_FALSE(doc.has_value());
}

TEST_F(XmlParserRsTest, MixedSiblingTypes) {
  ASSERT_OK_AND_ASSIGN(auto doc,
                       ParseXml("<root><![CDATA[cdata]]><child/>text</root>"));
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  ASSERT_EQ(root->GetType(), Node::Type::kElement);
  EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));
  ASSERT_TRUE(root->GetChildren());
  ASSERT_EQ(root->GetChildren()->size(), 3u);

  ASSERT_EQ((*root->GetChildren())[0]->GetType(), Node::Type::kCdata);
  EXPECT_EQ((*root->GetChildren())[0]->GetTextContent(), "cdata");

  EXPECT_EQ((*root->GetChildren())[1]->GetType(), Node::Type::kElement);
  EXPECT_THAT((*root->GetChildren())[1]->GetName(), Pointee(Name{"child"}));

  ASSERT_EQ((*root->GetChildren())[2]->GetType(), Node::Type::kText);
  EXPECT_EQ((*root->GetChildren())[2]->GetTextContent(), "text");
}

TEST_F(XmlParserRsTest, OmitUnspecifiedDefaultNamespaceOnRoot) {
  // This is a test for backwards compatibility with the original
  // libxml2-based parser.
  ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<root />"));
  ASSERT_OK(doc);

  EXPECT_THAT(doc->GetRoot()->GetNamespaces(), Pointee(IsEmpty()));
}

TEST_F(XmlParserRsTest, BasicNamespaces) {
  ASSERT_OK_AND_ASSIGN(auto doc,
                       ParseXml("<foo:root "
                                "    xmlns='https://example.com/default'"
                                "    xmlns:foo='https://example.com/foo'"
                                "    xmlns:bar='https://example.com/bar'"
                                "    bar:attr='fizz' >"
                                "  <bar:child foo:attr='buzz' />"
                                "</foo:root>"));
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  EXPECT_THAT(root->GetName(),
              Pointee(Eq(Name{.local_name = "root", .prefix = "foo"})));
  EXPECT_THAT(root->GetAttributes(),
              Pointee(UnorderedElementsAre(
                  Pair(Name{.local_name = "attr", .prefix = "bar"}, "fizz"))));
  EXPECT_THAT(
      root->GetNamespaces(),
      Pointee(UnorderedElementsAre(Pair("", "https://example.com/default"),
                                   Pair("foo", "https://example.com/foo"),
                                   Pair("bar", "https://example.com/bar"))));

  ASSERT_TRUE(root->GetChildren());
  ASSERT_EQ(root->GetChildren()->size(), 1u);
  EXPECT_THAT((*root->GetChildren())[0]->GetName(),
              Pointee(Eq(Name{.local_name = "child", .prefix = "bar"})));
  EXPECT_THAT((*root->GetChildren())[0]->GetNamespaces(), Pointee(IsEmpty()));
  EXPECT_THAT((*root->GetChildren())[0]->GetAttributes(),
              Pointee(UnorderedElementsAre(
                  Pair(Name{.local_name = "attr", .prefix = "foo"}, "buzz"))));
}

TEST_F(XmlParserRsTest, OverrideNamespaceInChild) {
  // TODO(dcheng): This test reveals a behavior difference from the legacy
  // libxml2-based parser, so for now,  do not use the ParseXml() helper.
  auto doc = Document::FromUtf8(
      "<root xmlns:a='https://example.com/1'>"
      "  <child xmlns:a='https://example.com/2'>"
      "    <nested-child xmlns:a='https://example.com/2' />"
      "  </child>"
      "</root>");
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  EXPECT_THAT(
      root->GetNamespaces(),
      Pointee(UnorderedElementsAre(Pair("a", "https://example.com/1"))));

  const auto* children = root->GetChildren();
  ASSERT_TRUE(children);
  ASSERT_EQ(children->size(), 1u);
  EXPECT_THAT(
      (*children)[0]->GetNamespaces(),
      Pointee(UnorderedElementsAre(Pair("a", "https://example.com/2"))));

  // Even though `nested-child` explicitly declares the namespace, the
  // declaration is identical to the one inherited from its parent, so it will
  // be omitted.
  const auto* grandchildren = (*children)[0]->GetChildren();
  ASSERT_TRUE(grandchildren);
  ASSERT_EQ(grandchildren->size(), 1u);
  EXPECT_THAT((*grandchildren)[0]->GetNamespaces(), Pointee(IsEmpty()));
}

TEST_F(XmlParserRsTest, UndeclareDefaultNamespaceTest) {
  ASSERT_OK_AND_ASSIGN(auto doc,
                       ParseXml("<root xmlns='https://example.com'><child "
                                "xmlns=''></child></root>"));
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  EXPECT_THAT(root->GetNamespaces(),
              Pointee(UnorderedElementsAre(Pair("", "https://example.com"))));

  // While an unspecified default namespace on the root is omitted (see
  // `OmitUnspecifiedDefaultNamespaceOnRoot` above), it should be included on
  // a child element if it the net effect is to clear the default namespace
  // binding.
  ASSERT_TRUE(root->GetChildren());
  ASSERT_EQ(root->GetChildren()->size(), 1u);
  EXPECT_THAT((*root->GetChildren())[0]->GetNamespaces(),
              Pointee(UnorderedElementsAre(Pair("", ""))));
}

TEST_F(XmlParserRsTest, CdataNode) {
  ASSERT_OK_AND_ASSIGN(
      auto doc,
      ParseXml("<root><![CDATA[some unescaped & chars < >]]></root>"));
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  ASSERT_TRUE(root->GetChildren());
  ASSERT_EQ(root->GetChildren()->size(), 1u);

  EXPECT_EQ((*root->GetChildren())[0]->GetType(), Node::Type::kCdata);
  EXPECT_EQ((*root->GetChildren())[0]->GetTextContent(),
            "some unescaped & chars < >");
}

TEST_F(XmlParserRsTest, NestedSiblings) {
  ASSERT_OK_AND_ASSIGN(
      auto doc, ParseXml("<root><a><b></b></a><c><d><e></e></d></c></root>"));
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  ASSERT_EQ(root->GetType(), Node::Type::kElement);
  EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));
  ASSERT_TRUE(root->GetChildren());
  ASSERT_EQ(root->GetChildren()->size(), 2u);
}

TEST_F(XmlParserRsTest, CharacterEntities) {
  ASSERT_OK_AND_ASSIGN(
      auto doc, ParseXml("<root>&lt;tag&gt; &amp; &quot;quoted&quot;</root>"));
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  ASSERT_EQ(root->GetType(), Node::Type::kElement);
  EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));
  ASSERT_TRUE(root->GetChildren());
  ASSERT_EQ(root->GetChildren()->size(), 1u);

  EXPECT_EQ((*root->GetChildren())[0]->GetType(), Node::Type::kText);
  EXPECT_EQ((*root->GetChildren())[0]->GetTextContent(), "<tag> & \"quoted\"");
}

// Max depth is 200, so test nesting both at the limit and one over. This
// limit is specific to the Rust-based parser, so this test case does not use
// the `ParseXml()` helper.
TEST_F(XmlParserRsTest, MaxDepth) {
  constexpr int kMaxDepth = 200;
  {
    std::string input;
    for (int i = 0; i < kMaxDepth; ++i) {
      input += "<tag>";
    }
    for (int i = 0; i < kMaxDepth; ++i) {
      input += "</tag>";
    }

    auto doc = Document::FromUtf8(input);
    EXPECT_TRUE(doc.has_value());
  }

  {
    std::string input;
    for (int i = 0; i < kMaxDepth + 1; ++i) {
      input += "<tag>";
    }
    for (int i = 0; i < kMaxDepth + 1; ++i) {
      input += "</tag>";
    }

    auto doc = Document::FromUtf8(input);
    EXPECT_FALSE(doc.has_value());
  }
}

TEST_F(XmlParserRsTest, NoRootElement) {
  // XML documents must have a root element, so empty or all whitespace input
  // should fail to parse.
  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml(""));
    EXPECT_FALSE(doc.has_value());
  }

  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml(" "));
    EXPECT_FALSE(doc.has_value());
  }

  // The root element... must be an element.
  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<!DOCTYPE html>"));
    EXPECT_FALSE(doc.has_value());
  }

  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<?pi content ?>"));
    EXPECT_FALSE(doc.has_value());
  }

  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<!-- comment -->"));
    EXPECT_FALSE(doc.has_value());
  }
}

TEST_F(XmlParserRsTest, WhitespaceAroundRootElement) {
  ASSERT_OK_AND_ASSIGN(auto doc, ParseXml(" <root></root> "));
  EXPECT_TRUE(doc.has_value());
  const Node* root = doc->GetRoot();

  ASSERT_EQ(root->GetType(), Node::Type::kElement);
  EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));
}

TEST_F(XmlParserRsTest, MultipleRootElements) {
  // A document can only have one root element.
  ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<root></root><root2></root2>"));
  EXPECT_FALSE(doc.has_value());
}

TEST_F(XmlParserRsTest, TextAroundRootElement) {
  // Text nodes at the top-level are invalid in well-formed XML documents.
  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("text<root></root>"));
    EXPECT_FALSE(doc.has_value());
  }
  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<root></root>text"));
    EXPECT_FALSE(doc.has_value());
  }
}

TEST_F(XmlParserRsTest, CdataAroundRoot) {
  // Cdata nodes at the top-level are invalid in well-formed XML documents.
  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<![CDATA[cdata]]><root></root>"));
    EXPECT_FALSE(doc.has_value());
  }
  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<root></root><![CDATA[cdata]]>"));
    EXPECT_FALSE(doc.has_value());
  }
}

TEST_F(XmlParserRsTest, MismatchedTags) {
  {
    ASSERT_OK_AND_ASSIGN(auto doc, ParseXml("<root><child></root>"));
    EXPECT_FALSE(doc.has_value());
  }

  {
    ASSERT_OK_AND_ASSIGN(auto doc,
                         ParseXml("<root><child attr=\"value\"></child>"));
    EXPECT_FALSE(doc.has_value());
  }
}

TEST_F(XmlParserRsTest, DoctypeIgnored) {
  ASSERT_OK_AND_ASSIGN(auto doc,
                       ParseXml("<!DOCTYPE html><html><body /></html>"));
  ASSERT_OK(doc);

  const Node* root = doc->GetRoot();
  ASSERT_EQ(root->GetType(), Node::Type::kElement);
  EXPECT_THAT(root->GetLocalName(), Pointee(std::string("html")));
  ASSERT_TRUE(root->GetChildren());
  ASSERT_EQ(root->GetChildren()->size(), 1u);

  ASSERT_EQ((*root->GetChildren())[0]->GetType(), Node::Type::kElement);
  EXPECT_THAT((*root->GetChildren())[0]->GetLocalName(),
              Pointee(std::string("body")));
}

TEST_F(XmlParserRsTest, ProcessingInstructionsIgnored) {
  {
    // A top-level processing instruction should be ignored and the actual
    // root element should still be correctly set.
    ASSERT_OK_AND_ASSIGN(auto doc,
                         ParseXml("<?pi content?><root><child/></root>"));
    ASSERT_OK(doc);

    const Node* root = doc->GetRoot();
    ASSERT_EQ(root->GetType(), Node::Type::kElement);
    EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));
    ASSERT_TRUE(root->GetChildren());
    ASSERT_EQ(root->GetChildren()->size(), 1u);

    ASSERT_EQ((*root->GetChildren())[0]->GetType(), Node::Type::kElement);
    EXPECT_THAT((*root->GetChildren())[0]->GetName(), Pointee(Name{"child"}));
  }

  {
    ASSERT_OK_AND_ASSIGN(auto doc,
                         ParseXml("<root><?pi content?><child/></root>"));
    ASSERT_OK(doc);

    const Node* root = doc->GetRoot();
    ASSERT_EQ(root->GetType(), Node::Type::kElement);
    EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));
    ASSERT_TRUE(root->GetChildren());
    ASSERT_EQ(root->GetChildren()->size(), 1u);

    ASSERT_EQ((*root->GetChildren())[0]->GetType(), Node::Type::kElement);
    EXPECT_THAT((*root->GetChildren())[0]->GetName(), Pointee(Name{"child"}));
  }
}

TEST_F(XmlParserRsTest, CommentsIgnored) {
  {
    // A top-level comment should be ignored and the actual root element
    // should still be correctly set.
    ASSERT_OK_AND_ASSIGN(auto doc,
                         ParseXml("<!-- comment --><root><child/></root>"));
    ASSERT_OK(doc);

    const Node* root = doc->GetRoot();
    ASSERT_EQ(root->GetType(), Node::Type::kElement);
    EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));
    ASSERT_TRUE(root->GetChildren());
    ASSERT_EQ(root->GetChildren()->size(), 1u);

    ASSERT_EQ((*root->GetChildren())[0]->GetType(), Node::Type::kElement);
    EXPECT_THAT((*root->GetChildren())[0]->GetName(), Pointee(Name{"child"}));
  }

  {
    ASSERT_OK_AND_ASSIGN(auto doc,
                         ParseXml("<root><!-- comment --><child/></root>"));
    ASSERT_OK(doc);

    const Node* root = doc->GetRoot();
    ASSERT_EQ(root->GetType(), Node::Type::kElement);
    EXPECT_THAT(root->GetName(), Pointee(Name{"root"}));
    ASSERT_TRUE(root->GetChildren());
    ASSERT_EQ(root->GetChildren()->size(), 1u);

    ASSERT_EQ((*root->GetChildren())[0]->GetType(), Node::Type::kElement);
    EXPECT_THAT((*root->GetChildren())[0]->GetName(), Pointee(Name{"child"}));
  }
}

}  // namespace

}  // namespace data_decoder::xml
