# -*- coding: utf-8 -*-
# frozen_string_literal: true

require "helper"

module Nokogiri
  module XML
    class TestReader < Nokogiri::TestCase
      class NonStringIO
        def read(size)
          :invalid_object
        end
      end

      def test_io_non_string
        io = NonStringIO.new
        reader = Nokogiri::XML::Reader(io)
        assert_equal(io, reader.source)
      end

      def test_from_io_sets_io_as_source
        io = File.open(SNUGGLES_FILE)
        reader = Nokogiri::XML::Reader.from_io(io)
        assert_equal(io, reader.source)
      end

      def test_empty_element?
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
          <xml><city>Paris</city><state/></xml>
        eoxml

        results = reader.map do |node|
          if node.node_type == Nokogiri::XML::Node::ELEMENT_NODE
            node.empty_element?
          end
        end
        assert_equal([false, false, nil, nil, true, nil], results)
      end

      def test_self_closing?
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
          <xml><city>Paris</city><state/></xml>
        eoxml

        results = reader.map do |node|
          if node.node_type == Nokogiri::XML::Node::ELEMENT_NODE
            node.self_closing?
          end
        end
        assert_equal([false, false, nil, nil, true, nil], results)
      end

      # Issue #831
      # Make sure that the reader doesn't block reading the entire input
      def test_reader_blocking
        rd, wr = IO.pipe
        node_out = nil
        t = Thread.start do
          reader = Nokogiri::XML::Reader(rd, "UTF-8")
          reader.each do |node|
            node_out = node
            break
          end
          rd.close
        end
        sleep(0.1) # sleep to make sure the reader will actually block for input
        begin
          wr.puts "<foo>"
          wr.puts "<bar/>" * 10000
          wr.flush
        rescue Errno::EPIPE # rubocop:disable Lint/SuppressedException
        end
        res = t.join(1) # give the thread a moment to finish
        wr.close
        refute_nil(node_out, "Didn't read any nodes, exclude the trivial case")
        refute_nil(res, "Reader blocks trying to read the entire stream")
      end

      def test_reader_takes_block
        options = nil
        Nokogiri::XML::Reader(File.read(XML_FILE), XML_FILE) do |cfg|
          options = cfg
          options.nonet.nowarning.dtdattr
        end
        assert_predicate(options, :nonet?)
        assert_predicate(options, :nowarning?)
        assert_predicate(options, :dtdattr?)
      end

      def test_nil_raises
        assert_raises(ArgumentError) do
          Nokogiri::XML::Reader.from_memory(nil)
        end
        assert_raises(ArgumentError) do
          Nokogiri::XML::Reader.from_io(nil)
        end
      end

      def test_from_io
        io = File.open(SNUGGLES_FILE)
        reader = Nokogiri::XML::Reader.from_io(io)
        refute_predicate(reader, :default?)
        assert_equal(
          [false, false, false, false, false, false, false],
          reader.map(&:default?),
        )
      end

      def test_io
        io = File.open(SNUGGLES_FILE)
        reader = Nokogiri::XML::Reader(io)
        refute_predicate(reader, :default?)
        assert_equal(
          [false, false, false, false, false, false, false],
          reader.map(&:default?),
        )
      end

      def test_string_io
        io = StringIO.new(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        reader = Nokogiri::XML::Reader(io)
        refute_predicate(reader, :default?)
        assert_equal(
          [false, false, false, false, false, false, false],
          reader.map(&:default?),
        )
      end

      class ReallyBadIO
        def read(size)
          "a" * size**10
        end
      end

      class ReallyBadIO4Java
        def read(size = 1)
          "a" * size**10
        end
      end

      def test_io_that_reads_too_much
        refute_raises do
          io = if Nokogiri.jruby?
            ReallyBadIO4Java.new
          else
            ReallyBadIO.new
          end
          Nokogiri::XML::Reader(io)
        end
      end

      def test_in_memory
        assert(Nokogiri::XML::Reader(<<-eoxml))
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
      end

      def test_reader_holds_on_to_string
        xml = <<-eoxml
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        reader = Nokogiri::XML::Reader(xml)
        assert_equal(xml, reader.source)
      end

      def test_default?
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        refute_predicate(reader, :default?)
        assert_equal(
          [false, false, false, false, false, false, false],
          reader.map(&:default?),
        )
      end

      def test_value?
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        refute_predicate(reader, :value?)
        assert_equal(
          [false, true, false, true, false, true, false],
          reader.map(&:value?),
        )
      end

      def test_read_error_document
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
          <foo>
        </x>
        eoxml
        assert_raises(Nokogiri::XML::SyntaxError) do
          reader.each { |node| }
        end
        assert_equal(1, reader.errors.length)
      end

      def test_errors_is_an_array
        reader = Nokogiri::XML::Reader(StringIO.new("&bogus;"))
        assert_raises(SyntaxError) do
          reader.read
        end
        assert_equal([SyntaxError], reader.errors.map(&:class))
      end

      def test_pushing_to_non_array_raises_TypeError
        skip_unless_libxml2("TODO: JRuby ext does not internally call `errors`")
        reader = Nokogiri::XML::Reader(StringIO.new("&bogus;"))
        def reader.errors
          1
        end
        assert_raises(TypeError) do
          reader.read
        end
      end

      def test_attributes?
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        refute_predicate(reader, :attributes?)
        assert_equal(
          [true, false, true, false, true, false, true],
          reader.map(&:attributes?),
        )
      end

      def test_reader_attributes
        reader = Nokogiri::XML::Reader.from_memory(<<~XML)
          <x xmlns:tenderlove='http://tenderlovemaking.com/' xmlns='http://mothership.connection.com/'>
            <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
          </x>
        XML
        assert_empty(reader.attributes)
        assert_equal(
          [
            {
              "xmlns:tenderlove" => "http://tenderlovemaking.com/",
              "xmlns" => "http://mothership.connection.com/",
            },
            {},
            { "awesome" => "true" },
            {},
            { "awesome" => "true" },
            {},
            {
              "xmlns:tenderlove" => "http://tenderlovemaking.com/",
              "xmlns" => "http://mothership.connection.com/",
            },
          ],
          reader.map(&:attributes),
        )
      end

      def test_reader_attributes_hash
        reader = Nokogiri::XML::Reader.from_memory(<<~XML)
          <x xmlns:tenderlove='http://tenderlovemaking.com/' xmlns='http://mothership.connection.com/'>
            <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
          </x>
        XML
        assert_empty(reader.attribute_hash)
        assert_equal(
          [
            {},
            {},
            { "awesome" => "true" },
            {},
            { "awesome" => "true" },
            {},
            {},
          ],
          reader.map(&:attribute_hash),
        )
      end

      def test_reader_namespaces
        reader = Nokogiri::XML::Reader.from_memory(<<~XML)
          <x xmlns:tenderlove='http://tenderlovemaking.com/' xmlns='http://mothership.connection.com/'>
            <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
          </x>
        XML
        assert_empty(reader.namespaces)
        assert_equal(
          [
            {
              "xmlns:tenderlove" => "http://tenderlovemaking.com/",
              "xmlns" => "http://mothership.connection.com/",
            },
            {},
            {},
            {},
            {},
            {},
            {
              "xmlns:tenderlove" => "http://tenderlovemaking.com/",
              "xmlns" => "http://mothership.connection.com/",
            },
          ],
          reader.map(&:namespaces),
        )
      end

      def test_attribute_roundtrip
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'
           xmlns='http://mothership.connection.com/'
           >
          <tenderlove:foo awesome='true' size='giant'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        reader.each do |node|
          node.attributes.each do |key, value|
            assert_equal(value, node.attribute(key))
          end
        end
      end

      def test_attribute_at
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        assert_nil(reader.attribute_at(nil))
        assert_nil(reader.attribute_at(0))
        assert_equal(
          ["http://tenderlovemaking.com/", nil, "true", nil, "true", nil, "http://tenderlovemaking.com/"],
          reader.map { |x| x.attribute_at(0) },
        )
      end

      def test_attribute
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        assert_nil(reader.attribute(nil))
        assert_nil(reader.attribute("awesome"))
        assert_equal(
          [nil, nil, "true", nil, "true", nil, nil],
          reader.map { |x| x.attribute("awesome") },
        )
      end

      def test_attribute_length
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo awesome='true'>snuggles!</tenderlove:foo>
        </x>
        eoxml
        assert_equal(0, reader.attribute_count)
        assert_equal([1, 0, 1, 0, 0, 0, 0], reader.map(&:attribute_count))
      end

      def test_depth
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo>snuggles!</tenderlove:foo>
        </x>
        eoxml
        assert_equal(0, reader.depth)
        assert_equal([0, 1, 1, 2, 1, 1, 0], reader.map(&:depth))
      end

      def test_encoding
        string = <<-eoxml
        <awesome>
          <p xml:lang="en">The quick brown fox jumps over the lazy dog.</p>
          <p xml:lang="ja">日本語が上手です</p>
        </awesome>
        eoxml
        reader = Nokogiri::XML::Reader.from_memory(string, nil, "UTF-8")
        assert_equal(["UTF-8"], reader.map(&:encoding).uniq)
      end

      def test_xml_version
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo>snuggles!</tenderlove:foo>
        </x>
        eoxml
        assert_nil(reader.xml_version)
        assert_equal(["1.0"], reader.map(&:xml_version).uniq)
      end

      def test_lang
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <awesome>
          <p xml:lang="en">The quick brown fox jumps over the lazy dog.</p>
          <p xml:lang="ja">日本語が上手です</p>
        </awesome>
        eoxml
        assert_nil(reader.lang)
        assert_equal(
          [nil, nil, "en", "en", "en", nil, "ja", "ja", "ja", nil, nil],
          reader.map(&:lang),
        )
      end

      def test_value
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:tenderlove='http://tenderlovemaking.com/'>
          <tenderlove:foo>snuggles!</tenderlove:foo>
        </x>
        eoxml
        assert_nil(reader.value)
        assert_equal(
          [nil, "\n          ", nil, "snuggles!", nil, "\n        ", nil],
          reader.map(&:value),
        )
      end

      def test_prefix
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:edi='http://ecommerce.example.org/schema'>
          <edi:foo>hello</edi:foo>
        </x>
        eoxml
        assert_nil(reader.prefix)
        assert_equal(
          [nil, nil, "edi", nil, "edi", nil, nil],
          reader.map(&:prefix),
        )
      end

      def test_node_type
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x>
          <y>hello</y>
        </x>
        eoxml
        assert_equal(0, reader.node_type)
        assert_equal([1, 14, 1, 3, 15, 14, 15], reader.map(&:node_type))
      end

      def test_inner_xml
        str = "<x><y>hello &amp; goodbye</y></x>"
        reader = Nokogiri::XML::Reader.from_memory(str)

        reader.read

        assert_equal("<y>hello &amp; goodbye</y>", reader.inner_xml)
      end

      def test_outer_xml
        str = [
          "<x><y>hello &amp; goodbye</y></x>",
          "<y>hello &amp; goodbye</y>",
          "hello &amp; goodbye",
          "<y/>",
          "<x/>",
        ]
        if Nokogiri.jruby?
          # jruby will split the string up into three nodes, shrug.
          str.delete_at(2)
          str.insert(2, "hello ", "&amp;", " goodbye")
        end
        reader = Nokogiri::XML::Reader.from_memory(str.first)

        xml = []
        reader.map { |node| xml << node.outer_xml }

        assert_equal(str, xml)
      end

      def test_outer_xml_with_empty_nodes
        str = ["<x><y/></x>", "<y/>", "<x/>"]
        reader = Nokogiri::XML::Reader.from_memory(str.first)

        xml = []
        reader.map { |node| xml << node.outer_xml }

        assert_equal(str, xml)
      end

      def test_state
        reader = Nokogiri::XML::Reader.from_memory("<foo>bar</bar>")
        assert(reader.state)
      end

      def test_ns_uri
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:edi='http://ecommerce.example.org/schema'>
          <edi:foo>hello</edi:foo>
        </x>
        eoxml
        assert_nil(reader.namespace_uri)
        assert_equal(
          [
            nil,
            nil,
            "http://ecommerce.example.org/schema",
            nil,
            "http://ecommerce.example.org/schema",
            nil,
            nil,
          ],
          reader.map(&:namespace_uri),
        )
      end

      def test_local_name
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:edi='http://ecommerce.example.org/schema'>
          <edi:foo>hello</edi:foo>
        </x>
        eoxml
        assert_nil(reader.local_name)
        assert_equal(
          ["x", "#text", "foo", "#text", "foo", "#text", "x"],
          reader.map(&:local_name),
        )
      end

      def test_name
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
        <x xmlns:edi='http://ecommerce.example.org/schema'>
          <edi:foo>hello</edi:foo>
        </x>
        eoxml
        assert_nil(reader.name)
        assert_equal(
          ["x", "#text", "edi:foo", "#text", "edi:foo", "#text", "x"],
          reader.map(&:name),
        )
      end

      def test_base_uri
        reader = Nokogiri::XML::Reader.from_memory(<<-eoxml)
          <x xml:base="http://base.example.org/base/">
            <link href="link"/>
            <other xml:base="http://other.example.org/"/>
            <relative xml:base="relative">
              <link href="stuff" />
            </relative>
          </x>
        eoxml

        assert_nil(reader.base_uri)
        assert_equal(
          [
            "http://base.example.org/base/",
            "http://base.example.org/base/",
            "http://base.example.org/base/",
            "http://base.example.org/base/",
            "http://other.example.org/",
            "http://base.example.org/base/",
            "http://base.example.org/base/relative",
            "http://base.example.org/base/relative",
            "http://base.example.org/base/relative",
            "http://base.example.org/base/relative",
            "http://base.example.org/base/relative",
            "http://base.example.org/base/",
            "http://base.example.org/base/",
          ],
          reader.map(&:base_uri),
        )
      end

      def test_xlink_href_without_base_uri
        reader = Nokogiri::XML::Reader(<<-eoxml)
          <x xmlns:xlink="http://www.w3.org/1999/xlink">
            <link xlink:href="#other">Link</link>
            <other id="other">Linked Element</other>
          </x>
        eoxml

        reader.each do |node|
          next unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT

          if node.name == "link"
            assert_nil(node.base_uri)
          end
        end
      end

      def test_xlink_href_with_base_uri
        reader = Nokogiri::XML::Reader(<<-eoxml)
          <x xml:base="http://base.example.org/base/"
             xmlns:xlink="http://www.w3.org/1999/xlink">
            <link xlink:href="#other">Link</link>
            <other id="other">Linked Element</other>
          </x>
        eoxml

        reader.each do |node|
          if node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT
            assert_equal("http://base.example.org/base/", node.base_uri)
          end
        end
      end

      def test_read_from_memory
        called = false
        reader = Nokogiri::XML::Reader.from_memory("<foo>bar</foo>")
        reader.each do |node|
          called = true
          assert(node)
        end
        assert(called)
      end

      def test_large_document_smoke_test
        skip_unless_libxml2("valgrind tests should only run with libxml2")

        refute_valgrind_errors do
          xml = []
          xml << "<elements>"
          10000.times { |j| xml << "<element id=\"#{j}\"/>" }
          xml << "</elements>"
          xml = xml.join("\n")

          Nokogiri::XML::Reader.from_memory(xml).each(&:attributes)
        end
      end

      def test_correct_outer_xml_inclusion
        xml = Nokogiri::XML::Reader.from_io(StringIO.new(<<-eoxml))
          <root-element>
            <children>
              <child n="1">
                <field>child-1</field>
              </child>
              <child n="2">
                <field>child-2</field>
              </child>
              <child n="3">
                <field>child-3</field>
              </child>
            </children>
          </root-element>
        eoxml

        nodelengths = []
        has_child2 = []

        xml.each do |node|
          if (node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT) && (node.name == "child")
            nodelengths << node.outer_xml.length
            has_child2 << !!node.outer_xml.include?("child-2")
          end
        end

        assert_equal(nodelengths[0], nodelengths[1])
        assert(has_child2[1])
        refute(has_child2[0])
      end

      def test_correct_inner_xml_inclusion
        xml = Nokogiri::XML::Reader.from_io(StringIO.new(<<-eoxml))
          <root-element>
            <children>
              <child n="1">
                <field>child-1</field>
              </child>
              <child n="2">
                <field>child-2</field>
              </child>
              <child n="3">
                <field>child-3</field>
              </child>
            </children>
          </root-element>
        eoxml

        nodelengths = []
        has_child2 = []

        xml.each do |node|
          if (node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT) && (node.name == "child")
            nodelengths << node.inner_xml.length
            has_child2 << !!node.inner_xml.include?("child-2")
          end
        end

        assert_equal(nodelengths[0], nodelengths[1])
        assert(has_child2[1])
        refute(has_child2[0])
      end

      def test_nonexistent_attribute
        require "nokogiri"
        reader = Nokogiri::XML::Reader("<root xmlns='bob'><el attr='fred' /></root>")
        reader.read # root
        reader.read # el
        assert_nil(reader.attribute("other"))
      end

      def test_broken_markup_attribute_hash
        xml = <<~XML
          <root><foo bar="asdf" xmlns:quux="qwer">#{"x" * 1024}
        XML
        reader = Nokogiri::XML::Reader(xml)
        reader.read # root
        reader.read # foo

        assert_equal("foo", reader.name)
        assert_equal(0, reader.errors.length)

        if Nokogiri.jruby?
          assert_equal({ "bar" => "asdf" }, reader.attribute_hash)

          reader.read # text (xxxxx)
          reader.read # text (newline)

          e = assert_raises(Nokogiri::XML::SyntaxError) do
            reader.read
          end
          assert_includes(e.message, "XML document structures must start and end")
        else
          e = assert_raises(Nokogiri::XML::SyntaxError) do
            reader.attribute_hash
          end
          expected = if Nokogiri.uses_libxml?(">= 2.12.0") # upstream commit 53050b1d
            "FATAL: Premature end of data in tag foo line 1"
          else
            "FATAL: Extra content at the end of the document"
          end
          assert_includes(e.message, expected)
        end

        assert_equal(1, reader.errors.length)
      end

      def test_broken_markup_namespaces
        xml = <<~XML
          <root><foo bar="asdf" xmlns:quux="qwer">#{"x" * 1024}
        XML
        reader = Nokogiri::XML::Reader(xml)
        reader.read # root
        reader.read # foo

        assert_equal("foo", reader.name)
        assert_equal(0, reader.errors.length)

        if Nokogiri.jruby?
          assert_equal({ "xmlns:quux" => "qwer" }, reader.namespaces)

          reader.read # text (xxxxx)
          reader.read # text (newline)

          e = assert_raises(Nokogiri::XML::SyntaxError) do
            reader.read
          end
          assert_includes(e.message, "XML document structures must start and end")
        else
          e = assert_raises(Nokogiri::XML::SyntaxError) do
            reader.namespaces
          end
          expected = if Nokogiri.uses_libxml?(">= 2.12.0") # upstream commit 53050b1d
            "FATAL: Premature end of data in tag foo line 1"
          else
            "FATAL: Extra content at the end of the document"
          end
          assert_includes(e.message, expected)
        end

        assert_equal(1, reader.errors.length)
      end
    end
  end
end
