var gClientKeyRaw="TESTCLIENTKEY";
// no btoa() available in xpcshell, precalculated for TESTCLIENTKEY.
var gClientKey = "VEVTVENMSUVOVEtFWQ==";

function MAC(content, clientKey)
{
  var hmac = Cc["@mozilla.org/security/hmac;1"].createInstance(Ci.nsICryptoHMAC);
  var converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
                  createInstance(Ci.nsIScriptableUnicodeConverter);
  converter.charset = "UTF-8";

  var keyObject = Cc["@mozilla.org/security/keyobjectfactory;1"]
    .getService(Ci.nsIKeyObjectFactory).keyFromString(Ci.nsIKeyObject.HMAC, clientKey);
  hmac.init(Ci.nsICryptoHMAC.SHA1, keyObject);

  var data = converter.convertToByteArray(content);
  hmac.update(data, data.length);
  return hmac.finish(true);
}

function doTest(updates, assertions, expectError, clientKey)
{
  if (expectError) {
    doUpdateTest(updates, assertions, updateError, runNextTest, clientKey);
  } else {
    doUpdateTest(updates, assertions, runNextTest, updateError, clientKey);
  }
}

function testFillDb() {
  var add1Urls = [ "zaz.com/a", "yxz.com/c" ];

  var update = "n:1000\n";
  update += "i:test-phish-simple\n";

  var update1 = buildBareUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:data:," + encodeURIComponent(update1) + "\n";

  var assertions = {
    "tableData" : "test-phish-simple;a:1",
    "urlsExist" : add1Urls
  };

  doTest([update], assertions, false);
}

function testSimpleForward() {
  var add1Urls = [ "foo.com/a", "bar.com/c" ];
  var add2Urls = [ "foo.com/b" ];
  var add3Urls = [ "bar.com/d" ];

  var update = "n:1000\n";
  update += "i:test-phish-simple\n";

  var update1 = buildBareUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:data:," + encodeURIComponent(update1) + "\n";

  var update2 = buildBareUpdate(
    [{ "chunkNum" : 2,
       "urls" : add2Urls }]);
  update += "u:data:," + encodeURIComponent(update2) + "\n";

  var update3 = buildBareUpdate(
    [{ "chunkNum" : 3,
       "urls" : add3Urls }]);
  update += "u:data:," + encodeURIComponent(update3) + "\n";

  var assertions = {
    "tableData" : "test-phish-simple;a:1-3",
    "urlsExist" : add1Urls.concat(add2Urls).concat(add3Urls)
  };

  doTest([update], assertions, false);
}

// Make sure that a nested forward (a forward within a forward) causes
// the update to fail.
function testNestedForward() {
  var add1Urls = [ "foo.com/a", "bar.com/c" ];
  var add2Urls = [ "foo.com/b" ];

  var update = "n:1000\n";
  update += "i:test-phish-simple\n";

  var update1 = buildBareUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:data:," + encodeURIComponent(update1) + "\n";

  var update2 = buildBareUpdate(
    [{ "chunkNum" : 2 }]);
  var update3 = buildBareUpdate(
    [{ "chunkNum" : 3,
       "urls" : add1Urls }]);

  update2 += "u:data:," + encodeURIComponent(update3) + "\n";

  update += "u:data:," + encodeURIComponent(update2) + "\n";

  var assertions = {
    "tableData" : "",
    "urlsDontExist" : add1Urls.concat(add2Urls)
  };

  doTest([update], assertions, true);
}

// An invalid URL forward causes the update to fail.
function testInvalidUrlForward() {
  var add1Urls = [ "foo.com/a", "bar.com/c" ];

  var update = buildPhishingUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:asdf://blah/blah\n";  // invalid URL scheme

  // The first part of the update should have succeeded.

  var assertions = {
    "tableData" : "test-phish-simple;a:1",
    "urlsExist" : add1Urls
  };

  doTest([update], assertions, false);
}

// A failed network request causes the update to fail.
function testErrorUrlForward() {
  var add1Urls = [ "foo.com/a", "bar.com/c" ];

  var update = buildPhishingUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:http://test.invalid/asdf/asdf\n";  // invalid URL scheme

  // The first part of the update should have succeeded

  var assertions = {
    "tableData" : "test-phish-simple;a:1",
    "urlsExist" : add1Urls
  };

  doTest([update], assertions, false);
}

