// Copyright 2005, Google Inc.
// All Rights Reserved.
//
// Unit test for the XPath parser and engine.
//
// Author: Steffen Meschkat <mesch@google.com>
//         Junji Takagi <jtakagi@google.com>

//********************************************
// DGF BEWARE!  You MUST update this function if you add tests!
//********************************************
function exposeTestFunctionNames() {
    return ['testParse', 'testEval', 'testAxes', 'testAttributeAsterisk', 'testEvalDom', 'testEvalDomJapanese'];
}

var expr = [
    "@*",
    "@*|node()",
    "/descendant-or-self::div",
    "/div",
    "//div",
    "/descendant-or-self::node()/child::para",
    "substring('12345', 0, 3)",
    "//title | //link",
    "$x//title",
    // "$x/title",  // TODO(mesch): parsing of this expression is broken
    "id('a')//title",
    "//*[@about]",
    "count(descendant::*)",
    "count(descendant::*) + count(ancestor::*)",
    "concat(substring-before(@image,'marker'),'icon',substring-after(@image,'marker'))",
    "@*|text()",
    "*|/",
    "source|destination",
    "$page != 'to' and $page != 'from'",
    "substring-after(icon/@image, '/mapfiles/marker')",
    "substring-before($str, $c)",
    "$page = 'from'",
    "segments/@time",
    "child::para",
    "child::*",
    "child::text()",
    "child::node()",
    "attribute::name",
    "attribute::*",
    "descendant::para",
    "ancestor::div",
    "ancestor-or-self::div",
    "descendant-or-self::para",
    "self::para",
    "child::chapter/descendant::para",
    "child::*/child::para",
    "/",
    "/descendant::para",
    "/descendant::olist/child::item",
    "child::para[position()=1]",
    "child::para[position()=last()]",
    "child::para[position()=last()-1]",
    "child::para[position()>1]",
    "following-sibling::chapter[position()=1]",
    "preceding-sibling::chapter[position()=1]",
    "/descendant::figure[position()=42]",
    "/child::doc/child::chapter[position()=5]/child::section[position()=2]",
    "child::para[attribute::type='warning']",
    "child::para[attribute::type='warning'][position()=5]",
    "child::para[position()=5][attribute::type='warning']",
    "child::chapter[child::title='Introduction']",
    "child::chapter[child::title]",
    "child::*[self::chapter or self::appendix]",
    "child::*[self::chapter or self::appendix][position()=last()]",
    "count(//*[id='u1']|//*[id='u2'])",
    "count(//*[id='u1']|//*[class='u'])",
    "count(//*[class='u']|//*[class='u'])",
    "count(//*[class='u']|//*[id='u1'])",

    // Axis expressions
    "count(//*[@id='self']/ancestor-or-self::*)",
    "count(//*[@id='self']/ancestor::*)",
    "count(//*[@id='self']/attribute::*)",
    "count(//*[@id='self']/child::*)",
    "count(//*[@id='self']/descendant-or-self::*)",
    "count(//*[@id='self']/descendant::*)",
    "count(//*[@id='self']/following-sibling::*)",
    "count(//*[@id='self']/following::*)",
    "//*[@id='self']/parent::*/@id",
    "count(//*[@id='self']/preceding-sibling::*)",
    "count(//*[@id='self']/preceding::*)",
    "//*[@id='self']/self::*/@id",

    // (Japanese)
    "/descendant-or-self::\u90e8\u5206",
    "//\u90e8\u5206",
    "substring('\uff11\uff12\uff13\uff14\uff15', 0, 3)",
    "//\u30bf\u30a4\u30c8\u30eb | //\u30ea\u30f3\u30af",
    "$\u8b0e//\u30bf\u30a4\u30c8\u30eb",
    "//*[@\u30c7\u30b9\u30c6\u30a3\u30cd\u30a4\u30b7\u30e7\u30f3]",
    "concat(substring-before(@\u30a4\u30e1\u30fc\u30b8,'\u76ee\u5370'),'\u30a2\u30a4\u30b3\u30f3',substring-after(@\u30a4\u30e1\u30fc\u30b8,'\u76ee\u5370'))",
    "\u30bd\u30fc\u30b9|\u30c7\u30b9\u30c6\u30a3\u30cd\u30a4\u30b7\u30e7\u30f3",
    "$\u30da\u30fc\u30b8 != '\u307e\u3067' and $\u30da\u30fc\u30b8 != '\u304b\u3089'",
    "substring-after(\u30a2\u30a4\u30b3\u30f3/@\u30a4\u30e1\u30fc\u30b8, '/\u5730\u56f3\u30d5\u30a1\u30a4\u30eb/\u76ee\u5370')",
    "substring-before($\u6587\u5b57\u5217, $\u6587\u5b57)",
    "$\u30da\u30fc\u30b8 = '\u304b\u3089'",
    "\u30bb\u30b0\u30e1\u30f3\u30c8/@\u6642\u523b",
    "child::\u6bb5\u843d",
    "attribute::\u540d\u524d",
    "descendant::\u6bb5\u843d",
    "ancestor::\u90e8\u5206",
    "ancestor-or-self::\u90e8\u5206",
    "descendant-or-self::\u6bb5\u843d",
    "self::\u6bb5\u843d",
    "child::\u7ae0/descendant::\u6bb5\u843d",
    "child::*/child::\u6bb5\u843d",
    "/descendant::\u6bb5\u843d",
    "/descendant::\u9806\u5e8f\u30ea\u30b9\u30c8/child::\u9805\u76ee",
    "child::\u6bb5\u843d[position()=1]",
    "child::\u6bb5\u843d[position()=last()]",
    "child::\u6bb5\u843d[position()=last()-1]",
    "child::\u6bb5\u843d[position()>1]",
    "following-sibling::\u7ae0[position()=1]",
    "preceding-sibling::\u7ae0[position()=1]",
    "/descendant::\u56f3\u8868[position()=42]",
    "/child::\u6587\u66f8/child::\u7ae0[position()=5]/child::\u7bc0[position()=2]",
    "child::\u6bb5\u843d[attribute::\u30bf\u30a4\u30d7='\u8b66\u544a']",
    "child::\u6bb5\u843d[attribute::\u30bf\u30a4\u30d7='\u8b66\u544a'][position()=5]",
    "child::\u6bb5\u843d[position()=5][attribute::\u30bf\u30a4\u30d7='\u8b66\u544a']",
    "child::\u7ae0[child::\u30bf\u30a4\u30c8\u30eb='\u306f\u3058\u3081\u306b']",
    "child::\u7ae0[child::\u30bf\u30a4\u30c8\u30eb]",
    "child::*[self::\u7ae0 or self::\u4ed8\u9332]",
    "child::*[self::\u7ae0 or self::\u4ed8\u9332][position()=last()]",

    //Selenium bugs
    "id('nested1')/div[1]//input[2]",
    "id('foo')//div[contains(@id, 'useful')]//input",
    "(//table[@class='stylee'])//th[text()='theHeaderText']/../td",

    // The following are all expressions that used to occur in google
    // maps XSLT templates.
    "$address",
    "$address=string(/page/user/defaultlocation)",
    "$count-of-snippet-of-url = 0",
    "$daddr",
    "$form",
    "$form = 'from'",
    "$form = 'to'",
    "$form='near'",
    "$home",
    "$i",
    "$i > $page and $i < $page + $range",
    "$i < $page and $i >= $page - $range",
    "$i < @max",
    "$i <= $page",
    "$i + 1",
    "$i = $page",
    "$i = 1",
    "$info = position() or (not($info) and position() = 1)",
    "$is-first-order",
    "$is-first-order and $snippets-exist",
    "$more",
    "$more > 0",
    "$near-point",
    "$page",
    "$page != 'from'",
    "$page != 'to'",
    "$page != 'to' and $page != 'from'",
    "$page > 1",
    "$page = 'basics'",
    "$page = 'details'",
    "$page = 'from'",
    "$page = 'to'",
    "$page='from'",
    "$page='to'",
    "$r >= 0.5",
    "$r >= 1",
    "$r - 0",
    "$r - 1",
    "$r - 2",
    "$r - 3",
    "$r - 4",
    "$saddr",
    "$sources",
    "$sources[position() < $details]",
    "$src",
    "$str",
    "\"'\"",
    "(//location[string(info/references/reference[1]/url)=string($current-url)]/info/references/reference[1])[1]",
    "(not($count-of-snippet-of-url = 0) and (position() = 1) or not($current-url = //locations/location[position() = $last-pos]//reference[1]/url))",
    "(not($info) and position() = 1) or $info = position()",
    ".",
    "../@arg0",
    "../@filterpng",
    "/page/@filterpng",
    "4",
    "@attribution",
    "@id",
    "@max > @num",
    "@meters > 16093",
    "@name",
    "@start div @num + 1",
    "@url",
    "ad",
    "address/line",
    "adsmessage",
    "attr",
    "boolean(location[@id='near'][icon/@image])",
    "bubble/node()",
    "calltoaction/node()",
    "category",
    "contains($str, $c)",
    "count(//location[string(info/references/reference[1]/url)=string($current-url)]//snippet)",
    "count(//snippet)",
    "count(attr)",
    "count(location)",
    "count(structured/source) > 1",
    "description/node()",
    "destination",
    "destinationAddress",
    "domain",
    "false()",
    "icon/@class != 'noicon'",
    "icon/@image",
    "info",
    "info/address/line",
    "info/distance",
    "info/distance and $near-point",
    "info/distance and info/phone and $near-point",
    "info/distance or info/phone",
    "info/panel/node()",
    "info/phone",
    "info/references/reference[1]",
    "info/references/reference[1]/snippet",
    "info/references/reference[1]/url",
    "info/title",
    "info/title/node()",
    "line",
    "location",
    "location[@id!='near']",
    "location[@id='near'][icon/@image]",
    "location[position() > $numlocations div 2]",
    "location[position() <= $numlocations div 2]",
    "locations",
    "locations/location",
    "near",
    "node()",
    "not($count-of-snippets = 0)",
    "not($form = 'from')",
    "not($form = 'near')",
    "not($form = 'to')",
    "not(../@page)",
    "not(structured/source)",
    "notice",
    "number(../@info)",
    "number(../@items)",
    "number(/page/@linewidth)",
    "page/ads",
    "page/directions",
    "page/error",
    "page/overlay",
    "page/overlay/locations/location",
    "page/refinements",
    "page/request/canonicalnear",
    "page/request/near",
    "page/request/query",
    "page/spelling/suggestion",
    "page/user/defaultlocation",
    "phone",
    "position()",
    "position() != 1",
    "position() != last()",
    "position() > 1",
    "position() < $details",
    "position()-1",
    "query",
    "references/@total",
    "references/reference",
    "references/reference/domain",
    "references/reference/url",
    "reviews/@positive div (reviews/@positive + reviews/@negative) * 5",
    "reviews/@positive div (reviews/@positive + reviews/@negative) * (5)",
    "reviews/@total",
    "reviews/@total > 1",
    "reviews/@total > 5",
    "reviews/@total = 1",
    "segments/@distance",
    "segments/@time",
    "segments/segment",
    "shorttitle/node()",
    "snippet",
    "snippet/node()",
    "source",
    "sourceAddress",
    "sourceAddress and destinationAddress",
    "string(../@daddr)",
    "string(../@form)",
    "string(../@page)",
    "string(../@saddr)",
    "string(info/title)",
    "string(page/request/canonicalnear) != ''",
    "string(page/request/near) != ''",
    "string-length($address) > $linewidth",
    "structured/@total - $details",
    "structured/source",
    "structured/source[@name]",
    "substring($address, 1, $linewidth - 3)",
    "substring-after($str, $c)",
    "substring-after(icon/@image, '/mapfiles/marker')",
    "substring-before($str, $c)",
    "tagline/node()",
    "targetedlocation",
    "title",
    "title/node()",
    "true()",
    "url",
    "visibleurl"
];

