#!/usr/bin/env ruby
# Ubuntu does not accept arguments to ruby when called using env. To get warnings to show up the -w options is
# required. That can be set in the RUBYOPT environment variable.
# export RUBYOPT=-w

$VERBOSE = true

$: << File.join(File.dirname(__FILE__), '../../lib')
$: << File.join(File.dirname(__FILE__), '../../ext')
$: << File.join(File.dirname(__FILE__), '.')

require 'stringio'
require 'test/unit'
require 'optparse'
require 'helpers'
require 'ox'

opts = OptionParser.new
opts.on('-h', '--help', 'Show this display') { puts opts; Process.exit!(0) }
opts.parse(ARGV)

class SaxSmartTest < Test::Unit::TestCase
  include SaxTestHelpers

  NORMALELEMENTS = {
    'a' => {},
    'abbr' => {},
    'address' => {},
    'article' => {},
    'aside' => {},
    'audio' => {},
    'b' => {},
    'bdi' => {},
    'bdo' => {},
    'blockquote' => {},
    'body' => { 'parents' => ['html'] },
    'button' => {},
    'canvas' => {},
    'caption' => { 'parents' => ['table'] },
    'cite' => {},
    'colgroup' => { 'parents' => ['table'], 'childs' => ['col'] },
    'data' => {},
    'datalist' => { 'childs' => ['option'] },
    'dl' => { 'childs' => ['dt', 'dd'] },
    'dt' => { 'parents' => ['dl'] },
    'dd' => { 'parents' => ['dl'] },
    'del' => {},
    'details' => { 'childs' => ['summary'] },
    'dfn' => {},
    'div' => {},
    'em' => {},
    'fieldset' => {},
    'figcaption' => {},
    'figure' => {},
    'footer' => {},
    'form' => {},
    'h1' => {},
    'h2' => {},
    'h3' => {},
    'h4' => {},
    'h5' => {},
    'h6' => {},
    'head' => { 'parents' => ['html'] },
    'hgroup' => { 'childs' => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] },
    'html' => { 'childs' => ['head', 'body'] },
    'i' => {},
    'iframe' => {},
    'ins' => {},
    'kdb' => {},
    'label' => {},
    'legend' => { 'parents' => ['fieldset'] },
    'li' => { 'parents' => ['ul', 'ol', 'menu'] },
    'map' => {},
    'mark' => {},
    'menu' => { 'childs' => ['li'] },
    'meter' => {},
    'nav' => {},
    'noscript' => {},
    'object' => { 'childs' => ['param'] },
    'ol' => { 'childs' => ['li'] },
    'optgroup' => { 'parents' => ['select'], 'childs' => ['option'] },
    'option' => { 'parents' => ['optgroup', 'select', 'datalist'] },
    'output' => {},
    'p' => {},
    'pre' => {},
    'progress' => {},
    'q' => {},
    'rp' => { 'parents' => ['ruby'] },
    'rt' => { 'parents' => ['ruby'] },
    'ruby' => { 'childs' => ['rt', 'rp'] },
    's' => {},
    'samp' => {},
    'script' => {},
    'section' => {},
    'select' => { 'childs' => ['option', 'optgroup'] },
    'small' => {},
    'span' => {},
    'strong' => {},
    'style' => {},
    'sub' => {},
    'summary' => { 'parents' => ['details'] },
    'sup' => {},
    'table' => { 'childs' => ['caption', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr'] },
    'tbody' => { 'childs' => ['tr'], 'parents' => ['table'] },
    'td' => { 'parents' => ['tr'] },
    'textarea' => {},
    'tfoot' => { 'parents' => ['table'], 'childs' => ['tr'] },
    'th' => { 'parents' => ['tr'] },
    'thead' => { 'childs' => ['tr'], 'parents' => ['table'] },
    'time' => {},
    'title' => { 'parents' => ['head'] },
    'tr' => { 'parents' => ['table', 'thead', 'tbody', 'tfoot'], 'childs' => ['td', 'th'] },
    'u' => {},
    'ul' => { 'childs' => ['li'] },
    'var' => {},
    'video' => {}
  }

  VOIDELEMENTS = {
    'area' => { 'parents' => ['map'] },
    'base' => { 'parents' => ['head'] },
    'br' => {},
    'col' => { 'parents' => ['colgroup'] },
    'command' => { 'parents' => ['colgroup'] },
    'embed' => {},
    'hr' => {},
    'img' => {},
    'input' => {},
    'keygen' => {},
    'link' => {},
    'meta' => { 'parents' => ['head'] },
    'param' => { 'parents' => ['object'] },
    'source' => { 'parents' => ['audio', 'video'] },
    'track' => { 'parents' => ['audio', 'video'] },
    'wbr' => {}
  }

  # Make the :smart => true option the default one
  def smart_parse_compare(xml, expected, handler = AllSax, opts = {}, handler_attr = :calls)
    parse_compare(xml, expected, handler, opts.merge(smart: true, skip: :skip_white), handler_attr)
  end
end

class ErrorsOnParentNormalElementTest < SaxSmartTest
  attr_reader :w

  def initialize(*args)
    @w = lambda { |s, ws| "<#{ws}>" + s + "</#{ws}>" }
    super
  end

  def parents_of(el)
    return [] if el == 'html'

    NORMALELEMENTS[el]['parents'] || ['body']
  end

  def ancestry_of(el, parent = nil)
    p = parent || parents_of(el)[0]
    [el] + (p ? ancestry_of(p) : [])
  end

  def ancestries(el)
    parents_of(el).map { |p| ancestry_of(el, p) }
  end

  def test_no_error_in_dependent_parent_elements
    NORMALELEMENTS.each_key do |el|
      ancestries(el).each do |arr|
        html = arr.reduce(&w)
        smart_parse_compare(html, [], ErrorSax, {}, :errors)
      end
    end
  end
end

class ErrorsOnParentVoidElementTest < SaxSmartTest
  def construct_html(el)
    s = ''
    str = (VOIDELEMENTS[el]['parents'] || []).each do |p|
      s += "<#{p}><#{el}></#{p}>"
    end
    "<html><body>#{str}</body></html>"
  end

  def test_no_error_in_dependent_parent_on_void_elements
    VOIDELEMENTS.each_key do |el|
      smart_parse_compare(construct_html(el), [], ErrorSax, {}, :errors)
    end
  end
end

##
# A class that groups tests concerning DOCTYPE of an html document.
# More info: http://www.w3.org/TR/html5/syntax.html#the-doctype
##
class SaxSmartDoctypeTest < SaxSmartTest
  def test_doctype_html5
    html = %{<!DOCTYPE HTML>}
    smart_parse_compare(html, [[:doctype, ' HTML']])
  end

  def test_doctype_html401_strict
    html = %{<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">}
    smart_parse_compare(html, [[:doctype, ' HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"']])
  end

  def test_doctype_html401_transitional
    html = %{<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">}
    smart_parse_compare(html, [[:doctype, ' HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"']])
  end

  def test_doctype_html401_frameset
    html = %{<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">}
    smart_parse_compare(html, [[:doctype, ' HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd"']])
  end

  def test_doctype_xhtml_10_transitional
    html = %{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">}
    smart_parse_compare(html, [[:doctype, ' html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"']])
  end

  def test_doctype_xhtml_10_strict
    html = %{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">}
    smart_parse_compare(html, [[:doctype, ' html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"']])
  end

  def test_doctype_xhtml_10_frameset
    html = %{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">}
    smart_parse_compare(html, [[:doctype, ' html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"']])
  end

  def test_doctype_xhtml_11
    html = %{<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">}
    smart_parse_compare(html, [[:doctype, ' html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"']])
  end

  def test_doctype_declaration_allcaps
    # allcaps is only required when not smart
    html = %{<!dOCTYPE HTML>}
    parse_compare(html,
                  [[:error, 'Case Error: expected DOCTYPE all in caps', 1, 10],
                   [:doctype, ' HTML']], AllSax)
  end
end

##
# A class that groups tests concerning the normal elements of an html document.
# More info: http://www.w3.org/TR/html5/syntax.html#normal-elements
##
class SaxSmartNormalTagTest < SaxSmartTest
  def test_normal_tags
    html = %{<html class="class1">A terminated text</html>}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:attr, :class, 'class1'],
                         [:text, 'A terminated text'],
                         [:end_element, :html]])
  end

  def test_empty_element
    html = %{<html class="class1"><>A terminated text</html>}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:attr, :class, 'class1'],
                         [:error, 'Invalid Format: empty element', 1, 22],
                         [:text, 'A terminated text'],
                         [:end_element, :html]])
  end

  def test_normal_self_closing_tags
    html = %{<br />}
    smart_parse_compare(html,
                        [[:start_element, :br],
                         [:end_element, :br]])
  end

  def test_with_no_close
    html = %{<html>}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:error, "Start End Mismatch: element 'html' not closed", 1, 6],
                         [:end_element, :html]])
  end

  def test_with_no_close_and_not_terminated_text
    html = %{<html>A not terminated text}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:error, 'Not Terminated: text not terminated', 1, 27],
                         [:text, 'A not terminated text'],
                         [:error, "Start End Mismatch: element 'html' not closed", 1, 27],
                         [:end_element, :html]])
  end

  def test_with_no_close_nested
    html = %{<html>Test <div style='width: 100px;'>A div element</html>}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:text, 'Test '],
                         [:start_element, :div],
                         [:attr, :style, 'width: 100px;'],
                         [:text, 'A div element'],
                         [:error, "Start End Mismatch: element 'html' close does not match 'div' open", 1, 52],
                         [:end_element, :div],
                         [:end_element, :html]])
  end

  def test_with_no_close_with_nested
    html = %{<html><div class='test'>A div element</div>}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:start_element, :div],
                         [:attr, :class, 'test'],
                         [:text, 'A div element'],
                         [:end_element, :div],
                         [:error, "Start End Mismatch: element 'html' not closed", 1, 43],
                         [:end_element, :html]])
  end

  def test_with_no_close_and_not_terminated_text_with_nested
    html = %{<html>A not terminated <div>A div</div> text}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:text, 'A not terminated '],
                         [:start_element, :div],
                         [:text, 'A div'],
                         [:end_element, :div],
                         [:error, 'Not Terminated: text not terminated', 1, 44],
                         [:text, ' text'],
                         [:error, "Start End Mismatch: element 'html' not closed", 1, 44],
                         [:end_element, :html]])
  end

  def test_invalid_element_name
    html = %{
<html>
  <budy>Hello</budy>
</html>
}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:error, 'Invalid Element: budy is not a valid element type for a HTML document type.', 3, 8],
                         [:start_element, :budy],
                         [:text, 'Hello'],
                         [:end_element, :budy],
                         [:end_element, :html]])
  end

  def test_comment_active
    html = %{
<html>
  <body>
    <p>hello</p>
    <!-- a comment -->
  </body>
</html>
}
    overlay = Ox.sax_html_overlay.dup
    overlay.each_key {|k| overlay[k] = :off }
    overlay['!--'] = :active

    handler = AllSax.new
    input = StringIO.new(html)
    options = {
      overlay: overlay,
      skip: :skip_white
    }
    Ox.sax_html(handler, input, options)
    expected = [[:comment, ' a comment ']]
    actual = handler.calls

    puts "\nexpected: #{expected}\n  actual: #{actual}" if expected != actual
    assert_equal(expected, actual)
  end

  def test_nested
    html = %{
<html>
  <body>
    <div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div><div>
Word
    </div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div>
  </body>
</html>
}
    smart_parse_compare(html,
                        [[:start_element, :html], [:start_element, :body]] +
                        ([[:start_element, :div]] * 20) +
                        [[:text, ' Word ']] +
                        ([[:end_element, :div]] * 20) +
                        [[:end_element, :body], [:end_element, :html]])
  end

  # The expected behaviour is to trigger an error and then trigger the start / end events
  # which is not the case for void elements, where only an error is triggered.
  def test_not_opened
    html = '</div>'
    smart_parse_compare(html,
                        [[:error, "Start End Mismatch: element 'div' closed but not opened", 1, 1],
                         [:start_element, :div],
                         [:end_element, :div]])
  end

  def test_not_opened_nested
    html = '<h1 class=a_class>A header</div> here</h1>'
    smart_parse_compare(html,
                        [[:start_element, :h1],
                         [:attr, :class, 'a_class'],
                         [:text, 'A header'],
                         [:error, "Start End Mismatch: element 'div' closed but not opened", 1, 27],
                         [:start_element, :div],
                         [:end_element, :div],
                         [:text, ' here'],
                         [:end_element, :h1]])
  end

  def test_unquoted_slash
    html = '<a href=abc/>def</a>'
    smart_parse_compare(html,
                        [[:start_element, :a],
                         [:attr, :href, 'abc/'],
                         [:text, 'def'],
                         [:end_element, :a]])
  end

  def test_unquoted_slash_no_close
    html = '<a href=abc/>def'
    smart_parse_compare(html,
                        [[:start_element, :a],
                         [:attr, :href, 'abc/'],
                         [:error, 'Not Terminated: text not terminated', 1, 16],
                         [:text, 'def'],
                         [:error, "Start End Mismatch: element 'a' not closed", 1, 16],
                         [:end_element, :a]])
  end