function testMultipleTables() {
  var add1Urls = [ "foo.com/a", "bar.com/c" ];
  var add2Urls = [ "foo.com/b" ];
  var add3Urls = [ "bar.com/d" ];

  var update = "n:1000\n";
  update += "i:test-phish-simple\n";

  var update1 = buildBareUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:data:," + encodeURIComponent(update1) + "\n";

  var update2 = buildBareUpdate(
    [{ "chunkNum" : 2,
       "urls" : add2Urls }]);
  update += "u:data:," + encodeURIComponent(update2) + "\n";

  update += "i:test-malware-simple\n";

  var update3 = buildBareUpdate(
    [{ "chunkNum" : 3,
       "urls" : add3Urls }]);
  update += "u:data:," + encodeURIComponent(update3) + "\n";

  var assertions = {
    "tableData" : "test-malware-simple;a:3\ntest-phish-simple;a:1-2",
    "urlsExist" : add1Urls.concat(add2Urls),
    "malwareUrlsExist" : add3Urls
  };

  doTest([update], assertions, false);
}

// Test a simple update with a valid message authentication code.
function testValidMAC() {
  var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
  var update = buildPhishingUpdate(
        [
          { "chunkNum" : 1,
            "urls" : addUrls
          }]);
  update = "m:" + MAC(update, gClientKeyRaw) + "\n" + update;

  var assertions = {
    "tableData" : "test-phish-simple;a:1",
    "urlsExist" : addUrls
  };

  doTest([update], assertions, false, gClientKey);
}

// Test a simple update with an invalid message authentication code.
function testInvalidMAC() {
  var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
  var update = buildPhishingUpdate(
        [
          { "chunkNum" : 1,
            "urls" : addUrls
          }]);
  update = "m:INVALIDMAC\n" + update;

  var assertions = {
    "tableData" : "",
    "urlsDontExist" : addUrls
  };

  doTest([update], assertions, true, gClientKey);
}

// Test a simple  update without a message authentication code, when it is
// expecting one.
function testNoMAC() {
  var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
  var update = buildPhishingUpdate(
        [
          { "chunkNum" : 1,
            "urls" : addUrls
          }]);

  var assertions = {
    "tableData" : "",
    "urlsDontExist" : addUrls
  };

  doTest([update], assertions, true, gClientKey);
}

// Test an update with a valid message authentication code, with forwards.
function testValidForwardMAC() {
  var add1Urls = [ "foo.com/a", "bar.com/c" ];
  var add2Urls = [ "foo.com/b" ];
  var add3Urls = [ "bar.com/d" ];

  var update = "n:1000\n";
  update += "i:test-phish-simple\n";

  var update1 = buildBareUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:data:," + encodeURIComponent(update1) +
    "," + MAC(update1, gClientKeyRaw) + "\n";

  var update2 = buildBareUpdate(
    [{ "chunkNum" : 2,
       "urls" : add2Urls }]);
  update += "u:data:," + encodeURIComponent(update2) +
    "," + MAC(update2, gClientKeyRaw) + "\n";


  var update3 = buildBareUpdate(
    [{ "chunkNum" : 3,
       "urls" : add3Urls }]);
  update += "u:data:," + encodeURIComponent(update3) +
    "," + MAC(update3, gClientKeyRaw) + "\n";

  var assertions = {
    "tableData" : "test-phish-simple;a:1-3",
    "urlsExist" : add1Urls.concat(add2Urls).concat(add3Urls)
  };

  update = "m:" + MAC(update, gClientKeyRaw) + "\n" + update;

  doTest([update], assertions, false, gClientKey);
}

// Test an update with a valid message authentication code, but with
// invalid MACs on the forwards.
function testInvalidForwardMAC() {
  var add1Urls = [ "foo.com/a", "bar.com/c" ];
  var add2Urls = [ "foo.com/b" ];
  var add3Urls = [ "bar.com/d" ];

  var update = "n:1000\n";
  update += "i:test-phish-simple\n";

  var update1 = buildBareUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:data:," + encodeURIComponent(update1) +
    ",BADMAC\n";

  var update2 = buildBareUpdate(
    [{ "chunkNum" : 2,
       "urls" : add2Urls }]);
  update += "u:data:," + encodeURIComponent(update2) +
    ",BADMAC\n";


  var update3 = buildBareUpdate(
    [{ "chunkNum" : 3,
       "urls" : add3Urls }]);
  update += "u:data:," + encodeURIComponent(update3) +
    ",BADMAC\n";

  var assertions = {
    "tableData" : "",
    "urlsDontExist" : add1Urls.concat(add2Urls).concat(add3Urls)
  };

  update = "m:" + MAC(update, gClientKeyRaw) + "\n" + update;

  doTest([update], assertions, true, gClientKey);
}

