shared_examples_for 'a parser' do |parser|
  before do
    begin
      MultiXml.parser = parser

      if parser == 'LibXML'
        LibXML::XML::Error.set_handler(&LibXML::XML::Error::QUIET_HANDLER)
      end
    rescue LoadError
      pending "Parser #{parser} couldn't be loaded"
    end
  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 = ' '.freeze
      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 Michaels-Ober]]></user>'
        end

        it 'returns the correct CDATA' do
          expect(MultiXml.parse(@xml)['user']).to eq('Erik Michaels-Ober')
        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 Michaels-Ober</user>'
        end

        it 'returns the correct content' do
          expect(MultiXml.parse(@xml)['user']).to eq('Erik Michaels-Ober')
        end
      end

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

        it 'returns the correct attribute' do
          expect(MultiXml.parse(@xml)['user']['name']).to eq('Erik Michaels-Ober')
        end
      end

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

        it 'returns the correct attributes' do
          expect(MultiXml.parse(@xml)['user']['name']).to eq('Erik Michaels-Ober')
          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 Michaels-Ober"/><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 Michaels-Ober'}, {: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 Michaels-Ober</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 Michaels-Ober', '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 Michaels-Ober</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 Michaels-Ober', 'Wynn Netherland'])
        end
      end

      context 'with an attribute type="array" containing only one item' do
        before do
          @xml = '<users type="array"><user>Erik Michaels-Ober</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 Michaels-Ober'])
        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 Michaels-Ober"/></users>'
          end

          it 'returns the correct attributes' do
            expect(MultiXml.parse(@xml)['users']['user']['name']).to eq('Erik Michaels-Ober')
          end
        end

        context 'with text' do
          before do
            @xml = '<user><name>Erik Michaels-Ober</name></user>'
          end

          it 'returns the correct text' do
            expect(MultiXml.parse(@xml)['user']['name']).to eq('Erik Michaels-Ober')
          end
        end

        context 'with an unrecognized attribute type' do
          before do
            @xml = '<user type="admin"><name>Erik Michaels-Ober</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 Michaels-Ober</name>
              </user>
            XML
          end

          it 'parses correctly' do
            expect(MultiXml.parse(@xml)).to eq('user' => {'name' => 'Erik Michaels-Ober'})
          end
        end

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

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

      context 'with sibling children' do
        before do
          @xml = '<users><user>Erik Michaels-Ober</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 Michaels-Ober', '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
