Cu.import("resource://services-sync/ext/Observers.js");
Cu.import("resource://services-sync/identity.js");
Cu.import("resource://services-sync/log4moz.js");
Cu.import("resource://services-sync/resource.js");
Cu.import("resource://services-sync/util.js");

let logger;

function server_open(metadata, response) {
  let body;
  if (metadata.method == "GET") {
    body = "This path exists";
    response.setStatusLine(metadata.httpVersion, 200, "OK");
  } else {
    body = "Wrong request method";
    response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
  }
  response.bodyOutputStream.write(body, body.length);
}

function server_protected(metadata, response) {
  let body;

  // no btoa() in xpcshell.  it's guest:guest
  if (metadata.hasHeader("Authorization") &&
      metadata.getHeader("Authorization") == "Basic Z3Vlc3Q6Z3Vlc3Q=") {
    body = "This path exists and is protected";
    response.setStatusLine(metadata.httpVersion, 200, "OK, authorized");
    response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
  } else {
    body = "This path exists and is protected - failed";
    response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
    response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
  }

  response.bodyOutputStream.write(body, body.length);
}

function server_404(metadata, response) {
  let body = "File not found";
  response.setStatusLine(metadata.httpVersion, 404, "Not Found");
  response.bodyOutputStream.write(body, body.length);
}


let sample_data = {
  some: "sample_data",
  injson: "format",
  number: 42
};

function server_upload(metadata, response) {
  let body;

  let input = readBytesFromInputStream(metadata.bodyInputStream);
  if (input == JSON.stringify(sample_data)) {
    body = "Valid data upload via " + metadata.method;
    response.setStatusLine(metadata.httpVersion, 200, "OK");
  } else {
    body = "Invalid data upload via " + metadata.method + ': ' + input;
    response.setStatusLine(metadata.httpVersion, 500, "Internal Server Error");
  }

  response.bodyOutputStream.write(body, body.length);
}

function server_delete(metadata, response) {
  let body;
  if (metadata.method == "DELETE") {
    body = "This resource has been deleted";
    response.setStatusLine(metadata.httpVersion, 200, "OK");
  } else {
    body = "Wrong request method";
    response.setStatusLine(metadata.httpVersion, 405, "Method Not Allowed");
  }
  response.bodyOutputStream.write(body, body.length);
}

function server_json(metadata, response) {
  let body = JSON.stringify(sample_data);
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.bodyOutputStream.write(body, body.length);
}

const TIMESTAMP = 1274380461;

function server_timestamp(metadata, response) {
  let body = "Thank you for your request";
  response.setHeader("X-Weave-Timestamp", ''+TIMESTAMP, false);
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.bodyOutputStream.write(body, body.length);
}

function server_backoff(metadata, response) {
  let body = "Hey, back off!";
  response.setHeader("X-Weave-Backoff", '600', false);
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.bodyOutputStream.write(body, body.length);  
}

function server_quota_notice(request, response) {
  let body = "You're approaching quota.";
  response.setHeader("X-Weave-Quota-Remaining", '1048576', false);
  response.setStatusLine(request.httpVersion, 200, "OK");
  response.bodyOutputStream.write(body, body.length);  
}

function server_quota_error(request, response) {
  let body = "14";
  response.setHeader("X-Weave-Quota-Remaining", '-1024', false);
  response.setStatusLine(request.httpVersion, 400, "OK");
  response.bodyOutputStream.write(body, body.length);  
}

function server_headers(metadata, response) {
  let ignore_headers = ["host", "user-agent", "accept", "accept-language",
                        "accept-encoding", "accept-charset", "keep-alive",
                        "connection", "pragma", "cache-control",
                        "content-length"];
  let headers = metadata.headers;
  let header_names = [];
  while (headers.hasMoreElements()) {
    let header = headers.getNext().toString();
    if (ignore_headers.indexOf(header) == -1) {
      header_names.push(header);
    }
  }
  header_names = header_names.sort();

  headers = {};
  for each (let header in header_names) {
    headers[header] = metadata.getHeader(header);
  }
  let body = JSON.stringify(headers);
  response.setStatusLine(metadata.httpVersion, 200, "OK");
  response.bodyOutputStream.write(body, body.length);
}