function testParse() {
  for (var i = 0; i < expr.length; ++i) {
    assert(expr[i], xpathParse(expr[i]));
  }
}

var numExpr = [
    /* number expressions */
    [ "1+1", 2 ],
    [ "floor( -3.1415 )", -4 ],
    [ "-5 mod -2", -1 ],
    [ "-5 mod 2", -1 ],
    [ "5 mod -2", 1 ],
    [ "5 mod 2", 1 ],
    [ "ceiling( 3.1415 )", 4.0 ],
    [ "floor( 3.1415 )", 3.0 ],
    [ "ceiling( -3.1415 )", -3.0 ],
    /* string expressions */
    [ "substring('12345', -42, 1 div 0)", "12345" ],
    [ "normalize-space( '  qwerty ' )", "qwerty" ],
    [ "contains('1234567890','9')", true ],
    [ "contains('1234567890','1')", true ],
    [ "'Hello World!'", 'Hello World!' ],
    [ "substring('12345', 1.5, 2.6)", "234" ],
    [ "substring('12345', 0, 3)", "12" ],
    /* string expressions (Japanese) */
    [ "substring('\u3042\u3044\u3046\u3048\u304a', -42, 1 div 0)",
      "\u3042\u3044\u3046\u3048\u304a" ],
    [ "normalize-space( '  \u3044\u308d\u306f\u306b\u307b\u3078\u3068 ' )",
      "\u3044\u308d\u306f\u306b\u307b\u3078\u3068" ],
    [ "contains('\u5357\u7121\u5999\u6cd5\u9023\u83ef\u7d4c','\u7d4c')",
      true ],
    [ "contains('\u5357\u7121\u5999\u6cd5\u9023\u83ef\u7d4c','\u5357')",
      true ],
    [ "'\u3053\u3093\u306b\u3061\u306f\u3001\u4e16\u754c\uff01'",
      '\u3053\u3093\u306b\u3061\u306f\u3001\u4e16\u754c\uff01' ],
    [ "substring('\uff11\uff12\uff13\uff14\uff15', 1.5, 2.6)",
      "\uff12\uff13\uff14" ],
    [ "substring('\uff11\uff12\uff13\uff14\uff15', 0, 3)",
      "\uff11\uff12" ],
    /* selenium bug SEL-347, AJAXSLT issue 19 */
    [ "count(//a[@href=\"javascript:doFoo('a', 'b')\"])", 1 ],
    /* variables */
    [ "$foo", 'bar', { foo: 'bar' } ],
    [ "$foo", 100, { foo: 100 } ],
    [ "$foo", true, { foo: true } ],
    [ "$foo + 1", 101, { foo: 100 } ],
    /* variables (Japanese) */
    [ "$\u307b\u3052", '\u307b\u3048', { \u307b\u3052: '\u307b\u3048' } ],
    [ "$\u307b\u3052", 100, { \u307b\u3052: 100 } ],
    [ "$\u307b\u3052", true, { \u307b\u3052: true } ],
    [ "$\u307b\u3052 + 1", 101, { \u307b\u3052: 100 } ],
    /* functions */
    // function id() with string argument
    [ "count(id('test1'))", 1 ],
    // function id() with node-set argument
    [ "count(id(//*[@id='testid']))", 1 ],
    /* union expressions */
    [ "count(//*[@id='u1'])", 1 ],
    [ "count(//*[@class='u'])", 3 ],
    [ "count(//*[@id='u1']|//*[@id='u2'])", 2 ],
    [ "count(//*[@id='u1']|//*[@class='u'])", 3 ],
    [ "count(//*[@class='u']|//*[@class='u'])", 3 ],
    [ "count(//*[@class='u']|//*[@id='u1'])", 3 ]
];

