shared_examples_for "a parser" do |parser|
  before do
    MultiXml.parser = parser

    LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER) if parser == "LibXML"
  rescue LoadError
    pending "Parser #{parser} couldn't be loaded"
  end

  describe ".parse" do
    context "a blank string" do
      before do
        @xml = ""
      end

      it "returns an empty Hash" do
        expect(MultiXml.parse(@xml)).to eq({})
      end
    end

    context "a whitespace string" do
      before do
        @xml = " "
      end

      it "returns an empty Hash" do
        expect(MultiXml.parse(@xml)).to eq({})
      end
    end

    context "a frozen string" do
      before do
        @xml = " "
      end

      it "returns an empty Hash" do
        expect(MultiXml.parse(@xml)).to eq({})
      end
    end

    unless parser == "Oga"
      context "an invalid XML document" do
        before do
          @xml = "<open></close>"
        end

        it "raises MultiXml::ParseError" do
          expect { MultiXml.parse(@xml) }.to raise_error(MultiXml::ParseError)
        end
      end
    end

    context "a valid XML document" do
      before do
        @xml = "<user/>"
      end

      it "parses correctly" do
        expect(MultiXml.parse(@xml)).to eq("user" => nil)
      end

      context "with CDATA" do
        before do
          @xml = "<user><![CDATA[Erik Berlin]]></user>"
        end

        it "returns the correct CDATA" do
          expect(MultiXml.parse(@xml)["user"]).to eq("Erik Berlin")
        end
      end

      context "element with the same inner element and attribute name" do
        before do
          @xml = "<user name='John'><name>Smith</name></user>"
        end

        it "returns names as Array" do
          expect(MultiXml.parse(@xml)["user"]["name"]).to eq %w[John Smith]
        end
      end

      context "with content" do
        before do
          @xml = "<user>Erik Berlin</user>"
        end

        it "returns the correct content" do
          expect(MultiXml.parse(@xml)["user"]).to eq("Erik Berlin")
        end
      end

      context "with an attribute" do
        before do
          @xml = '<user name="Erik Berlin"/>'
        end

        it "returns the correct attribute" do
          expect(MultiXml.parse(@xml)["user"]["name"]).to eq("Erik Berlin")
        end
      end

      context "with multiple attributes" do
        before do
          @xml = '<user name="Erik Berlin" screen_name="sferik"/>'
        end

        it "returns the correct attributes" do
          expect(MultiXml.parse(@xml)["user"]["name"]).to eq("Erik Berlin")
          expect(MultiXml.parse(@xml)["user"]["screen_name"]).to eq("sferik")
        end
      end

      context "typecast management" do
        before do
          @xml = %(
            <global-settings>
              <group>
                <name>Settings</name>
                <setting type="string">
                  <description>Test</description>
                </setting>
              </group>
            </global-settings>
          )
        end

        context "with :typecast_xml_value => true" do
          before do
            @setting = MultiXml.parse(@xml)["global_settings"]["group"]["setting"]
          end

          it { expect(@setting).to eq "" }
        end

        context "with :typecast_xml_value => false" do
          before do
            @setting = MultiXml.parse(@xml, typecast_xml_value: false)["global_settings"]["group"]["setting"]
          end

          it { expect(@setting).to eq("type" => "string", "description" => {"__content__" => "Test"}) }
        end
      end

      context "with :symbolize_keys => true" do
        before do
          @xml = '<users><user name="Erik Berlin"/><user><name>Wynn Netherland</name></user></users>'
        end

        it "symbolizes keys" do
          expect(MultiXml.parse(@xml,
            symbolize_keys: true)).to eq(users: {user: [{name: "Erik Berlin"},
                                                        {name: "Wynn Netherland"}]})
        end
      end

      context 'with an attribute type="boolean"' do
        %w[true false].each do |boolean|
          context "when #{boolean}" do
            it "returns #{boolean}" do
              xml = "<tag type=\"boolean\">#{boolean}</tag>"
              expect(MultiXml.parse(xml)["tag"]).to be instance_eval(boolean)
            end
          end
        end

        context "when 1" do
          before do
            @xml = '<tag type="boolean">1</tag>'
          end

          it "returns true" do
            expect(MultiXml.parse(@xml)["tag"]).to be true
          end
        end

        context "when 0" do
          before do
            @xml = '<tag type="boolean">0</tag>'
          end

          it "returns false" do
            expect(MultiXml.parse(@xml)["tag"]).to be false
          end
        end
      end

      context 'with an attribute type="integer"' do
        context "with a positive integer" do
          before do
            @xml = '<tag type="integer">1</tag>'
          end

          it "returns a Integer" do
            expect(MultiXml.parse(@xml)["tag"]).to be_a(Integer)
          end

          it "returns a positive number" do
            expect(MultiXml.parse(@xml)["tag"]).to be > 0
          end

          it "returns the correct number" do
            expect(MultiXml.parse(@xml)["tag"]).to eq(1)
          end
        end

        context "with a negative integer" do
          before do
            @xml = '<tag type="integer">-1</tag>'
          end

          it "returns a Integer" do
            expect(MultiXml.parse(@xml)["tag"]).to be_a(Integer)
          end

          it "returns a negative number" do
            expect(MultiXml.parse(@xml)["tag"]).to be < 0
          end

          it "returns the correct number" do
            expect(MultiXml.parse(@xml)["tag"]).to eq(-1)
          end
        end
      end

      context 'with an attribute type="string"' do
        before do
          @xml = '<tag type="string"></tag>'
        end

        it "returns a String" do
          expect(MultiXml.parse(@xml)["tag"]).to be_a(String)
        end

        it "returns the correct string" do
          expect(MultiXml.parse(@xml)["tag"]).to eq("")
        end
      end

      context 'with an attribute type="date"' do
        before do
          @xml = '<tag type="date">1970-01-01</tag>'
        end

        it "returns a Date" do
          expect(MultiXml.parse(@xml)["tag"]).to be_a(Date)
        end

        it "returns the correct date" do
          expect(MultiXml.parse(@xml)["tag"]).to eq(Date.parse("1970-01-01"))
        end
      end

      context 'with an attribute type="datetime"' do
        before do
          @xml = '<tag type="datetime">1970-01-01 00:00</tag>'
        end

        it "returns a Time" do
          expect(MultiXml.parse(@xml)["tag"]).to be_a(Time)
        end

        it "returns the correct time" do
          expect(MultiXml.parse(@xml)["tag"]).to eq(Time.parse("1970-01-01 00:00"))
        end
      end

      context 'with an attribute type="dateTime"' do
        before do
          @xml = '<tag type="datetime">1970-01-01 00:00</tag>'
        end

        it "returns a Time" do
          expect(MultiXml.parse(@xml)["tag"]).to be_a(Time)
        end

        it "returns the correct time" do
          expect(MultiXml.parse(@xml)["tag"]).to eq(Time.parse("1970-01-01 00:00"))
        end
      end

      context 'with an attribute type="double"' do
        before do
          @xml = '<tag type="double">3.14159265358979</tag>'
        end

        it "returns a Float" do
          expect(MultiXml.parse(@xml)["tag"]).to be_a(Float)
        end

        it "returns the correct number" do
          expect(MultiXml.parse(@xml)["tag"]).to eq(3.14159265358979)
        end
      end

      context 'with an attribute type="decimal"' do
        before do
          @xml = '<tag type="decimal">3.14159265358979</tag>'
        end

        it "returns a BigDecimal" do
          expect(MultiXml.parse(@xml)["tag"]).to be_a(BigDecimal)
        end

        it "returns the correct number" do
          expect(MultiXml.parse(@xml)["tag"]).to eq(3.14159265358979)
        end
      end

      context 'with an attribute type="base64Binary"' do
        before do
          @xml = '<tag type="base64Binary">aW1hZ2UucG5n</tag>'
        end

        it "returns a String" do
          expect(MultiXml.parse(@xml)["tag"]).to be_a(String)
        end

        it "returns the correct string" do
          expect(MultiXml.parse(@xml)["tag"]).to eq("image.png")
        end
      end

      context 'with an attribute type="yaml"' do
        before do
          @xml = "<tag type=\"yaml\">--- \n1: returns an integer\n:message: Have a nice day\narray: \n- has-dashes: true\n  has_underscores: true\n</tag>"
        end

        it "raises MultiXML::DisallowedTypeError by default" do
          expect { MultiXml.parse(@xml)["tag"] }.to raise_error(MultiXml::DisallowedTypeError)
        end

        it "returns the correctly parsed YAML when the type is allowed" do
          expect(MultiXml.parse(@xml,
            disallowed_types: [])["tag"]).to eq(:message => "Have a nice day", 1 => "returns an integer",
              "array" => [{"has-dashes" => true, "has_underscores" => true}])
        end
      end

      context 'with an attribute type="symbol"' do
        before do
          @xml = '<tag type="symbol">my_symbol</tag>'
        end

        it "raises MultiXML::DisallowedTypeError" do
          expect { MultiXml.parse(@xml)["tag"] }.to raise_error(MultiXml::DisallowedTypeError)
        end

        it "returns the correctly parsed Symbol when the type is allowed" do
          expect(MultiXml.parse(@xml, disallowed_types: [])["tag"]).to eq(:my_symbol)
        end
      end

      context 'with an attribute type="file"' do
        before do
          @xml = '<tag type="file" name="data.txt" content_type="text/plain">ZGF0YQ==</tag>'
        end

        it "returns a StringIO" do
          expect(MultiXml.parse(@xml)["tag"]).to be_a(StringIO)
        end

        it "is decoded correctly" do
          expect(MultiXml.parse(@xml)["tag"].string).to eq("data")
        end

        it "has the correct file name" do
          expect(MultiXml.parse(@xml)["tag"].original_filename).to eq("data.txt")
        end

        it "has the correct content type" do
          expect(MultiXml.parse(@xml)["tag"].content_type).to eq("text/plain")
        end

        context "with missing name and content type" do
          before do
            @xml = '<tag type="file">ZGF0YQ==</tag>'
          end

          it "returns a StringIO" do
            expect(MultiXml.parse(@xml)["tag"]).to be_a(StringIO)
          end

          it "is decoded correctly" do
            expect(MultiXml.parse(@xml)["tag"].string).to eq("data")
          end

          it "has the default file name" do
            expect(MultiXml.parse(@xml)["tag"].original_filename).to eq("untitled")
          end

          it "has the default content type" do
            expect(MultiXml.parse(@xml)["tag"].content_type).to eq("application/octet-stream")
          end
        end
      end

      context 'with an attribute type="array"' do
        before do
          @xml = '<users type="array"><user>Erik Berlin</user><user>Wynn Netherland</user></users>'
        end

        it "returns an Array" do
          expect(MultiXml.parse(@xml)["users"]).to be_a(Array)
        end

        it "returns the correct array" do
          expect(MultiXml.parse(@xml)["users"]).to eq(["Erik Berlin", "Wynn Netherland"])
        end
      end

      context 'with an attribute type="array" in addition to other attributes' do
        before do
          @xml = '<users type="array" foo="bar"><user>Erik Berlin</user><user>Wynn Netherland</user></users>'
        end

        it "returns an Array" do
          expect(MultiXml.parse(@xml)["users"]).to be_a(Array)
        end

        it "returns the correct array" do
          expect(MultiXml.parse(@xml)["users"]).to eq(["Erik Berlin", "Wynn Netherland"])
        end
      end

      context 'with an attribute type="array" containing only one item' do
        before do
          @xml = '<users type="array"><user>Erik Berlin</user></users>'
        end

        it "returns an Array" do
          expect(MultiXml.parse(@xml)["users"]).to be_a(Array)
        end

        it "returns the correct array" do
          expect(MultiXml.parse(@xml)["users"]).to eq(["Erik Berlin"])
        end
      end

      %w[integer boolean date datetime file].each do |type|
        context "with an empty attribute type=\"#{type}\"" do
          before do
            @xml = "<tag type=\"#{type}\"/>"
          end

          it "returns nil" do
            expect(MultiXml.parse(@xml)["tag"]).to be_nil
          end
        end
      end

      %w[yaml symbol].each do |type|
        context "with an empty attribute type=\"#{type}\"" do
          before do
            @xml = "<tag type=\"#{type}\"/>"
          end

          it "raises MultiXml::DisallowedTypeError by default" do
            expect { MultiXml.parse(@xml)["tag"] }.to raise_error(MultiXml::DisallowedTypeError)
          end

          it "returns nil when the type is allowed" do
            expect(MultiXml.parse(@xml, disallowed_types: [])["tag"]).to be_nil
          end
        end
      end

      context 'with an empty attribute type="array"' do
        before do
          @xml = '<tag type="array"/>'
        end

        it "returns an empty Array" do
          expect(MultiXml.parse(@xml)["tag"]).to eq([])
        end

        context "with whitespace" do
          before do
            @xml = '<tag type="array"> </tag>'
          end

          it "returns an empty Array" do
            expect(MultiXml.parse(@xml)["tag"]).to eq([])
          end
        end
      end

      context "with XML entities" do
        before do
          @xml_entities = {
            "<" => "&lt;",
            ">" => "&gt;",
            '"' => "&quot;",
            "'" => "&apos;",
            "&" => "&amp;"
          }
        end

        context "in content" do
          it "returns unescaped XML entities" do
            @xml_entities.each do |key, value|
              xml = "<tag>#{value}</tag>"
              expect(MultiXml.parse(xml)["tag"]).to eq(key)
            end
          end
        end

        context "in attribute" do
          it "returns unescaped XML entities" do
            @xml_entities.each do |key, value|
              xml = "<tag attribute=\"#{value}\"/>"
              expect(MultiXml.parse(xml)["tag"]["attribute"]).to eq(key)
            end
          end
        end
      end

      context "with dasherized tag" do
        before do
          @xml = "<tag-1/>"
        end

        it "returns undasherize tag" do
          expect(MultiXml.parse(@xml).keys).to include("tag_1")
        end
      end

      context "with dasherized attribute" do
        before do
          @xml = '<tag attribute-1="1"></tag>'
        end

        it "returns undasherize attribute" do
          expect(MultiXml.parse(@xml)["tag"].keys).to include("attribute_1")
        end
      end

      context "with children" do
        context "with attributes" do
          before do
            @xml = '<users><user name="Erik Berlin"/></users>'
          end

          it "returns the correct attributes" do
            expect(MultiXml.parse(@xml)["users"]["user"]["name"]).to eq("Erik Berlin")
          end
        end

        context "with text" do
          before do
            @xml = "<user><name>Erik Berlin</name></user>"
          end

          it "returns the correct text" do
            expect(MultiXml.parse(@xml)["user"]["name"]).to eq("Erik Berlin")
          end
        end

        context "with an unrecognized attribute type" do
          before do
            @xml = '<user type="admin"><name>Erik Berlin</name></user>'
          end

          it "passes through the type" do
            expect(MultiXml.parse(@xml)["user"]["type"]).to eq("admin")
          end
        end

        context "with attribute tags on content nodes" do
          context "non 'type' attributes" do
            before do
              @xml = <<-XML
                <options>
                  <value currency='USD'>123</value>
                  <value number='percent'>0.123</value>
                </options>
              XML
              @parsed_xml = MultiXml.parse(@xml)
            end

            it "adds the attributes to the value hash" do
              expect(@parsed_xml["options"]["value"][0]["__content__"]).to eq("123")
              expect(@parsed_xml["options"]["value"][0]["currency"]).to eq("USD")
              expect(@parsed_xml["options"]["value"][1]["__content__"]).to eq("0.123")
              expect(@parsed_xml["options"]["value"][1]["number"]).to eq("percent")
            end
          end

          context "unrecognized type attributes" do
            before do
              @xml = <<-XML
                <options>
                  <value type='USD'>123</value>
                  <value type='percent'>0.123</value>
                  <value currency='USD'>123</value>
                </options>
              XML
              @parsed_xml = MultiXml.parse(@xml)
            end

            it "adds the attributes to the value hash passing through the type" do
              expect(@parsed_xml["options"]["value"][0]["__content__"]).to eq("123")
              expect(@parsed_xml["options"]["value"][0]["type"]).to eq("USD")
              expect(@parsed_xml["options"]["value"][1]["__content__"]).to eq("0.123")
              expect(@parsed_xml["options"]["value"][1]["type"]).to eq("percent")
              expect(@parsed_xml["options"]["value"][2]["__content__"]).to eq("123")
              expect(@parsed_xml["options"]["value"][2]["currency"]).to eq("USD")
            end
          end

          context "mixing attributes and non-attributes content nodes type attributes" do
            before do
              @xml = <<-XML
                <options>
                  <value type='USD'>123</value>
                  <value type='percent'>0.123</value>
                  <value>123</value>
                </options>
              XML
              @parsed_xml = MultiXml.parse(@xml)
            end

            it "adds the attributes to the value hash passing through the type" do
              expect(@parsed_xml["options"]["value"][0]["__content__"]).to eq("123")
              expect(@parsed_xml["options"]["value"][0]["type"]).to eq("USD")
              expect(@parsed_xml["options"]["value"][1]["__content__"]).to eq("0.123")
              expect(@parsed_xml["options"]["value"][1]["type"]).to eq("percent")
              expect(@parsed_xml["options"]["value"][2]).to eq("123")
            end
          end

          context "mixing recognized type attribute and non-type attributes on content nodes" do
            before do
              @xml = <<-XML
                <options>
                  <value number='USD' type='integer'>123</value>
                </options>
              XML
              @parsed_xml = MultiXml.parse(@xml)
            end

            it "adds the the non-type attribute and remove the recognized type attribute and do the typecast" do
              expect(@parsed_xml["options"]["value"]["__content__"]).to eq(123)
              expect(@parsed_xml["options"]["value"]["number"]).to eq("USD")
            end
          end

          context "mixing unrecognized type attribute and non-type attributes on content nodes" do
            before do
              @xml = <<-XML
                <options>
                  <value number='USD' type='currency'>123</value>
                </options>
              XML
              @parsed_xml = MultiXml.parse(@xml)
            end

            it "adds the the non-type attributes and type attribute to the value hash" do
              expect(@parsed_xml["options"]["value"]["__content__"]).to eq("123")
              expect(@parsed_xml["options"]["value"]["number"]).to eq("USD")
              expect(@parsed_xml["options"]["value"]["type"]).to eq("currency")
            end
          end
        end

        context "with newlines and whitespace" do
          before do
            @xml = <<-XML
              <user>
                <name>Erik Berlin</name>
              </user>
            XML
          end

          it "parses correctly" do
            expect(MultiXml.parse(@xml)).to eq("user" => {"name" => "Erik Berlin"})
          end
        end

        # Babies having babies
        context "with children" do
          before do
            @xml = '<users><user name="Erik Berlin"><status text="Hello"/></user></users>'
          end

          it "parses correctly" do
            expect(MultiXml.parse(@xml)).to eq("users" => {"user" => {"name" => "Erik Berlin",
                                                                      "status" => {"text" => "Hello"}}})
          end
        end
      end

      context "with sibling children" do
        before do
          @xml = "<users><user>Erik Berlin</user><user>Wynn Netherland</user></users>"
        end

        it "returns an Array" do
          expect(MultiXml.parse(@xml)["users"]["user"]).to be_a(Array)
        end

        it "parses correctly" do
          expect(MultiXml.parse(@xml)).to eq("users" => {"user" => ["Erik Berlin", "Wynn Netherland"]})
        end
      end
    end

    context "a duplexed stream" do
      before do
        @xml, wr = IO.pipe

        Thread.new do
          "<user/>".each_char do |chunk|
            wr << chunk
          end
          wr.close
        end
      end

      it "parses correctly" do
        expect(MultiXml.parse(@xml)).to eq("user" => nil)
      end
    end
  end
end
