/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */

Cu.import("resource://services-common/async.js");
Cu.import("resource://services-common/tokenserverclient.js");

function run_test() {
  initTestLogging("Trace");

  run_next_test();
}

add_test(function test_working_bid_exchange() {
  _("Ensure that working BrowserID token exchange works as expected.");

  let service = "http://example.com/foo";
  let duration = 300;

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      Assert.ok(request.hasHeader("accept"));
      Assert.ok(!request.hasHeader("x-conditions-accepted"));
      Assert.equal("application/json", request.getHeader("accept"));

      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "application/json");

      let body = JSON.stringify({
        id:           "id",
        key:          "key",
        api_endpoint: service,
        uid:          "uid",
        duration,
      });
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let cb = Async.makeSpinningCallback();
  let url = server.baseURI + "/1.0/foo/1.0";
  client.getTokenFromBrowserIDAssertion(url, "assertion", cb);
  let result = cb.wait();
  Assert.equal("object", typeof(result));
  do_check_attribute_count(result, 6);
  Assert.equal(service, result.endpoint);
  Assert.equal("id", result.id);
  Assert.equal("key", result.key);
  Assert.equal("uid", result.uid);
  Assert.equal(duration, result.duration);
  server.stop(run_next_test);
});

add_test(function test_invalid_arguments() {
  _("Ensure invalid arguments to APIs are rejected.");

  let args = [
    [null, "assertion", function() {}],
    ["http://example.com/", null, function() {}],
    ["http://example.com/", "assertion", null]
  ];

  for (let arg of args) {
    try {
      let client = new TokenServerClient();
      client.getTokenFromBrowserIDAssertion(arg[0], arg[1], arg[2]);
      do_throw("Should never get here.");
    } catch (ex) {
      Assert.ok(ex instanceof TokenServerClientError);
    }
  }

  run_next_test();
});

add_test(function test_conditions_required_response_handling() {
  _("Ensure that a conditions required response is handled properly.");

  let description = "Need to accept conditions";
  let tosURL = "http://example.com/tos";

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      Assert.ok(!request.hasHeader("x-conditions-accepted"));

      response.setStatusLine(request.httpVersion, 403, "Forbidden");
      response.setHeader("Content-Type", "application/json");

      let body = JSON.stringify({
        errors: [{description, location: "body", name: ""}],
        urls: {tos: tosURL}
      });
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";

  function onResponse(error, token) {
    Assert.ok(error instanceof TokenServerClientServerError);
    Assert.equal(error.cause, "conditions-required");
    // Check a JSON.stringify works on our errors as our logging will try and use it.
    Assert.ok(JSON.stringify(error), "JSON.stringify worked");
    Assert.equal(null, token);

    Assert.equal(error.urls.tos, tosURL);

    server.stop(run_next_test);
  }

  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse);
});

add_test(function test_invalid_403_no_content_type() {
  _("Ensure that a 403 without content-type is handled properly.");

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      response.setStatusLine(request.httpVersion, 403, "Forbidden");
      // No Content-Type header by design.

      let body = JSON.stringify({
        errors: [{description: "irrelevant", location: "body", name: ""}],
        urls: {foo: "http://bar"}
      });
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";

  function onResponse(error, token) {
    Assert.ok(error instanceof TokenServerClientServerError);
    Assert.equal(error.cause, "malformed-response");
    Assert.equal(null, token);

    Assert.equal(null, error.urls);

    server.stop(run_next_test);
  }

  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse);
});

add_test(function test_invalid_403_bad_json() {
  _("Ensure that a 403 with JSON that isn't proper is handled properly.");

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      response.setStatusLine(request.httpVersion, 403, "Forbidden");
      response.setHeader("Content-Type", "application/json; charset=utf-8");

      let body = JSON.stringify({
        foo: "bar"
      });
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";

  function onResponse(error, token) {
    Assert.ok(error instanceof TokenServerClientServerError);
    Assert.equal(error.cause, "malformed-response");
    Assert.equal(null, token);
    Assert.equal(null, error.urls);

    server.stop(run_next_test);
  }

  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse);
});

add_test(function test_403_no_urls() {
  _("Ensure that a 403 without a urls field is handled properly.");

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      response.setStatusLine(request.httpVersion, 403, "Forbidden");
      response.setHeader("Content-Type", "application/json; charset=utf-8");

      let body = "{}";
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";

  client.getTokenFromBrowserIDAssertion(url, "assertion",
                                        function onResponse(error, result) {
    Assert.ok(error instanceof TokenServerClientServerError);
    Assert.equal(error.cause, "malformed-response");
    Assert.equal(null, result);

    server.stop(run_next_test);

  });
});

add_test(function test_send_extra_headers() {
  _("Ensures that the condition acceptance header is sent when asked.");

  let duration = 300;
  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      Assert.ok(request.hasHeader("x-foo"));
      Assert.equal(request.getHeader("x-foo"), "42");

      Assert.ok(request.hasHeader("x-bar"));
      Assert.equal(request.getHeader("x-bar"), "17");

      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "application/json");

      let body = JSON.stringify({
        id:           "id",
        key:          "key",
        api_endpoint: "http://example.com/",
        uid:          "uid",
        duration,
      });
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";

  function onResponse(error, token) {
    Assert.equal(null, error);

    // Other tests validate other things.

    server.stop(run_next_test);
  }

  let extra = {
    "X-Foo": 42,
    "X-Bar": 17
  };
  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse, extra);
});

