# typed: false
normalize_html = lambda { |html| Nokogiri::HTML5.fragment(html.to_s) }

nodes_equal =
  lambda do |a, b|
    return true if a == b
    return false unless a.name == b.name
    stype = a.node_type
    otype = b.node_type
    return false unless stype == otype
    sa = a.attributes
    oa = b.attributes
    return false unless sa.length == oa.length
    sa = sa.sort.map { |n, a| [n, a.value, a.namespace && a.namespace.href] }
    oa = oa.sort.map { |n, a| [n, a.value, a.namespace && a.namespace.href] }
    return false unless sa == oa
    skids = a.children
    okids = b.children
    return false unless skids.length == okids.length
    if stype == Nokogiri::XML::Node::TEXT_NODE &&
         (a.content.strip != b.content.strip)
      return false
    end
    skids.to_enum.with_index.all? { |ski, i| nodes_equal.call(ski, okids[i]) }
  end

RSpec::Matchers.define :eq_html do |expected|
  match do |actual|
    nodes_equal.call(normalize_html.call(expected), normalize_html.call(actual))
  end

  failure_message do |actual|
    "expected HTML to match.\nExpected: #{normalize_html.call(expected)}\nGot:      #{normalize_html.call(actual)}"
  end
end

RSpec::Matchers.define :include_html do |expected|
  match do |actual|
    actual = normalize_html.call(actual)
    expected = normalize_html.call(expected).children.first

    node_or_children_equal =
      lambda do |node|
        nodes_equal.call(node, expected) ||
          node.children.any? { |child| node_or_children_equal.call(child) }
      end

    node_or_children_equal.call(actual)
  end

  failure_message do |actual|
    "expected HTML to include fragment.\nExpected fragment:\n#{normalize_html.call(expected)}\nIn:\n#{normalize_html.call(actual)}"
  end
end