function testEval() {
  for (var i = 0; i < numExpr.length; ++i) {
    var ctx = new ExprContext(document.body);
    var e = numExpr[i];
    if (e[2]) {
      for (var k in e[2]) {
        var v = e[2][k];
        if (typeof v == 'number') {
          ctx.setVariable(k, new NumberValue(v));
        } else if (typeof v == 'string') {
          ctx.setVariable(k, new StringValue(v));
        } else if (typeof v == 'boolean') {
          ctx.setVariable(k, new BooleanValue(v));
        }
      }
    }
    var result = xpathParse(e[0]).evaluate(ctx);
    if (typeof e[1] == 'number') {
      assertEquals(e[0], e[1], result.numberValue());
    } else if (typeof e[1] == 'string') {
      assertEquals(e[0], e[1], result.stringValue());
    } else if (typeof e[1] == 'boolean') {
      assertEquals(e[0], e[1], result.booleanValue());
    }
  }
}

// For the following axis expressions, we need full control over the
// entire document, so we cannot evaluate them against document.body,
// but use our own XML document here. We verify that they give the
// right results by counting the nodes in their result node sets. For
// the axes that contain only one node, we check that we found the
// right node using the id. For axes that contain elements, we only
// count the elements, so we don't have to worry about whitespace
// normalization for the text nodes.
var axisTests = [
    [ "count(//*[@id='self']/ancestor-or-self::*)", 3 ],
    [ "count(//*[@id='self']/ancestor::*)", 2 ],
    [ "count(//*[@id='self']/attribute::node())", 1 ],
    [ "count(//*[@id='self']/child::*)", 1 ],
    [ "count(//*[@id='self']/descendant-or-self::*)", 3 ],
    [ "count(//*[@id='self']/descendant::*)", 2 ],
    [ "count(//*[@id='self']/following-sibling::*)", 3 ],
    [ "count(//*[@id='self']/@*/following-sibling::*)", 0 ],
    [ "count(//*[@id='self']/following::*)", 4 ],
    [ "//*[@id='self']/parent::*/@id", "parent" ],
    [ "count(/parent::*)", 0 ],
    [ "count(//*[@id='self']/preceding-sibling::*)", 1 ],
    [ "count(//*[@id='self']/@*/preceding-sibling::*)", 0 ],
    [ "count(//*[@id='self']/preceding::*)", 2 ],
    [ "//*[@id='self']/self::*/@id", "self" ]
];

