require "spec_helper"

describe Nori do

  Nori::PARSERS.each do |parser, class_name|
    context "using the :#{parser} parser" do

      let(:parser) { parser }

      it "should work with unnormalized characters" do
        xml = '<root>&amp;</root>'
        expect(parse(xml)).to eq({ 'root' => "&" })
      end

      it "should transform a simple tag with content" do
        xml = "<tag>This is the contents</tag>"
        expect(parse(xml)).to eq({ 'tag' => 'This is the contents' })
      end

      it "should work with cdata tags" do
        xml = <<-END
          <tag>
          <![CDATA[
            text inside cdata &lt; test
          ]]>
          </tag>
        END
        expect(parse(xml)["tag"].strip).to eq("text inside cdata &lt; test")
      end

      it "should scrub bad characters" do
        xml = "<tag>a\xfbc</tag>".force_encoding('UTF-8')
        expect(parse(xml)["tag"]).to eq("a\uFFFDc")
      end

      it "should transform a simple tag with attributes" do
        xml = "<tag attr1='1' attr2='2'></tag>"
        hash = { 'tag' => { '@attr1' => '1', '@attr2' => '2' } }
        expect(parse(xml)).to eq(hash)
      end

      it "should transform repeating siblings into an array" do
        xml =<<-XML
          <opt>
            <user login="grep" fullname="Gary R Epstein" />
            <user login="stty" fullname="Simon T Tyson" />
          </opt>
        XML

        expect(parse(xml)['opt']['user'].class).to eq(Array)

        hash = {
          'opt' => {
            'user' => [{
              '@login'    => 'grep',
              '@fullname' => 'Gary R Epstein'
            },{
              '@login'    => 'stty',
              '@fullname' => 'Simon T Tyson'
            }]
          }
        }

        expect(parse(xml)).to eq(hash)
      end

      it "should not transform non-repeating siblings into an array" do
        xml =<<-XML
          <opt>
            <user login="grep" fullname="Gary R Epstein" />
          </opt>
        XML

        expect(parse(xml)['opt']['user'].class).to eq(Hash)

        hash = {
          'opt' => {
            'user' => {
              '@login' => 'grep',
              '@fullname' => 'Gary R Epstein'
            }
          }
        }

        expect(parse(xml)).to eq(hash)
      end

      it "should prefix attributes with an @-sign to avoid problems with overwritten values" do
        xml =<<-XML
          <multiRef id="id1">
            <login>grep</login>
            <id>76737</id>
          </multiRef>
        XML

        expect(parse(xml)["multiRef"]).to eq({ "login" => "grep", "@id" => "id1", "id" => "76737" })
      end

      context "without advanced typecasting" do
        it "should not transform 'true'" do
          hash = parse("<value>true</value>", :advanced_typecasting => false)
          expect(hash["value"]).to eq("true")
        end

        it "should not transform 'false'" do
          hash = parse("<value>false</value>", :advanced_typecasting => false)
          expect(hash["value"]).to eq("false")
        end

        it "should not transform Strings matching the xs:time format" do
          hash = parse("<value>09:33:55Z</value>", :advanced_typecasting => false)
          expect(hash["value"]).to eq("09:33:55Z")
        end

        it "should not transform Strings matching the xs:date format" do
          hash = parse("<value>1955-04-18-05:00</value>", :advanced_typecasting => false)
          expect(hash["value"]).to eq("1955-04-18-05:00")
        end

        it "should not transform Strings matching the xs:dateTime format" do
          hash = parse("<value>1955-04-18T11:22:33-05:00</value>", :advanced_typecasting => false)
          expect(hash["value"]).to eq("1955-04-18T11:22:33-05:00")
        end
      end

      context "with advanced typecasting" do
        it "should transform 'true' to TrueClass" do
          expect(parse("<value>true</value>")["value"]).to eq(true)
        end

        it "should transform 'false' to FalseClass" do
          expect(parse("<value>false</value>")["value"]).to eq(false)
        end

        it "should transform Strings matching the xs:time format to Time objects" do
          expect(parse("<value>09:33:55.7Z</value>")["value"]).to eq(Time.parse("09:33:55.7Z"))
        end

        it "should transform Strings matching the xs:time format ahead of utc to Time objects" do
          expect(parse("<value>09:33:55+02:00</value>")["value"]).to eq(Time.parse("09:33:55+02:00"))
        end

        it "should transform Strings matching the xs:date format to Date objects" do
          expect(parse("<value>1955-04-18-05:00</value>")["value"]).to eq(Date.parse("1955-04-18-05:00"))
        end

        it "should transform Strings matching the xs:dateTime format ahead of utc to Date objects" do
          expect(parse("<value>1955-04-18+02:00</value>")["value"]).to eq(Date.parse("1955-04-18+02:00"))
        end

        it "should transform Strings matching the xs:dateTime format to DateTime objects" do
          expect(parse("<value>1955-04-18T11:22:33.5Z</value>")["value"]).to eq(
            DateTime.parse("1955-04-18T11:22:33.5Z")
          )
        end

        it "should transform Strings matching the xs:dateTime format ahead of utc to DateTime objects" do
          expect(parse("<value>1955-04-18T11:22:33+02:00</value>")["value"]).to eq(
            DateTime.parse("1955-04-18T11:22:33+02:00")
          )
        end

        it "should transform Strings matching the xs:dateTime format with seconds and an offset to DateTime objects" do
          expect(parse("<value>2004-04-12T13:20:15.5-05:00</value>")["value"]).to eq(
            DateTime.parse("2004-04-12T13:20:15.5-05:00")
          )
        end

        it "should not transform Strings containing an xs:time String and more" do
          expect(parse("<value>09:33:55Z is a time</value>")["value"]).to eq("09:33:55Z is a time")
          expect(parse("<value>09:33:55Z_is_a_file_name</value>")["value"]).to eq("09:33:55Z_is_a_file_name")
        end

        it "should not transform Strings containing an xs:date String and more" do
          expect(parse("<value>1955-04-18-05:00 is a date</value>")["value"]).to eq("1955-04-18-05:00 is a date")
          expect(parse("<value>1955-04-18-05:00_is_a_file_name</value>")["value"]).to eq("1955-04-18-05:00_is_a_file_name")
        end

        it "should not transform Strings containing an xs:dateTime String and more" do
          expect(parse("<value>1955-04-18T11:22:33-05:00 is a dateTime</value>")["value"]).to eq(
            "1955-04-18T11:22:33-05:00 is a dateTime"
          )
          expect(parse("<value>1955-04-18T11:22:33-05:00_is_a_file_name</value>")["value"]).to eq(
            "1955-04-18T11:22:33-05:00_is_a_file_name"
          )
        end

        ["00-00-00", "0000-00-00", "0000-00-00T00:00:00", "0569-23-0141", "DS2001-19-1312654773", "e6:53:01:00:ce:b4:06"].each do |date_string|
          it "should not transform a String like '#{date_string}' to date or time" do
            expect(parse("<value>#{date_string}</value>")["value"]).to eq(date_string)
          end
        end
      end

      context "Parsing xml with text and attributes" do
        before do
          xml =<<-XML
            <opt>
              <user login="grep">Gary R Epstein</user>
              <user>Simon T Tyson</user>
            </opt>
          XML
          @data = parse(xml)
        end

        it "correctly parse text nodes" do
          expect(@data).to eq({
            'opt' => {
              'user' => [
                'Gary R Epstein',
                'Simon T Tyson'
              ]
            }
          })
        end

        it "parses attributes for text node if present" do
          expect(@data['opt']['user'][0].attributes).to eq({'login' => 'grep'})
        end

        it "default attributes to empty hash if not present" do
          expect(@data['opt']['user'][1].attributes).to eq({})
        end

        it "add 'attributes' accessor methods to parsed instances of String" do
          expect(@data['opt']['user'][0]).to respond_to(:attributes)
          expect(@data['opt']['user'][0]).to respond_to(:attributes=)
        end

        it "not add 'attributes' accessor methods to all instances of String" do
          expect("some-string").not_to respond_to(:attributes)
          expect("some-string").not_to respond_to(:attributes=)
        end
      end

      context "with the :serializable profile" do
        it "returns a text node with attributes as a plain hash" do
          xml = '<test attr="value" attr1="value1">Text</test>'
          expect(parse(xml, :serializable => true)).to eq(
            "test" => { "#text" => "Text", "@attr" => "value", "@attr1" => "value1" }
          )
        end

        it "passes the #text key through a :convert_tags_to formula" do
          # the formula sees every key Nori emits: element names, @-attributes
          # and the synthetic #text key alike
          expect(parse('<foo bar="baz">Content</foo>', :serializable => true, :convert_tags_to => :to_sym.to_proc)).
            to eq(:foo => { :"#text" => "Content", :"@bar" => "baz" })
        end

        it "keeps attributes when advanced typecasting converts the value" do
          expect(parse('<status source="ldap">true</status>', :serializable => true)).
            to eq("status" => { "#text" => true, "@source" => "ldap" })
        end

        it "keeps attributes when a bare type attribute casts the value" do
          expect(parse('<amount type="decimal" currency="EUR">42.50</amount>', :serializable => true)).
            to eq("amount" => { "#text" => BigDecimal("42.50"), "@currency" => "EUR" })
        end

        it "returns a typecast value without attributes as the plain value" do
          expect(parse("<value>true</value>", :serializable => true)).to eq("value" => true)
        end

        it "can rename the #text key to a plain text key" do
          # a # never occurs in element or attribute names (it is not a valid
          # XML name character), so dropping it only affects the #text key
          converter = lambda { |tag| tag.delete_prefix("#") }
          expect(parse('<foo bar="baz">Content</foo>', :serializable => true, :convert_tags_to => converter)).
            to eq("foo" => { "text" => "Content", "@bar" => "baz" })
        end

        it "returns a text node without attributes as a plain String" do
          xml =<<-XML
            <opt>
              <user login="grep">Gary R Epstein</user>
              <user>Simon T Tyson</user>
            </opt>
          XML
          data = parse(xml, :serializable => true)

          expect(data["opt"]["user"][0]).to eq("#text" => "Gary R Epstein", "@login" => "grep")
          expect(data["opt"]["user"][1]).to eq("Simon T Tyson")
          expect(data["opt"]["user"][1]).to be_an_instance_of(String)
          expect(data["opt"]["user"][1]).not_to respond_to(:attributes)
        end

        it "applies :convert_tags_to to the attribute keys, matching element-node attributes" do
          xml = '<test attr="value">Text</test>'
          expect(parse(xml, :serializable => true, :convert_tags_to => :upcase.to_proc)).to eq(
            "TEST" => { "#TEXT" => "Text", "@ATTR" => "value" }
          )
        end

        it "leaves the default (profile off) output as a StringWithAttributes" do
          result = parse('<test attr="value">Text</test>')["test"]

          expect(result).to eq("Text")
          expect(result).to be_a(Nori::StringWithAttributes)
          expect(result.attributes).to eq("attr" => "value")
        end

        it "returns an empty tag with attributes as a hash" do
          options = { :serializable => true, :consistent_empty_tags => true, :empty_tag_value => "" }
          expect(parse('<foo bar="baz"/>', options)).to eq(
            "foo" => { "#text" => "", "@bar" => "baz" }
          )
        end

        it "keeps CDATA text as plain data, never a StringWithAttributes" do
          with_attr = parse('<foo bar="baz"><![CDATA[hello]]></foo>', :serializable => true)["foo"]
          without_attr = parse('<foo><![CDATA[hello]]></foo>', :serializable => true)["foo"]

          expect(with_attr).to eq("#text" => "hello", "@bar" => "baz")
          expect(without_attr).to eq("hello")
          expect(without_attr).to be_an_instance_of(String)
        end

        it "does not decode a type=\"file\" node, returning plain data" do
          xml = '<doc type="file" name="x.pdf" content_type="application/pdf">aGVsbG8=</doc>'
          result = parse(xml, :serializable => true)["doc"]

          expect(result).to eq(
            "#text" => "aGVsbG8=", "@type" => "file", "@name" => "x.pdf", "@content_type" => "application/pdf"
          )
          expect(result).not_to be_a(Nori::StringIOFile)
        end

        it "still decodes a type=\"file\" node into a StringIOFile when the profile is off" do
          xml = '<doc type="file" name="x.pdf" content_type="application/pdf">aGVsbG8=</doc>'
          result = parse(xml)["doc"]

          expect(result).to be_a(Nori::StringIOFile)
          expect(result.read).to eq("hello")
          expect(result.original_filename).to eq("x.pdf")
          expect(result.content_type).to eq("application/pdf")
        end
      end

      context "with the :standards and :serializable profiles combined" do
        # the two profiles together: values stay text (no typecasting) and
        # every value with attributes takes the plain-data hash shape

        def parse_profiles(xml)
          parse(xml, :standards => true, :serializable => true)
        end

        it "keeps values as strings and attributes in the hash shape" do
          expect(parse_profiles('<foo type="integer" bar="baz">123</foo>')).to eq(
            "foo" => { "#text" => "123", "@type" => "integer", "@bar" => "baz" }
          )
        end

        it "does not guess types from string shape" do
          expect(parse_profiles("<value>true</value>")).to eq("value" => "true")
          expect(parse_profiles("<value>2003-07-16</value>")).to eq("value" => "2003-07-16")
        end

        it "returns an empty tag with attributes as the hash shape with an empty string" do
          expect(parse_profiles('<foo bar="baz"/>')).to eq(
            "foo" => { "#text" => "", "@bar" => "baz" }
          )
        end

        it "still returns nil for an explicit xsi:nil" do
          xml = '<foo xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>'
          expect(parse_profiles(xml)).to eq("foo" => nil)
        end

        it "keeps preserved whitespace in the hash shape under xml:space=\"preserve\"" do
          expect(parse_profiles('<foo xml:space="preserve">   </foo>')).to eq(
            "foo" => { "#text" => "   ", "@xml:space" => "preserve" }
          )
        end

        it "treats a bare nil attribute as an ordinary attribute in the hash shape" do
          expect(parse_profiles('<foo nil="true"/>')).to eq(
            "foo" => { "#text" => "", "@nil" => "true" }
          )
        end

        it "does not decode a bare type=\"file\" element" do
          expect(parse_profiles('<doc type="file" name="x.pdf">aGVsbG8=</doc>')).to eq(
            "doc" => { "#text" => "aGVsbG8=", "@type" => "file", "@name" => "x.pdf" }
          )
        end

        it "lets an explicit :advanced_typecasting override compose with the hash shape" do
          xml = '<status source="ldap">true</status>'
          expect(parse(xml, :standards => true, :serializable => true, :advanced_typecasting => true)).
            to eq("status" => { "#text" => true, "@source" => "ldap" })
        end
      end

      it "should typecast an integer" do
        xml = "<tag type='integer'>10</tag>"
        expect(parse(xml)['tag']).to eq(10)
      end

      it "should typecast a true boolean" do
        xml = "<tag type='boolean'>true</tag>"
        expect(parse(xml)['tag']).to be(true)
      end

      it "should typecast a false boolean" do
        ["false"].each do |w|
          expect(parse("<tag type='boolean'>#{w}</tag>")['tag']).to be(false)
        end
      end

      it "should typecast a datetime" do
        xml = "<tag type='datetime'>2007-12-31 10:32</tag>"
        expect(parse(xml)['tag']).to eq(Time.parse( '2007-12-31 10:32' ).utc)
      end

      it "should typecast a date" do
        xml = "<tag type='date'>2007-12-31</tag>"
        expect(parse(xml)['tag']).to eq(Date.parse('2007-12-31'))
      end

      xml_entities = {
        "<" => "&lt;",
        ">" => "&gt;",
        '"' => "&quot;",
        "'" => "&apos;",
        "&" => "&amp;"
      }

      it "should unescape html entities" do
        xml_entities.each do |k,v|
          xml = "<tag>Some content #{v}</tag>"
          expect(parse(xml)['tag']).to match(Regexp.new(k))
        end
      end

      it "should unescape XML entities in attributes" do
        xml_entities.each do |key, value|
          xml = "<tag attr='Some content #{value}'></tag>"
          expect(parse(xml)['tag']['@attr']).to match(Regexp.new(key))
        end
      end

      it "should undasherize keys as tags" do
        xml = "<tag-1>Stuff</tag-1>"
        expect(parse(xml).keys).to include('tag_1')
      end

      it "should undasherize keys as attributes" do
        xml = "<tag1 attr-1='1'></tag1>"
        expect(parse(xml)['tag1'].keys).to include('@attr_1')
      end

      it "should undasherize keys as tags and attributes" do
        xml = "<tag-1 attr-1='1'></tag-1>"
        expect(parse(xml).keys).to include('tag_1')
        expect(parse(xml)['tag_1'].keys).to include('@attr_1')
      end

      it "should render nested content correctly" do
        xml = "<root><tag1>Tag1 Content <em><strong>This is strong</strong></em></tag1></root>"
        expect(parse(xml)['root']['tag1']).to eq("Tag1 Content <em><strong>This is strong</strong></em>")
      end

      it "should render nested content with text nodes correctly" do
        xml = "<root>Tag1 Content<em>Stuff</em> Hi There</root>"
        expect(parse(xml)['root']).to eq("Tag1 Content<em>Stuff</em> Hi There")
      end

      it "should ignore attributes when a child is a text node" do
        xml = "<root attr1='1'>Stuff</root>"
        expect(parse(xml)).to eq({ "root" => "Stuff" })
      end

      it "should ignore attributes when any child is a text node" do
        xml = "<root attr1='1'>Stuff <em>in italics</em></root>"
        expect(parse(xml)).to eq({ "root" => "Stuff <em>in italics</em>" })
      end

      it "should correctly transform multiple children" do
        xml = <<-XML
        <user gender='m'>
          <age type='integer'>35</age>
          <name>Home Simpson</name>
          <dob type='date'>1988-01-01</dob>
          <joined-at type='datetime'>2000-04-28 23:01</joined-at>
          <is-cool type='boolean'>true</is-cool>
        </user>
        XML

        hash = {
          "user" => {
            "@gender"   => "m",
            "age"       => 35,
            "name"      => "Home Simpson",
            "dob"       => Date.parse('1988-01-01'),
            "joined_at" => Time.parse("2000-04-28 23:01"),
            "is_cool"   => true
          }
        }

        expect(parse(xml)).to eq(hash)
      end

      it "should properly handle nil values (ActiveSupport Compatible)" do
        topic_xml = <<-EOT
          <topic>
            <title></title>
            <id type="integer"></id>
            <approved type="boolean"></approved>
            <written-on type="date"></written-on>
            <viewed-at type="datetime"></viewed-at>
            <content type="yaml"></content>
            <parent-id></parent-id>
            <nil_true nil="true"/>
            <namespaced xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
          </topic>
        EOT

        expected_topic_hash = {
          'title'      => nil,
          'id'         => nil,
          'approved'   => nil,
          'written_on' => nil,
          'viewed_at'  => nil,
          # don't execute arbitary YAML code
          'content'    => { "@type" => "yaml" },
          'parent_id'  => nil,
          'nil_true'   => nil,
          'namespaced' => nil
        }
        expect(parse(topic_xml)["topic"]).to eq(expected_topic_hash)
      end

      it "should handle a single record from xml (ActiveSupport Compatible)" do
        topic_xml = <<-EOT
          <topic>
            <title>The First Topic</title>
            <author-name>David</author-name>
            <id type="integer">1</id>
            <approved type="boolean"> true </approved>
            <replies-count type="integer">0</replies-count>
            <replies-close-in type="integer">2592000000</replies-close-in>
            <written-on type="date">2003-07-16</written-on>
            <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
            <content type="yaml">--- \n1: should be an integer\n:message: Have a nice day\narray: \n- should-have-dashes: true\n  should_have_underscores: true</content>
            <author-email-address>david@loudthinking.com</author-email-address>
            <parent-id></parent-id>
            <ad-revenue type="decimal">1.5</ad-revenue>
            <optimum-viewing-angle type="float">135</optimum-viewing-angle>
            <resident type="symbol">yes</resident>
          </topic>
        EOT

        expected_topic_hash = {
          'title' => "The First Topic",
          'author_name' => "David",
          'id' => 1,
          'approved' => true,
          'replies_count' => 0,
          'replies_close_in' => 2592000000,
          'written_on' => Date.new(2003, 7, 16),
          'viewed_at' => Time.utc(2003, 7, 16, 9, 28),
          # Changed this line where the key is :message.  The yaml specifies this as a symbol, and who am I to change what you specify
          # The line in ActiveSupport is
          # 'content' => { 'message' => "Have a nice day", 1 => "should be an integer", "array" => [{ "should-have-dashes" => true, "should_have_underscores" => true }] },
          'content' => "--- \n1: should be an integer\n:message: Have a nice day\narray: \n- should-have-dashes: true\n  should_have_underscores: true",
          'author_email_address' => "david@loudthinking.com",
          'parent_id' => nil,
          'ad_revenue' => BigDecimal("1.50"),
          'optimum_viewing_angle' => 135.0,
          # don't create symbols from arbitary remote code
          'resident' => "yes"
        }

        parse(topic_xml)["topic"].each do |k,v|
          expect(v).to eq(expected_topic_hash[k])
        end
      end

      it "should handle multiple records (ActiveSupport Compatible)" do
        topics_xml = <<-EOT
          <topics type="array">
            <topic>
              <title>The First Topic</title>
              <author-name>David</author-name>
              <id type="integer">1</id>
              <approved type="boolean">false</approved>
              <replies-count type="integer">0</replies-count>
              <replies-close-in type="integer">2592000000</replies-close-in>
              <written-on type="date">2003-07-16</written-on>
              <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
              <content>Have a nice day</content>
              <author-email-address>david@loudthinking.com</author-email-address>
              <parent-id nil="true"></parent-id>
            </topic>
            <topic>
              <title>The Second Topic</title>
              <author-name>Jason</author-name>
              <id type="integer">1</id>
              <approved type="boolean">false</approved>
              <replies-count type="integer">0</replies-count>
              <replies-close-in type="integer">2592000000</replies-close-in>
              <written-on type="date">2003-07-16</written-on>
              <viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
              <content>Have a nice day</content>
              <author-email-address>david@loudthinking.com</author-email-address>
              <parent-id></parent-id>
            </topic>
          </topics>
        EOT

        expected_topic_hash = {
          'title' => "The First Topic",
          'author_name' => "David",
          'id' => 1,
          'approved' => false,
          'replies_count' => 0,
          'replies_close_in' => 2592000000,
          'written_on' => Date.new(2003, 7, 16),
          'viewed_at' => Time.utc(2003, 7, 16, 9, 28),
          'content' => "Have a nice day",
          'author_email_address' => "david@loudthinking.com",
          'parent_id' => nil
        }

        # puts Nori.parse(topics_xml)['topics'].first.inspect
        parse(topics_xml)["topics"].first.each do |k,v|
          expect(v).to eq(expected_topic_hash[k])
        end
      end

      context "with convert_attributes_to set to a custom formula" do
        it "alters attributes and values" do
          converter = lambda {|key, value| ["#{key}_k", "#{value}_v"] }
          xml = <<-XML
            <user name="value"><age>21</age></user>
          XML

          expect(parse(xml, :convert_attributes_to => converter)).to eq({'user' => {'@name_k' => 'value_v', 'age' => '21'}})
        end
      end

      it "should handle a single record from_xml with attributes other than type (ActiveSupport Compatible)" do
        topic_xml = <<-EOT
        <rsp stat="ok">
          <photos page="1" pages="1" perpage="100" total="16">
            <photo id="175756086" owner="55569174@N00" secret="0279bf37a1" server="76" title="Colored Pencil PhotoBooth Fun" ispublic="1" isfriend="0" isfamily="0"/>
          </photos>
        </rsp>
        EOT

        expected_topic_hash = {
          '@id' => "175756086",
          '@owner' => "55569174@N00",
          '@secret' => "0279bf37a1",
          '@server' => "76",
          '@title' => "Colored Pencil PhotoBooth Fun",
          '@ispublic' => "1",
          '@isfriend' => "0",
          '@isfamily' => "0",
        }

        parse(topic_xml)["rsp"]["photos"]["photo"].each do |k, v|
          expect(v).to eq(expected_topic_hash[k])
        end
      end

      it "should handle an emtpy array (ActiveSupport Compatible)" do
        blog_xml = <<-XML
          <blog>
            <posts type="array"></posts>
          </blog>
        XML
        expected_blog_hash = {"blog" => {"posts" => []}}
        expect(parse(blog_xml)).to eq(expected_blog_hash)
      end

      it "should handle empty array with whitespace from xml (ActiveSupport Compatible)" do
        blog_xml = <<-XML
          <blog>
            <posts type="array">
            </posts>
          </blog>
        XML
        expected_blog_hash = {"blog" => {"posts" => []}}
        expect(parse(blog_xml)).to eq(expected_blog_hash)
      end

      it "should handle array with one entry from_xml (ActiveSupport Compatible)" do
        blog_xml = <<-XML
          <blog>
            <posts type="array">
              <post>a post</post>
            </posts>
          </blog>
        XML
        expected_blog_hash = {"blog" => {"posts" => ["a post"]}}
        expect(parse(blog_xml)).to eq(expected_blog_hash)
      end

      it "should handle array with multiple entries from xml (ActiveSupport Compatible)" do
        blog_xml = <<-XML
          <blog>
            <posts type="array">
              <post>a post</post>
              <post>another post</post>
            </posts>
          </blog>
        XML
        expected_blog_hash = {"blog" => {"posts" => ["a post", "another post"]}}
        expect(parse(blog_xml)).to eq(expected_blog_hash)
      end

      it "should handle file types (ActiveSupport Compatible)" do
        blog_xml = <<-XML
          <blog>
            <logo type="file" name="logo.png" content_type="image/png">
            </logo>
          </blog>
        XML
        hash = parse(blog_xml)
        expect(hash.keys).to include('blog')
        expect(hash['blog'].keys).to include('logo')

        file = hash['blog']['logo']
        expect(file.original_filename).to eq('logo.png')
        expect(file.content_type).to eq('image/png')
      end

      it "should handle file from xml with defaults (ActiveSupport Compatible)" do
        blog_xml = <<-XML
          <blog>
            <logo type="file">
            </logo>
          </blog>
        XML
        file = parse(blog_xml)['blog']['logo']
        expect(file.original_filename).to eq('untitled')
        expect(file.content_type).to eq('application/octet-stream')
      end

      it "should handle xsd like types from xml (ActiveSupport Compatible)" do
        bacon_xml = <<-EOT
        <bacon>
          <weight type="double">0.5</weight>
          <price type="decimal">12.50</price>
          <chunky type="boolean"> 1 </chunky>
          <expires-at type="dateTime">2007-12-25T12:34:56+0000</expires-at>
          <notes type="string"></notes>
          <illustration type="base64Binary">YmFiZS5wbmc=</illustration>
        </bacon>
        EOT

        expected_bacon_hash = {
          'weight' => 0.5,
          'chunky' => true,
          'price' => BigDecimal("12.50"),
          'expires_at' => Time.utc(2007,12,25,12,34,56),
          'notes' => "",
          'illustration' => "babe.png"
        }

        expect(parse(bacon_xml)["bacon"]).to eq(expected_bacon_hash)
      end

      it "should let type trickle through when unknown (ActiveSupport Compatible)" do
        product_xml = <<-EOT
        <product>
          <weight type="double">0.5</weight>
          <image type="ProductImage"><filename>image.gif</filename></image>

        </product>
        EOT

        expected_product_hash = {
          'weight' => 0.5,
          'image' => {'@type' => 'ProductImage', 'filename' => 'image.gif' },
        }

        expect(parse(product_xml)["product"]).to eq(expected_product_hash)
      end

      it "should handle unescaping from xml (ActiveResource Compatible)" do
       xml_string = '<person><bare-string>First &amp; Last Name</bare-string><pre-escaped-string>First &amp;amp; Last Name</pre-escaped-string></person>'
       expected_hash = {
         'bare_string'        => 'First & Last Name',
         'pre_escaped_string' => 'First &amp; Last Name'
       }

       expect(parse(xml_string)['person']).to eq(expected_hash)
     end

      it "handle an empty xml string" do
        expect(parse('')).to eq({})
      end

      # As returned in the response body by the unfuddle XML API when creating objects
      it "handle an xml string containing a single space" do
        expect(parse(' ')).to eq({})
      end

      context "empty tags with the default options" do

        it "returns the prefixed attributes for a whitespace-only tag with attributes" do
          expect(parse('<foo bar="baz">   </foo>')).to eq("foo" => { "@bar" => "baz" })
        end

        it "returns the prefixed attributes when xsi:nil is combined with other attributes" do
          xml = '<foo xsi:nil="true" bar="baz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>'
          expect(parse(xml)).to eq("foo" => { "@bar" => "baz" })
        end

        it "ignores :empty_tag_value for an empty tag with attributes" do
          expect(parse('<foo bar="baz"/>', :empty_tag_value => "")).to eq("foo" => { "@bar" => "baz" })
        end

        it "applies :empty_tag_value to an empty tag with an explicit xsi:nil" do
          xml = '<foo xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>'
          expect(parse(xml, :empty_tag_value => "")).to eq("foo" => "")
        end
      end

      context "empty tags with :consistent_empty_tags" do

        it "returns :empty_tag_value for an empty tag with attributes" do
          expect(parse('<foo bar="baz"/>', :consistent_empty_tags => true)).to eq("foo" => nil)
        end

        it "returns :empty_tag_value for a whitespace-only tag with attributes" do
          expect(parse('<foo bar="baz">   </foo>', :consistent_empty_tags => true)).to eq("foo" => nil)
        end

        it "returns :empty_tag_value for an empty tag without attributes" do
          expect(parse('<foo/>', :consistent_empty_tags => true)).to eq("foo" => nil)
        end

        context "and :empty_tag_value set to an empty string" do

          it "returns the string for an empty tag without attributes" do
            expect(parse_consistent("<foo/>")).to eq("foo" => "")
          end

          it "attaches the attributes of an empty tag to the string" do
            value = parse_consistent('<foo bar="baz"/>')["foo"]
            expect(value).to eq("")
            expect(value).to be_a(Nori::StringWithAttributes)
            expect(value.attributes).to eq("bar" => "baz")
          end

          it "attaches the attributes of a whitespace-only tag to the string" do
            value = parse_consistent('<foo bar="baz">   </foo>')["foo"]
            expect(value).to eq("")
            expect(value.attributes).to eq("bar" => "baz")
          end

          it "returns nil for an explicit xsi:nil" do
            xml = '<foo xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>'
            expect(parse_consistent(xml)).to eq("foo" => nil)
          end

          it "returns nil for an explicit xsi:nil combined with other attributes" do
            xml = '<foo xsi:nil="true" bar="baz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>'
            expect(parse_consistent(xml)).to eq("foo" => nil)
          end

          it "does not change tags with text" do
            expect(parse_consistent("<foo>text</foo>")).to eq("foo" => "text")
          end

          it "does not change tags with children" do
            # the attribute would be lost if the empty-tag rule also covered elements with children
            xml = '<foo bar="baz"><child>x</child></foo>'
            expect(parse_consistent(xml)).to eq("foo" => { "@bar" => "baz", "child" => "x" })
          end

          it "does not change empty tags typed as array" do
            expect(parse_consistent('<foo type="array"/>')).to eq("foo" => [])
          end

          def parse_consistent(xml)
            parse(xml, :consistent_empty_tags => true, :empty_tag_value => "")
          end
        end
      end

      context "whitespace-only CDATA content" do
        it "keeps whitespace-only CDATA instead of stripping it to nil" do
          expect(parse('<foo><![CDATA[   ]]></foo>')).to eq("foo" => "   ")
        end

        it "attaches the attributes of a whitespace-only CDATA tag to the string" do
          value = parse('<foo bar="baz"><![CDATA[   ]]></foo>')["foo"]
          expect(value).to eq("   ")
          expect(value).to be_a(Nori::StringWithAttributes)
          expect(value.attributes).to eq("bar" => "baz")
        end

        it "keeps CDATA whitespace adjacent to text" do
          expect(parse('<foo>text<![CDATA[  ]]></foo>')).to eq("foo" => "text  ")
        end

        it "still strips whitespace-only plain text to an empty tag" do
          expect(parse('<foo>   </foo>')).to eq("foo" => nil)
        end

        it "still returns non-whitespace CDATA content unchanged" do
          expect(parse('<foo><![CDATA[hi]]></foo>')).to eq("foo" => "hi")
        end
      end

      context "whitespace via character references" do
        # XML 1.0 §4.4.2: a character reference is replaced by its character and
        # processed as ordinary character data. Unlike a CDATA section it is not
        # a literal-data marker, so &#32; is equivalent to a plain space and
        # follows the whitespace rules of the active mode.

        it "strips whitespace-only character references like literal whitespace" do
          expect(parse("<foo>&#32;</foo>")).to eq("foo" => nil)
          expect(parse("<foo>&#x20;</foo>")).to eq("foo" => nil)
          expect(parse("<foo>&#9;&#10;</foo>")).to eq("foo" => nil)
        end

        it "resolves character references inside mixed text" do
          expect(parse("<foo>a&#32;b</foo>")).to eq("foo" => "a b")
        end

        it "treats whitespace-only character references as an empty tag under :standards" do
          expect(parse("<foo>&#32;</foo>", :standards => true)).to eq("foo" => "")
        end

        it "keeps whitespace-only character references under xml:space=\"preserve\"" do
          value = parse('<foo xml:space="preserve">&#32;&#9;</foo>', :standards => true)["foo"]
          expect(value).to eq(" \t")
        end
      end

      context "the :standards profile" do
        # `standards: true` is a profile. It turns on the spec-correct behaviors as a group.

        it "implies :consistent_empty_tags with an empty-string :empty_tag_value" do
          expect(parse("<foo/>", :standards => true)).to eq("foo" => "")
          value = parse('<foo bar="baz"/>', :standards => true)["foo"]
          expect(value).to eq("")
          expect(value).to be_a(Nori::StringWithAttributes)
          expect(value.attributes).to eq("bar" => "baz")
        end

        it "lets an explicit :empty_tag_value override the profile default" do
          expect(parse("<foo/>", :standards => true, :empty_tag_value => nil)).to eq("foo" => nil)
        end

        it "lets an explicit :consistent_empty_tags override the profile default" do
          # without the override the profile would collapse this to ""
          expect(parse('<foo bar="baz"/>', :standards => true, :consistent_empty_tags => false)).
            to eq("foo" => { "@bar" => "baz" })
        end

        it "does not change parsing when it is off (the default)" do
          expect(parse("<foo/>")).to eq("foo" => nil)
        end

        context "xml:space honoring" do
          # XML 1.0 §2.10: xml:space="preserve" makes whitespace significant.
          # The nearest ancestor that sets it wins and "default" resets it.

          it "keeps whitespace-only text under xml:space=\"preserve\"" do
            value = parse('<foo xml:space="preserve">   </foo>', :standards => true)["foo"]
            expect(value).to eq("   ")
            expect(value).to be_a(Nori::StringWithAttributes)
            expect(value.attributes).to eq("xml:space" => "preserve")
          end

          it "inherits preserve to descendants and resets it on xml:space=\"default\"" do
            xml = '<r xml:space="preserve"><keep>  </keep><reset xml:space="default">  </reset></r>'
            result = parse(xml, :standards => true)["r"]
            # both children hold whitespace-only text
            expect(result["keep"]).to eq("  ")
            expect(result["reset"]).to eq("")
          end

          it "still strips whitespace under xml:space=\"preserve\" when the profile is off" do
            expect(parse('<foo xml:space="preserve">   </foo>')).to eq("foo" => { "@xml:space" => "preserve" })
          end
        end

        context "no schema-less typing magic" do
          # Without a schema, character data is just text. The bare type=/nil=
          # attribute conventions come from Rails' Hash.from_xml (via crack),
          # not from any XML spec, so the profile treats them as ordinary
          # attributes and leaves typing to a schema-aware layer.

          it "turns off advanced typecasting" do
            expect(parse("<value>true</value>", :standards => true)).to eq("value" => "true")
            expect(parse("<value>2003-07-16</value>", :standards => true)).to eq("value" => "2003-07-16")
          end

          it "lets an explicit :advanced_typecasting override the profile default" do
            expect(parse("<value>true</value>", :standards => true, :advanced_typecasting => true)).
              to eq("value" => true)
          end

          it "treats a bare type attribute as an ordinary attribute instead of casting" do
            value = parse('<id type="integer">123</id>', :standards => true)["id"]
            expect(value).to eq("123")
            expect(value.attributes).to eq("type" => "integer")
          end

          it "does not fold a bare type=\"array\" element into an Array" do
            xml = '<posts type="array"><post>a</post><post>b</post></posts>'
            expect(parse(xml, :standards => true)).to eq(
              "posts" => { "post" => ["a", "b"], "@type" => "array" }
            )
          end

          it "does not decode a bare type=\"file\" element" do
            value = parse('<doc type="file" name="x.pdf">aGVsbG8=</doc>', :standards => true)["doc"]
            expect(value).to eq("aGVsbG8=")
            expect(value.attributes).to eq("type" => "file", "name" => "x.pdf")
          end

          it "treats a bare nil attribute as an ordinary attribute" do
            value = parse('<foo nil="true"/>', :standards => true)["foo"]
            expect(value).to eq("")
            expect(value.attributes).to eq("nil" => "true")
          end

          it "still honors a prefixed xsi:nil" do
            xml = '<foo xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>'
            expect(parse(xml, :standards => true)).to eq("foo" => nil)
          end
        end
      end

    end
  end

  def parse(xml, options = {})
    defaults = {:parser => parser}
    Nori.new(defaults.merge(options)).parse(xml)
  end
end