end

##
# A class that groups tests concerning the so called void elements of an html document.
# More info: http://www.w3.org/TR/html5/syntax.html#void-elements
##
# class SaxSmartVoidTagTest < SaxSmartTest
#
#   VOIDELEMENTS = [
#                   #"area", # only in map
#                   #"base", # only in head
#                   "br",
#                   #"col", # only in colgroup
#                   #"command", # not a void tag
#                   "embed",
#                   "hr",
#                   "img",
#                   "input",
#                   "keygen",
#                   #"link", # only in head
#                   #"meta", # only in head
#                   "param",
#                   #"source", # only in media (audio, video)
#                   #"track", # only in media (audio, video)
#                   "wbr"
#                  ]
#
#   def test_closed
#     VOIDELEMENTS.each do |el|
#       html = %{<html>A terminated <#{el}/> text</html>}
#       smart_parse_compare(html,
#                     [
#                      [:start_element, :html],
#                      [:text, "A terminated "],
#                      [:start_element, el.to_sym],
#                      [:end_element, el.to_sym],
#                      [:text, " text"],
#                      [:end_element, :html]
#                     ])
#     end
#   end
#
#   def test_not_closed
#     VOIDELEMENTS.each do |el|
#       html = %{<html>A terminated <#{el}> text</html>}
#       smart_parse_compare(html,
#                     [
#                      [:start_element, :html],
#                      [:text, "A terminated "],
#                      [:start_element, el.to_sym],
#                      [:end_element, el.to_sym],
#                      [:text, " text"],
#                      [:end_element, :html]
#                     ])
#     end
#   end
#
#   # Fix this error to be supported
#   def test_invalid_syntax
#     VOIDELEMENTS.each do |el|
#       html = %{<html>A terminated <#{el}>nice\n</#{el}> text</html>}
#       smart_parse_compare(html,
#                     [
#                      [:start_element, :html],
#                      [:text, "A terminated "],
#                      [:start_element, el.to_sym],
#                      [:end_element, el.to_sym],
#                      [:text, "nice\n"],
#                      [:error, "Start End Mismatch: element '#{el}' should not have a separate close element", 2, 1],
#                      [:text, " text"],
#                      [:end_element, :html]
#                     ])
#     end
#   end
#
# end