// Test an update with a valid message authentication code, but no MAC
// specified for sub-urls.
function testNoForwardMAC() {
  var add1Urls = [ "foo.com/a", "bar.com/c" ];
  var add2Urls = [ "foo.com/b" ];
  var add3Urls = [ "bar.com/d" ];

  var update = "n:1000\n";
  update += "i:test-phish-simple\n";

  // XXX : This test presents invalid data: urls as forwards.  A valid
  // data url requires a comma, which the code will interpret as the
  // separator for a MAC.

  // Unfortunately this means that the update will fail even if the code
  // isn't properly detecting a missing MAC update.  I'm not sure how to
  // test that :/

  var update1 = buildBareUpdate(
    [{ "chunkNum" : 1,
       "urls" : add1Urls }]);
  update += "u:data:" + encodeURIComponent(update1) + "\n";

  var update2 = buildBareUpdate(
    [{ "chunkNum" : 2,
       "urls" : add2Urls }]);
  update += "u:data:" + encodeURIComponent(update2) + "\n";

  var update3 = buildBareUpdate(
    [{ "chunkNum" : 3,
       "urls" : add3Urls }]);
  update += "u:data:" + encodeURIComponent(update3) + "\n";

  var assertions = {
    "tableData" : "",
    "urlsDontExist" : add1Urls.concat(add2Urls).concat(add3Urls)
  };

  update = "m:" + MAC(update, gClientKeyRaw) + "\n" + update;

  doTest([update], assertions, true, gClientKey);
}

function Observer(callback) {
  this.observe = callback;
}

Observer.prototype =
{
QueryInterface: function(iid)
{
  if (!iid.equals(Ci.nsISupports) &&
      !iid.equals(Ci.nsIObserver)) {
    throw Cr.NS_ERROR_NO_INTERFACE;
  }
  return this;
}
};

var gGotRekey;

gAssertions.gotRekey = function(data, cb)
{
  do_check_eq(gGotRekey, data);
  cb();
}

// Tests a rekey request.
function testRekey() {
  var addUrls = [ "foo.com/a", "foo.com/b", "bar.com/c" ];
  var update = buildPhishingUpdate(
        [
          { "chunkNum" : 1,
            "urls" : addUrls
          }]);
  update = "e:pleaserekey\n" + update;

  var assertions = {
    "tableData" : "",
    "urlsDontExist" : addUrls,
    "gotRekey" : true
  };

  gGotRekey = false;
  var observerService = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);

  observerService.addObserver(new Observer(function(subject, topic, data) {
        if (topic == "url-classifier-rekey-requested") {
          gGotRekey = true;
        }
      }),
    "url-classifier-rekey-requested",
    false);

  doTest([update], assertions, true, gClientKey);
}

// Tests a database reset request.
function testReset() {
  var addUrls1 = [ "foo.com/a", "foo.com/b" ];
  var update1 = buildPhishingUpdate(
    [
      { "chunkNum" : 1,
        "urls" : addUrls1
      }]);

  var update2 = "n:1000\nr:pleasereset\n";

  var addUrls3 = [ "bar.com/a", "bar.com/b" ];
  var update3 = buildPhishingUpdate(
    [
      { "chunkNum" : 3,
        "urls" : addUrls3
      }]);

  var assertions = {
    "tableData" : "test-phish-simple;a:3",
    "urlsExist" : addUrls3,
    "urlsDontExist" : addUrls1
  };

  doTest([update1, update2, update3], assertions, false);
}


function run_test()
{
  runTests([
    testSimpleForward,
    testNestedForward,
    testInvalidUrlForward,
    testErrorUrlForward,
    testMultipleTables,
    testReset,
    // XXX: we're currently "once MAC, always MAC",
    // so any test not using a MAC must go above
    testValidMAC,
    testInvalidMAC,
    testNoMAC,
    testValidForwardMAC,
    testInvalidForwardMAC,
    testNoForwardMAC,
    testRekey
  ]);
}

do_test_pending();