function run_test() {
  logger = Log4Moz.repository.getLogger('Test');
  Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());

  do_test_pending();
  let server = httpd_setup({
    "/open": server_open,
    "/protected": server_protected,
    "/404": server_404,
    "/upload": server_upload,
    "/delete": server_delete,
    "/json": server_json,
    "/timestamp": server_timestamp,
    "/headers": server_headers,
    "/backoff": server_backoff,
    "/quota-notice": server_quota_notice,
    "/quota-error": server_quota_error
  });

  Utils.prefs.setIntPref("network.numRetries", 1); // speed up test

  let did401 = false;
  Observers.add("weave:resource:status:401", function() did401 = true);

  let quotaValue;
  Observers.add("weave:service:quota:remaining",
                function (subject) quotaValue = subject);


  let res_upload = new AsyncResource("http://localhost:8080/upload");
  let res_headers = new AsyncResource("http://localhost:8080/headers");

  _("Resource object memebers");
  let res = new AsyncResource("http://localhost:8080/open");
  do_check_true(res.uri instanceof Ci.nsIURI);
  do_check_eq(res.uri.spec, "http://localhost:8080/open");
  do_check_eq(res.spec, "http://localhost:8080/open");
  do_check_eq(typeof res.headers, "object");
  do_check_eq(typeof res.authenticator, "object");
  // Initially res.data is null since we haven't performed a GET or
  // PUT/POST request yet.
  do_check_eq(res.data, null);

  Utils.asyncChain(function (next) {

    _("GET a non-password-protected resource");
    do_test_pending();
    res.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "This path exists");
      do_check_eq(content.status, 200);
      do_check_true(content.success);
      // res.data has been updated with the result from the request
      do_check_eq(res.data, content);

      // Since we didn't receive proper JSON data, accessing content.obj
      // will result in a SyntaxError from JSON.parse
      let didThrow = false;
      try {
        content.obj;
      } catch (ex) {
        didThrow = true;
      }
      do_check_true(didThrow);

      do_test_finished();
      next();
    }));

  }, function (next) {

    _("Test that the BasicAuthenticator doesn't screw up header case.");
    let res1 = new AsyncResource("http://localhost:8080/foo");
    res1.setHeader("Authorization", "Basic foobar");
    res1.authenticator = new NoOpAuthenticator();
    do_check_eq(res1._headers["authorization"], "Basic foobar");
    do_check_eq(res1.headers["authorization"], "Basic foobar");
    let id = new Identity("secret", "guest", "guest");
    res1.authenticator = new BasicAuthenticator(id);

    // In other words... it correctly overwrites our downcased version
    // when accessed through .headers.
    do_check_eq(res1._headers["authorization"], "Basic foobar");
    do_check_eq(res1.headers["authorization"], "Basic Z3Vlc3Q6Z3Vlc3Q=");
    do_check_eq(res1._headers["authorization"], "Basic Z3Vlc3Q6Z3Vlc3Q=");
    do_check_true(!res1._headers["Authorization"]);
    do_check_true(!res1.headers["Authorization"]);
    next();

  }, function (next) {

    _("GET a password protected resource (test that it'll fail w/o pass, no throw)");
    let res2 = new AsyncResource("http://localhost:8080/protected");
    do_test_pending();
    res2.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_true(did401);
      do_check_eq(content, "This path exists and is protected - failed");
      do_check_eq(content.status, 401);
      do_check_false(content.success);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("GET a password protected resource");
    let auth = new BasicAuthenticator(new Identity("secret", "guest", "guest"));
    let res3 = new AsyncResource("http://localhost:8080/protected");
    res3.authenticator = auth;
    do_check_eq(res3.authenticator, auth);
    do_test_pending();
    res3.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "This path exists and is protected");
      do_check_eq(content.status, 200);
      do_check_true(content.success);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("GET a non-existent resource (test that it'll fail, but not throw)");
    let res4 = new AsyncResource("http://localhost:8080/404");
    do_test_pending();
    res4.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "File not found");
      do_check_eq(content.status, 404);
      do_check_false(content.success);

      // Check some headers of the 404 response
      do_check_eq(content.headers.connection, "close");
      do_check_eq(content.headers.server, "httpd.js");
      do_check_eq(content.headers["content-length"], 14);

      do_test_finished();
      next();
    }));

  }, function (next) {

    _("PUT to a resource (string)");
    do_test_pending();
    res_upload.put(JSON.stringify(sample_data), ensureThrows(function(error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "Valid data upload via PUT");
      do_check_eq(content.status, 200);
      do_check_eq(res_upload.data, content);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("PUT to a resource (object)");
    do_test_pending();
    res_upload.put(sample_data, ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "Valid data upload via PUT");
      do_check_eq(content.status, 200);
      do_check_eq(res_upload.data, content);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("PUT without data arg (uses resource.data) (string)");
    do_test_pending();
    res_upload.data = JSON.stringify(sample_data);
    res_upload.put(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "Valid data upload via PUT");
      do_check_eq(content.status, 200);
      do_check_eq(res_upload.data, content);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("PUT without data arg (uses resource.data) (object)");
    do_test_pending();
    res_upload.data = sample_data;
    res_upload.put(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "Valid data upload via PUT");
      do_check_eq(content.status, 200);
      do_check_eq(res_upload.data, content);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("POST to a resource (string)");
    do_test_pending();
    res_upload.post(JSON.stringify(sample_data), ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "Valid data upload via POST");
      do_check_eq(content.status, 200);
      do_check_eq(res_upload.data, content);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("POST to a resource (object)");
    do_test_pending();
    res_upload.post(sample_data, ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "Valid data upload via POST");
      do_check_eq(content.status, 200);
      do_check_eq(res_upload.data, content);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("POST without data arg (uses resource.data) (string)");
    do_test_pending();
    res_upload.data = JSON.stringify(sample_data);
    res_upload.post(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "Valid data upload via POST");
      do_check_eq(content.status, 200);
      do_check_eq(res_upload.data, content);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("POST without data arg (uses resource.data) (object)");
    do_test_pending();
    res_upload.data = sample_data;
    res_upload.post(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "Valid data upload via POST");
      do_check_eq(content.status, 200);
      do_check_eq(res_upload.data, content);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("DELETE a resource");
    do_test_pending();
    let res6 = new AsyncResource("http://localhost:8080/delete");
    res6.delete(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "This resource has been deleted");
      do_check_eq(content.status, 200);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("JSON conversion of response body");
    do_test_pending();
    let res7 = new AsyncResource("http://localhost:8080/json");
    res7.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, JSON.stringify(sample_data));
      do_check_eq(content.status, 200);
      do_check_eq(JSON.stringify(content.obj), JSON.stringify(sample_data));
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("X-Weave-Timestamp header updates Resource.serverTime");
    do_test_pending();
    // Before having received any response containing the
    // X-Weave-Timestamp header, Resource.serverTime is null.
    do_check_eq(Resource.serverTime, null);
    let res8 = new AsyncResource("http://localhost:8080/timestamp");
    res8.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(Resource.serverTime, TIMESTAMP);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("GET: no special request headers");
    do_test_pending();
    res_headers.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, '{}');
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("PUT: Content-Type defaults to text/plain");
    do_test_pending();
    res_headers.put('data', ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("POST: Content-Type defaults to text/plain");
    do_test_pending();
    res_headers.post('data', ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, JSON.stringify({"content-type": "text/plain"}));
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("setHeader(): setting simple header");
    do_test_pending();
    res_headers.setHeader('X-What-Is-Weave', 'awesome');
    do_check_eq(res_headers.headers['x-what-is-weave'], 'awesome');
    res_headers.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, JSON.stringify({"x-what-is-weave": "awesome"}));
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("setHeader(): setting multiple headers, overwriting existing header");
    do_test_pending();
    res_headers.setHeader('X-WHAT-is-Weave', 'more awesomer');
    res_headers.setHeader('X-Another-Header', 'hello world');
    do_check_eq(res_headers.headers['x-what-is-weave'], 'more awesomer');
    do_check_eq(res_headers.headers['x-another-header'], 'hello world');
    res_headers.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, JSON.stringify({"x-another-header": "hello world",
                                           "x-what-is-weave": "more awesomer"}));
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("Setting headers object");
    do_test_pending();
    res_headers.headers = {};
    res_headers.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, "{}");
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("PUT: override default Content-Type");
    do_test_pending();
    res_headers.setHeader('Content-Type', 'application/foobar');
    do_check_eq(res_headers.headers['content-type'], 'application/foobar');
    res_headers.put('data', ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("POST: override default Content-Type");
    do_test_pending();
    res_headers.post('data', ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content, JSON.stringify({"content-type": "application/foobar"}));
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("X-Weave-Backoff header notifies observer");
    let backoffInterval;
    function onBackoff(subject, data) {
      backoffInterval = subject;
    }
    Observers.add("weave:service:backoff:interval", onBackoff);

    do_test_pending();
    let res10 = new AsyncResource("http://localhost:8080/backoff");
    res10.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(backoffInterval, 600);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("X-Weave-Quota-Remaining header notifies observer on successful requests.");
    do_test_pending();
    let res10 = new AsyncResource("http://localhost:8080/quota-error");
    res10.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content.status, 400);
      do_check_eq(quotaValue, undefined); // HTTP 400, so no observer notification.
      do_test_finished();
      next();
    }));

  }, function (next) {

    do_test_pending();
    let res10 = new AsyncResource("http://localhost:8080/quota-notice");
    res10.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(content.status, 200);
      do_check_eq(quotaValue, 1048576);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("Error handling in ChannelListener etc. preserves exception information");
    do_test_pending();
    let res11 = new AsyncResource("http://localhost:12345/does/not/exist");
    res11.get(ensureThrows(function (error, content) {
      do_check_neq(error, null);
      do_check_eq(error.result, Cr.NS_ERROR_CONNECTION_REFUSED);
      do_check_eq(error.message, "NS_ERROR_CONNECTION_REFUSED");
      do_test_finished();
      next();
    }));

  }, function (next) {

    let redirRequest;
    let redirToOpen = function(subject) {
      subject.newUri = "http://localhost:8080/open";
      redirRequest = subject;
    };
    Observers.add("weave:resource:status:401", redirToOpen);

    _("Notification of 401 can redirect to another uri");
    did401 = false;
    do_test_pending();
    let res12 = new AsyncResource("http://localhost:8080/protected");
    res12.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_eq(res12.spec, "http://localhost:8080/open");
      do_check_eq(content, "This path exists");
      do_check_eq(content.status, 200);
      do_check_true(content.success);
      do_check_eq(res.data, content);
      do_check_true(did401);
      do_check_eq(redirRequest.response, "This path exists and is protected - failed");
      do_check_eq(redirRequest.response.status, 401);
      do_check_false(redirRequest.response.success);

      Observers.remove("weave:resource:status:401", redirToOpen);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("Removing the observer should result in the original 401");
    did401 = false;
    do_test_pending();
    let res13 = new AsyncResource("http://localhost:8080/protected");
    res13.get(ensureThrows(function (error, content) {
      do_check_eq(error, null);
      do_check_true(did401);
      do_check_eq(content, "This path exists and is protected - failed");
      do_check_eq(content.status, 401);
      do_check_false(content.success);
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("Exception handling inside fetches.");
    do_test_pending();
    let res14 = new AsyncResource("http://localhost:8080/json");
    res14._onProgress = function(rec) {
      // Provoke an XPC exception without a Javascript wrapper.
      Svc.IO.newURI("::::::::", null, null);
    };
    let warnings = [];
    res14._log.warn = function(msg) { warnings.push(msg) };

    res14.get(ensureThrows(function (error, content) {
      do_check_eq(error.result, Cr.NS_ERROR_MALFORMED_URI);
      do_check_eq(error.message, "NS_ERROR_MALFORMED_URI");
      do_check_eq(content, null);
      do_check_eq(warnings.pop(),
                  "Got exception calling onProgress handler during fetch of " +
                  "http://localhost:8080/json");
      
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("JS exception handling inside fetches.");
    do_test_pending();
    let res15 = new AsyncResource("http://localhost:8080/json");
    res15._onProgress = function(rec) {
      throw "BOO!";
    };
    let warnings = [];
    res15._log.warn = function(msg) { warnings.push(msg) };

    res15.get(ensureThrows(function (error, content) {
      do_check_eq(error.result, Cr.NS_ERROR_XPC_JS_THREW_STRING);
      do_check_eq(error.message, "NS_ERROR_XPC_JS_THREW_STRING");
      do_check_eq(content, null);
      do_check_eq(warnings.pop(),
                  "Got exception calling onProgress handler during fetch of " +
                  "http://localhost:8080/json");
      
      do_test_finished();
      next();
    }));

  }, function (next) {

    _("Ensure channel timeouts are thrown appropriately.");
    let res19 = new AsyncResource("http://localhost:8080/json");
    res19.ABORT_TIMEOUT = 0;
    res19.get(ensureThrows(function (error, content) {
      do_check_eq(error.result, Cr.NS_ERROR_NET_TIMEOUT);
      next();
    }));

  }, function (next) {

    // Don't quit test harness before server shuts down.
    server.stop(do_test_finished);

  })();

}