function testAxes() {
  var xml = [
      '<page>',
      ' <p></p>',
      ' <list id="parent">',
      '  <item></item>',
      '  <item id="self"><d><d></d></d></item>',
      '  <item></item>',
      '  <item></item>',
      '  <item></item>',
      ' </list>',
      ' <f></f>',
      '</page>'
  ].join("");
  var ctx = new ExprContext(xmlParse(xml));
  for (var i = 0; i < axisTests.length; ++i) {
    var e = axisTests[i];
    var result = xpathParse(e[0]).evaluate(ctx);
    if (typeof e[1] == 'number') {
      assertEquals(e[0], e[1], result.numberValue());
    } else if (typeof e[1] == 'string') {
      assertEquals(e[0], e[1], result.stringValue());
    } else if (typeof e[1] == 'boolean') {
      assertEquals(e[0], e[1], result.booleanValue());
    }
  }
}


function testAttributeAsterisk() {
  var ctx = new ExprContext(xmlParse('<x a="1" b="1"><y><z></z></y></x>'));
  var expr = xpathParse("count(/x/@*)");
  assertEquals(2, expr.evaluate(ctx).numberValue());
}


// eval an xpath expression to a single node
function evalNodeSet(expr, ctx) {
  var expr1 = xpathParse(expr);
  var e = expr1.evaluate(ctx);
  return e.nodeSetValue();
}