##
# A class that groups tests concerning the table element. Because this element
# assumes a specific nested structure its testing must be made seperately from
# the normal tags testing.
##
class SaxSmartTableTagTest < SaxSmartTest
  def test_html_table
    html = %{
<html>
  <body>Table
    <table>
      <tr><td>one</td><td>two</td></tr>
      <tr><td>bad<td>bad2</tr>
    </table>
  </body>
</html>
}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:start_element, :body],
                         [:text, 'Table '],
                         [:start_element, :table],
                         [:start_element, :tr],
                         [:start_element, :td],
                         [:text, 'one'],
                         [:end_element, :td],
                         [:start_element, :td],
                         [:text, 'two'],
                         [:end_element, :td],
                         [:end_element, :tr],
                         [:start_element, :tr],
                         [:start_element, :td],
                         [:text, 'bad'],
                         [:error, 'Invalid Element: td can not be nested in a HTML document, closing previous.', 6, 21],
                         [:end_element, :td],
                         [:start_element, :td],
                         [:text, 'bad2'],
                         [:error, "Start End Mismatch: element 'tr' close does not match 'td' open", 6, 26],
                         [:end_element, :td],
                         [:end_element, :tr],
                         [:end_element, :table],
                         [:end_element, :body],
                         [:end_element, :html]])
  end

  def test_html_bad_table
    html = %{
<html>
  <body>
    <table>
      <div>
        <tr><td>one</td></tr>
      </div>
    </table>
  </body>
</html>
}
    smart_parse_compare(html,
                        [[:start_element, :html],
                         [:start_element, :body],
                         [:start_element, :table],
                         [:start_element, :div],
                         [:error, 'Invalid Element: tr can not be a child of a div in a HTML document.', 6, 12],
                         [:start_element, :tr],
                         [:start_element, :td],
                         [:text, 'one'],
                         [:end_element, :td],
                         [:end_element, :tr],
                         [:end_element, :div],
                         [:end_element, :table],
                         [:end_element, :body],
                         [:end_element, :html]])
  end

  def html_parse_compare(xml, expected, opts = {})
    handler = AllSax.new
    input = StringIO.new(xml)
    options = {
      symbolize: true,
      smart: false,
      skip: :skip_white
    }.merge(opts)

    Ox.sax_html(handler, input, options)

    actual = handler.send(:calls)

    puts "\nexpected: #{expected}\n  actual: #{actual}" if expected != actual
    assert_equal(expected, actual)
  end

  def test_nest_ok
    html = %{
<html>
  <body>Table
    <table>
      <tr><td>one<td>two</td></td></tr>
    </table>
  </body>
</html>
}

    hints = Ox.sax_html_overlay
    hints['td'] = :nest_ok
    html_parse_compare(html,
                       [[:start_element, :html],
                        [:start_element, :body],
                        [:text, 'Table '],
                        [:start_element, :table],
                        [:start_element, :tr],
                        [:start_element, :td],
                        [:text, 'one'],
                        [:start_element, :td],
                        [:text, 'two'],
                        [:end_element, :td],
                        [:end_element, :td],
                        [:end_element, :tr],
                        [:end_element, :table],
                        [:end_element, :body],
                        [:end_element, :html]],
                       { overlay: hints })
  end

  def test_nest_ok_auto_closing
    html = %{<html><body><h5>test</body></html>}
    hints = Ox.sax_html_overlay
    hints['h5'] = :nest_ok
    html_parse_compare(html,
                       [[:start_element, :html],
                        [:start_element, :body],
                        [:start_element, :h5],
                        [:text, 'test'],
                        [:error, "Start End Mismatch: element 'body' close does not match 'h5' open", 1, 21],
                        [:end_element, :h5],
                        [:end_element, :body],
                        [:end_element, :html]],
                       { overlay: hints })
  end
end
