require "helper"

module Nokogiri
  module CSS
    class TestParser < Nokogiri::TestCase
      def setup
        super
        @parser = Nokogiri::CSS::Parser.new
        @parser_with_ns = Nokogiri::CSS::Parser.new({
          "xmlns" => "http://default.example.com/",
          "hoge" => "http://hoge.example.com/",
        })
      end

      def test_extra_single_quote
        assert_raises(CSS::SyntaxError) { @parser.parse("'") }
      end

      def test_syntax_error_raised
        assert_raises(CSS::SyntaxError) { @parser.parse("a[x=]") }
      end

      def test_function_and_pseudo
        assert_xpath '//child::text()[position() = 99]', @parser.parse('text():nth-of-type(99)')
      end

      def test_find_by_type
        ast = @parser.parse("a:nth-child(2)").first
        matches = ast.find_by_type(
          [:CONDITIONAL_SELECTOR,
            [:ELEMENT_NAME],
            [:PSEUDO_CLASS,
              [:FUNCTION]
            ]
          ]
        )
        assert_equal(1, matches.length)
        assert_equal(ast, matches.first)
      end

      def test_to_type
        ast = @parser.parse("a:nth-child(2)").first
        assert_equal(
          [:CONDITIONAL_SELECTOR,
            [:ELEMENT_NAME],
            [:PSEUDO_CLASS,
              [:FUNCTION]
            ]
          ], ast.to_type
        )
      end

      def test_to_a
        asts = @parser.parse("a:nth-child(2)")
        assert_equal(
          [:CONDITIONAL_SELECTOR,
            [:ELEMENT_NAME, ["a"]],
            [:PSEUDO_CLASS,
              [:FUNCTION, ["nth-child("], ["2"]]
            ]
          ], asts.first.to_a
        )
      end

      def test_has
        assert_xpath  "//a[b]", @parser.parse("a:has(b)")
        assert_xpath  "//a[b/c]", @parser.parse("a:has(b > c)")
      end

      def test_dashmatch
        assert_xpath  "//a[@class = 'bar' or starts-with(@class, concat('bar', '-'))]",
                      @parser.parse("a[@class|='bar']")
        assert_xpath  "//a[@class = 'bar' or starts-with(@class, concat('bar', '-'))]",
                      @parser.parse("a[@class |= 'bar']")
      end

      def test_includes
        assert_xpath  "//a[contains(concat(\" \", @class, \" \"),concat(\" \", 'bar', \" \"))]",
                      @parser.parse("a[@class~='bar']")
        assert_xpath  "//a[contains(concat(\" \", @class, \" \"),concat(\" \", 'bar', \" \"))]",
                      @parser.parse("a[@class ~= 'bar']")
      end

      def test_function_with_arguments
        assert_xpath  "//a[count(preceding-sibling::*) = 1]",
                      @parser.parse("a[2]")
        assert_xpath  "//a[count(preceding-sibling::*) = 1]",
                      @parser.parse("a:nth-child(2)")
      end

      def test_carrot
        assert_xpath  "//a[starts-with(@id, 'Boing')]",
                      @parser.parse("a[id^='Boing']")
        assert_xpath  "//a[starts-with(@id, 'Boing')]",
                      @parser.parse("a[id ^= 'Boing']")
      end

      def test_suffix_match
        assert_xpath "//a[substring(@id, string-length(@id) - string-length('Boing') + 1, string-length('Boing')) = 'Boing']",
                      @parser.parse("a[id$='Boing']")
        assert_xpath "//a[substring(@id, string-length(@id) - string-length('Boing') + 1, string-length('Boing')) = 'Boing']",
                      @parser.parse("a[id $= 'Boing']")
      end

      def test_attributes_with_at
        ## This is non standard CSS
        assert_xpath  "//a[@id = 'Boing']",
                      @parser.parse("a[@id='Boing']")
        assert_xpath  "//a[@id = 'Boing']",
                      @parser.parse("a[@id = 'Boing']")
      end

      def test_attributes_with_at_and_stuff
        ## This is non standard CSS
        assert_xpath  "//a[@id = 'Boing']//div",
                      @parser.parse("a[@id='Boing'] div")
      end

      def test_not_equal
        ## This is non standard CSS
        assert_xpath  "//a[child::text() != 'Boing']",
                      @parser.parse("a[text()!='Boing']")
        assert_xpath  "//a[child::text() != 'Boing']",
                      @parser.parse("a[text() != 'Boing']")
      end

      def test_function
        ## This is non standard CSS
        assert_xpath  "//a[child::text()]",
                      @parser.parse("a[text()]")

        ## This is non standard CSS
        assert_xpath  "//child::text()",
                      @parser.parse("text()")

        ## This is non standard CSS
        assert_xpath  "//a[contains(child::text(), 'Boing')]",
                      @parser.parse("a[text()*='Boing']")
        assert_xpath  "//a[contains(child::text(), 'Boing')]",
                      @parser.parse("a[text() *= 'Boing']")

        ## This is non standard CSS
        assert_xpath  "//script//comment()",
                      @parser.parse("script comment()")
      end

      def test_nonstandard_nth_selectors
        ## These are non standard CSS
        assert_xpath '//a[position() = 1]',             @parser.parse('a:first()')
        assert_xpath '//a[position() = 1]',             @parser.parse('a:first') # no parens
        assert_xpath '//a[position() = 99]',            @parser.parse('a:eq(99)')
        assert_xpath '//a[position() = 99]',            @parser.parse('a:nth(99)')
        assert_xpath '//a[position() = last()]',        @parser.parse('a:last()')
        assert_xpath '//a[position() = last()]',        @parser.parse('a:last') # no parens
        assert_xpath '//a[node()]',                     @parser.parse('a:parent')
      end

      def test_standard_nth_selectors
        assert_xpath '//a[position() = 1]',             @parser.parse('a:first-of-type()')
        assert_xpath '//a[position() = 1]',             @parser.parse('a:first-of-type') # no parens
        assert_xpath "//a[contains(concat(' ', normalize-space(@class), ' '), ' b ')][position() = 1]",
                     @parser.parse('a.b:first-of-type') # no parens
        assert_xpath '//a[position() = 99]',            @parser.parse('a:nth-of-type(99)') 
        assert_xpath "//a[contains(concat(' ', normalize-space(@class), ' '), ' b ')][position() = 99]",
                     @parser.parse('a.b:nth-of-type(99)')
        assert_xpath '//a[position() = last()]',        @parser.parse('a:last-of-type()')
        assert_xpath '//a[position() = last()]',        @parser.parse('a:last-of-type') # no parens
        assert_xpath "//a[contains(concat(' ', normalize-space(@class), ' '), ' b ')][position() = last()]",
                     @parser.parse('a.b:last-of-type') # no parens
        assert_xpath '//a[position() = last()]',        @parser.parse('a:nth-last-of-type(1)')
        assert_xpath '//a[position() = last() - 98]',   @parser.parse('a:nth-last-of-type(99)')
        assert_xpath "//a[contains(concat(' ', normalize-space(@class), ' '), ' b ')][position() = last() - 98]",
                     @parser.parse('a.b:nth-last-of-type(99)')
      end

      def test_nth_child_selectors
        assert_xpath '//a[count(preceding-sibling::*) = 0]',  @parser.parse('a:first-child')
        assert_xpath '//a[count(preceding-sibling::*) = 98]', @parser.parse('a:nth-child(99)')
        assert_xpath '//a[count(following-sibling::*) = 0]',  @parser.parse('a:last-child')
        assert_xpath '//a[count(following-sibling::*) = 0]',  @parser.parse('a:nth-last-child(1)')
        assert_xpath '//a[count(following-sibling::*) = 98]', @parser.parse('a:nth-last-child(99)')
      end

      def test_miscellaneous_selectors
        assert_xpath '//a[count(preceding-sibling::*) = 0 and count(following-sibling::*) = 0]',  @parser.parse('a:only-child')
        assert_xpath '//a[last() = 1]', @parser.parse('a:only-of-type')
        assert_xpath '//a[not(node())]', @parser.parse('a:empty')
      end

      def test_nth_a_n_plus_b
        assert_xpath '//a[(position() mod 2) = 0]', @parser.parse('a:nth-of-type(2n)')
        assert_xpath '//a[(position() >= 1) and (((position()-1) mod 2) = 0)]', @parser.parse('a:nth-of-type(2n+1)')
        assert_xpath '//a[(position() mod 2) = 0]', @parser.parse('a:nth-of-type(even)')
        assert_xpath '//a[(position() >= 1) and (((position()-1) mod 2) = 0)]', @parser.parse('a:nth-of-type(odd)')
        assert_xpath '//a[(position() >= 3) and (((position()-3) mod 4) = 0)]', @parser.parse('a:nth-of-type(4n+3)')
        assert_xpath '//a[position() <= 3]', @parser.parse('a:nth-of-type(-1n+3)')
        assert_xpath '//a[position() <= 3]', @parser.parse('a:nth-of-type(-n+3)')
        assert_xpath '//a[position() >= 3]', @parser.parse('a:nth-of-type(1n+3)')
        assert_xpath '//a[position() >= 3]', @parser.parse('a:nth-of-type(n+3)')

        assert_xpath '//a[((last()-position()+1) mod 2) = 0]', @parser.parse('a:nth-last-of-type(2n)')
        assert_xpath '//a[((last()-position()+1) >= 1) and ((((last()-position()+1)-1) mod 2) = 0)]', @parser.parse('a:nth-last-of-type(2n+1)')
        assert_xpath '//a[((last()-position()+1) mod 2) = 0]', @parser.parse('a:nth-last-of-type(even)')
        assert_xpath '//a[((last()-position()+1) >= 1) and ((((last()-position()+1)-1) mod 2) = 0)]', @parser.parse('a:nth-last-of-type(odd)')
        assert_xpath '//a[((last()-position()+1) >= 3) and ((((last()-position()+1)-3) mod 4) = 0)]', @parser.parse('a:nth-last-of-type(4n+3)')
        assert_xpath '//a[(last()-position()+1) <= 3]', @parser.parse('a:nth-last-of-type(-1n+3)')
        assert_xpath '//a[(last()-position()+1) <= 3]', @parser.parse('a:nth-last-of-type(-n+3)')
        assert_xpath '//a[(last()-position()+1) >= 3]', @parser.parse('a:nth-last-of-type(1n+3)')
        assert_xpath '//a[(last()-position()+1) >= 3]', @parser.parse('a:nth-last-of-type(n+3)')
      end

      def test_preceding_selector
        assert_xpath  "//E/following-sibling::F",
                      @parser.parse("E ~ F")

        assert_xpath  "//E/following-sibling::F//G",
                      @parser.parse("E ~ F G")
      end

      def test_direct_preceding_selector
        assert_xpath  "//E/following-sibling::*[1]/self::F",
                      @parser.parse("E + F")

        assert_xpath  "//E/following-sibling::*[1]/self::F//G",
                      @parser.parse("E + F G")
      end

      def test_child_selector
        assert_xpath("//a//b/i", @parser.parse('a b>i'))
        assert_xpath("//a//b/i", @parser.parse('a b > i'))
        assert_xpath("//a/b/i", @parser.parse('a > b > i'))
      end

      def test_prefixless_child_selector
        assert_xpath("./a", @parser.parse('>a'))
        assert_xpath("./a", @parser.parse('> a'))
        assert_xpath("./a//b/i", @parser.parse('>a b>i'))
        assert_xpath("./a/b/i", @parser.parse('> a > b > i'))
      end

      def test_prefixless_preceding_sibling_selector
        assert_xpath("./following-sibling::a", @parser.parse('~a'))
        assert_xpath("./following-sibling::a", @parser.parse('~ a'))
        assert_xpath("./following-sibling::a//b/following-sibling::i", @parser.parse('~a b~i'))
        assert_xpath("./following-sibling::a//b/following-sibling::i", @parser.parse('~ a b ~ i'))
      end

      def test_prefixless_direct_adjacent_selector
        assert_xpath("./following-sibling::*[1]/self::a", @parser.parse('+a'))
        assert_xpath("./following-sibling::*[1]/self::a", @parser.parse('+ a'))
        assert_xpath("./following-sibling::*[1]/self::a/following-sibling::*[1]/self::b", @parser.parse('+a+b'))
        assert_xpath("./following-sibling::*[1]/self::a/following-sibling::*[1]/self::b", @parser.parse('+ a + b'))
      end

      def test_attribute
        assert_xpath  "//h1[@a = 'Tender Lovemaking']",
                      @parser.parse("h1[a='Tender Lovemaking']")
      end

      def test_id
        assert_xpath "//*[@id = 'foo']", @parser.parse('#foo')
      end

      def test_pseudo_class_no_ident
        assert_xpath "//*[link(.)]", @parser.parse(':link')
      end

      def test_pseudo_class
        assert_xpath "//a[link(.)]", @parser.parse('a:link')
        assert_xpath "//a[visited(.)]", @parser.parse('a:visited')
        assert_xpath "//a[hover(.)]", @parser.parse('a:hover')
        assert_xpath "//a[active(.)]", @parser.parse('a:active')
        assert_xpath  "//a[active(.) and contains(concat(' ', normalize-space(@class), ' '), ' foo ')]",
                      @parser.parse('a:active.foo')
      end

      def test_significant_space
        assert_xpath "//x//*[count(preceding-sibling::*) = 0]//*[@a]//*[@b]", @parser.parse("x :first-child [a] [b]")
        assert_xpath "//*[@a]//*[@b]", @parser.parse(" [a] [b]")
      end

      def test_star
        assert_xpath "//*", @parser.parse('*')
        assert_xpath "//*[contains(concat(' ', normalize-space(@class), ' '), ' pastoral ')]",
                      @parser.parse('*.pastoral')
      end

      def test_class
        assert_xpath  "//*[contains(concat(' ', normalize-space(@class), ' '), ' a ') and contains(concat(' ', normalize-space(@class), ' '), ' b ')]",
                      @parser.parse('.a.b')
        assert_xpath  "//*[contains(concat(' ', normalize-space(@class), ' '), ' awesome ')]",
                      @parser.parse('.awesome')
        assert_xpath  "//foo[contains(concat(' ', normalize-space(@class), ' '), ' awesome ')]",
                      @parser.parse('foo.awesome')
        assert_xpath  "//foo//*[contains(concat(' ', normalize-space(@class), ' '), ' awesome ')]",
                      @parser.parse('foo .awesome')
      end

      def test_bare_not
        assert_xpath "//*[not(contains(concat(' ', normalize-space(@class), ' '), ' a '))]",
                     @parser.parse(':not(.a)')
      end

      def test_not_so_simple_not
        assert_xpath "//*[@id = 'p' and not(contains(concat(' ', normalize-space(@class), ' '), ' a '))]",
                     @parser.parse('#p:not(.a)')
        assert_xpath "//p[contains(concat(' ', normalize-space(@class), ' '), ' a ') and not(contains(concat(' ', normalize-space(@class), ' '), ' b '))]",
                     @parser.parse('p.a:not(.b)')
        assert_xpath "//p[@a = 'foo' and not(contains(concat(' ', normalize-space(@class), ' '), ' b '))]",
                     @parser.parse("p[a='foo']:not(.b)")
      end

      def test_multiple_not
        assert_xpath "//p[not(contains(concat(' ', normalize-space(@class), ' '), ' a ')) and not(contains(concat(' ', normalize-space(@class), ' '), ' b ')) and not(contains(concat(' ', normalize-space(@class), ' '), ' c '))]",
                    @parser.parse("p:not(.a):not(.b):not(.c)")
      end

      def test_ident
        assert_xpath '//x', @parser.parse('x')
      end

      def test_parse_space
        assert_xpath '//x//y', @parser.parse('x y')
      end

      def test_parse_descendant
        assert_xpath '//x/y', @parser.parse('x > y')
      end

      def test_parse_slash
        ## This is non standard CSS
        assert_xpath '//x/y', @parser.parse('x/y')
      end

      def test_parse_doubleslash
        ## This is non standard CSS
        assert_xpath '//x//y', @parser.parse('x//y')
      end

      def test_multi_path
        assert_xpath ['//x/y', '//y/z'], @parser.parse('x > y, y > z')
        assert_xpath ['//x/y', '//y/z'], @parser.parse('x > y,y > z')
        ###
        # TODO: should we make this work?
        # assert_xpath ['//x/y', '//y/z'], @parser.parse('x > y | y > z')
      end

      def test_attributes_with_namespace
        ## Default namespace is not applied to attributes.
        ## So this must be @class, not @xmlns:class.
        assert_xpath "//xmlns:a[@class = 'bar']", @parser_with_ns.parse("a[class='bar']")
        assert_xpath "//xmlns:a[@hoge:class = 'bar']", @parser_with_ns.parse("a[hoge|class='bar']")
      end

      def assert_xpath expecteds, asts
        expecteds = [expecteds].flatten
        expecteds.zip(asts).each do |expected, actual|
          assert_equal expected, actual.to_xpath
        end
      end
    end
  end
end