add_test(function test_error_404_empty() {
  _("Ensure that 404 responses without proper response are handled properly.");

  let server = httpd_setup();

  let client = new TokenServerClient();
  let url = server.baseURI + "/foo";
  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
    Assert.ok(error instanceof TokenServerClientServerError);
    Assert.equal(error.cause, "malformed-response");

    Assert.notEqual(null, error.response);
    Assert.equal(null, r);

    server.stop(run_next_test);
  });
});

add_test(function test_error_404_proper_response() {
  _("Ensure that a Cornice error report for 404 is handled properly.");

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      response.setStatusLine(request.httpVersion, 404, "Not Found");
      response.setHeader("Content-Type", "application/json; charset=utf-8");

      let body = JSON.stringify({
        status: 404,
        errors: [{description: "No service", location: "body", name: ""}],
      });

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

  function onResponse(error, token) {
    Assert.ok(error instanceof TokenServerClientServerError);
    Assert.equal(error.cause, "unknown-service");
    Assert.equal(null, token);

    server.stop(run_next_test);
  }

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";
  client.getTokenFromBrowserIDAssertion(url, "assertion", onResponse);
});

add_test(function test_bad_json() {
  _("Ensure that malformed JSON is handled properly.");

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "application/json");

      let body = '{"id": "id", baz}';
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";
  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
    Assert.notEqual(null, error);
    Assert.equal("TokenServerClientServerError", error.name);
    Assert.equal(error.cause, "malformed-response");
    Assert.notEqual(null, error.response);
    Assert.equal(null, r);

    server.stop(run_next_test);
  });
});

add_test(function test_400_response() {
  _("Ensure HTTP 400 is converted to malformed-request.");

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      response.setStatusLine(request.httpVersion, 400, "Bad Request");
      response.setHeader("Content-Type", "application/json; charset=utf-8");

      let body = "{}"; // Actual content may not be used.
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";
  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
    Assert.notEqual(null, error);
    Assert.equal("TokenServerClientServerError", error.name);
    Assert.notEqual(null, error.response);
    Assert.equal(error.cause, "malformed-request");

    server.stop(run_next_test);
  });
});

add_test(function test_401_with_error_cause() {
  _("Ensure 401 cause is specified in body.status");

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      response.setStatusLine(request.httpVersion, 401, "Unauthorized");
      response.setHeader("Content-Type", "application/json; charset=utf-8");

      let body = JSON.stringify({status: "no-soup-for-you"});
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let client = new TokenServerClient();
  let url = server.baseURI + "/1.0/foo/1.0";
  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
    Assert.notEqual(null, error);
    Assert.equal("TokenServerClientServerError", error.name);
    Assert.notEqual(null, error.response);
    Assert.equal(error.cause, "no-soup-for-you");

    server.stop(run_next_test);
  });
});

add_test(function test_unhandled_media_type() {
  _("Ensure that unhandled media types throw an error.");

  let server = httpd_setup({
    "/1.0/foo/1.0": function(request, response) {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "text/plain");

      let body = "hello, world";
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let url = server.baseURI + "/1.0/foo/1.0";
  let client = new TokenServerClient();
  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
    Assert.notEqual(null, error);
    Assert.equal("TokenServerClientServerError", error.name);
    Assert.notEqual(null, error.response);
    Assert.equal(null, r);

    server.stop(run_next_test);
  });
});

add_test(function test_rich_media_types() {
  _("Ensure that extra tokens in the media type aren't rejected.");

  let duration = 300;
  let server = httpd_setup({
    "/foo": function(request, response) {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "application/json; foo=bar; bar=foo");

      let body = JSON.stringify({
        id:           "id",
        key:          "key",
        api_endpoint: "foo",
        uid:          "uid",
        duration,
      });
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let url = server.baseURI + "/foo";
  let client = new TokenServerClient();
  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
    Assert.equal(null, error);

    server.stop(run_next_test);
  });
});

add_test(function test_exception_during_callback() {
  _("Ensure that exceptions thrown during callback handling are handled.");

  let duration = 300;
  let server = httpd_setup({
    "/foo": function(request, response) {
      response.setStatusLine(request.httpVersion, 200, "OK");
      response.setHeader("Content-Type", "application/json");

      let body = JSON.stringify({
        id:           "id",
        key:          "key",
        api_endpoint: "foo",
        uid:          "uid",
        duration,
      });
      response.bodyOutputStream.write(body, body.length);
    }
  });

  let url = server.baseURI + "/foo";
  let client = new TokenServerClient();
  let cb = Async.makeSpinningCallback();
  let callbackCount = 0;

  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
    Assert.equal(null, error);

    cb();

    callbackCount += 1;
    throw new Error("I am a bad function!");
  });

  cb.wait();
  // This relies on some heavy event loop magic. The error in the main
  // callback should already have been raised at this point.
  Assert.equal(callbackCount, 1);

  server.stop(run_next_test);
});