function testEvalDom() {
  var xml = [
      '<page>',
      '<request>',
      '<q>new york</q>',
      '</request>',
      '<location lat="100" lon="200"/>',
      '</page>'
  ].join('');

  doTestEvalDom(xml, 'page', 'location', 'lat', '100', 'lon', '200');
}

function testEvalDomJapanese() {
  var xml = [
      '<\u30da\u30fc\u30b8>',
      '<\u30ea\u30af\u30a8\u30b9\u30c8>',
      '<\u30af\u30a8\u30ea>\u6771\u4eac</\u30af\u30a8\u30ea>',
      '</\u30ea\u30af\u30a8\u30b9\u30c8>',
      '<\u4f4d\u7f6e \u7def\u5ea6="\u4e09\u5341\u4e94" ',
      "\u7d4c\u5ea6='\u767e\u56db\u5341'/>",
      '</\u30da\u30fc\u30b8>'
  ].join('');

  doTestEvalDom(xml, '\u30da\u30fc\u30b8', '\u4f4d\u7f6e',
                '\u7def\u5ea6', '\u4e09\u5341\u4e94',
                '\u7d4c\u5ea6', '\u767e\u56db\u5341');
}

function doTestEvalDom(xml, page, location, lat, latValue, lon, lonValue) {
  var slashPage = '/' + page;
  var slashPageLocationAtLat = '/' + page + '/' + location + '/@' + lat;
  var slashPageLocationAtLon = '/' + page + '/' + location + '/@' + lon;

  var ctx = new ExprContext(xmlParse(xml));
  // DGF if we have access to an official DOMParser, compare output with that also
  var ctx1;
  if (typeof(DOMParser) != 'undefined') {
    ctx1 = new ExprContext((new DOMParser).parseFromString(xml, 'text/xml'));
  } else {
    ctx1 = ctx;
  }

  var ns = evalNodeSet(page, ctx);
  assertEquals(page, ns.length, 1);

  ns = evalNodeSet(page, ctx1);
  assertEquals(page, ns.length, 1);

  ns = evalNodeSet(slashPage, ctx);
  assertEquals(slashPage, ns.length, 1);

  ns = evalNodeSet(slashPage, ctx1);
  assertEquals(slashPage, ns.length, 1);

  assertEquals('/', evalNodeSet('/', ctx).length, 1);
  assertEquals('/', evalNodeSet('/', ctx1).length, 1);

  assertEquals('/', evalNodeSet('/', ctx)[0].nodeName, '#document');
  assertEquals('/', evalNodeSet('/', ctx1)[0].nodeName, '#document');

  assertEquals(slashPage, evalNodeSet(slashPage, ctx)[0].nodeName, page);
  assertEquals(slashPage, evalNodeSet(slashPage, ctx1)[0].nodeName, page);

  var n = evalNodeSet(slashPageLocationAtLat, ctx)[0];
  assertEquals(slashPageLocationAtLat, n.nodeName, lat);
  assertEquals(slashPageLocationAtLat, n.nodeValue, latValue);

  n = evalNodeSet(slashPageLocationAtLat, ctx1)[0];
  assertEquals(slashPageLocationAtLat, n.nodeName, lat);
  assertEquals(slashPageLocationAtLat, n.nodeValue, latValue);

  var n = evalNodeSet(slashPageLocationAtLon, ctx)[0];
  assertEquals(slashPageLocationAtLon, n.nodeName, lon);
  assertEquals(slashPageLocationAtLon, n.nodeValue, lonValue);

  n = evalNodeSet(slashPageLocationAtLon, ctx1)[0];
  assertEquals(slashPageLocationAtLon, n.nodeName, lon);
  assertEquals(slashPageLocationAtLon, n.nodeValue, lonValue);
}
