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

const { HttpServer } = ChromeUtils.importESModule(
  "resource://testing-common/httpd.sys.mjs"
);

const BinaryInputStream = Components.Constructor(
  "@mozilla.org/binaryinputstream;1",
  "nsIBinaryInputStream",
  "setInputStream"
);

var server;

function getBaseUrl() {
  return "http://localhost:" + server.identity.primaryPort;
}

// a way to create some test defaults
function createTestData(testPath) {
  return {
    testPath,
    request: {
      headers: {},
      contentType: "application/json",
    },
    response: {
      headers: {},
      contentType: "application/json",
      body: '{"Look": "Success!"}',
      status: 200,
      statusText: "OK",
    },
  };
}

// read body and content type information from a request
function readDataFromRequest(aRequest) {
  let requestData = {};
  if (aRequest.method == "POST" || aRequest.method == "PUT") {
    if (aRequest.bodyInputStream) {
      let inputStream = new BinaryInputStream(aRequest.bodyInputStream);
      let bytes = [];
      let available;

      while ((available = inputStream.available()) > 0) {
        Array.prototype.push.apply(bytes, inputStream.readByteArray(available));
      }

      requestData.body = String.fromCharCode.apply(null, bytes);
      requestData.contentType = aRequest.getHeader("Content-Type");
    }
  }
  return requestData;
}

// write status information, content type information and body to a response
function writeDataToResponse(aData, aResponse) {
  aResponse.setStatusLine(null, aData.status, aData.statusText);
  aResponse.setHeader("Content-Type", aData.contentType);
  for (let header in aData.headers) {
    aResponse.setHeader(header, aData.headers[header]);
  }
  aResponse.write(aData.body);
}

// test some GET functionality
add_test(function test_GetData() {
  do_test_pending();

  let testData = createTestData("/getData");

  // set up some headers to test
  let headerNames = ["x-test-header", "x-other-test-header"];
  for (let headerName of headerNames) {
    testData.request.headers[headerName] = "test-value-for-" + headerName;
  }

  server.registerPathHandler(testData.testPath, function (aRequest, aResponse) {
    try {
      // check our request headers made it OK
      for (let headerName of headerNames) {
        Assert.equal(
          testData.request.headers[headerName],
          aRequest.getHeader(headerName)
        );
      }

      // send a response
      writeDataToResponse(testData.response, aResponse);
    } catch (e) {
      do_report_unexpected_exception(e);
    }
  });

  // fetch, via GET, with some request headers set
  fetch(getBaseUrl() + testData.testPath, { headers: testData.request.headers })
    .then(function (response) {
      // check response looks as expected
      Assert.ok(response.ok);
      Assert.equal(response.status, testData.response.status);
      Assert.equal(response.statusText, testData.response.statusText);

      // check a response header looks OK:
      Assert.equal(
        response.headers.get("Content-Type"),
        testData.response.contentType
      );

      // ... and again to check header names are case insensitive
      Assert.equal(
        response.headers.get("content-type"),
        testData.response.contentType
      );

      // ensure response.text() returns a promise that resolves appropriately
      response.text().then(function (text) {
        Assert.equal(text, testData.response.body);
        do_test_finished();
        run_next_test();
      });
    })
    .catch(function (e) {
      do_report_unexpected_exception(e);
      do_test_finished();
      run_next_test();
    });
});

// test a GET with no init
add_test(function test_GetDataNoInit() {
  do_test_pending();

  let testData = createTestData("/getData");

  server.registerPathHandler(testData.testPath, function (aRequest, aResponse) {
    try {
      // send a response
      writeDataToResponse(testData.response, aResponse);
    } catch (e) {
      do_report_unexpected_exception(e);
    }
  });

  fetch(getBaseUrl() + testData.testPath, { headers: testData.request.headers })
    .then(function (response) {
      // check response looks as expected
      Assert.ok(response.ok);
      Assert.equal(response.status, testData.response.status);

      // ensure response.text() returns a promise that resolves appropriately
      response.text().then(function (text) {
        Assert.equal(text, testData.response.body);
        do_test_finished();
        run_next_test();
      });
    })
    .catch(function (e) {
      do_report_unexpected_exception(e);
      do_test_finished();
      run_next_test();
    });
});

// test some error responses
add_test(function test_get40x() {
  do_test_pending();

  // prepare a response with some 40x error - a 404 will do
  let notFoundData = createTestData("/getNotFound");
  notFoundData.response.status = 404;
  notFoundData.response.statusText = "Not found";
  notFoundData.response.body = null;

  // No need to register a path handler - httpd will return 404 anyway.
  // Fetch, via GET, the resource that doesn't exist
  fetch(getBaseUrl() + notFoundData.testPath).then(function (response) {
    Assert.equal(response.status, 404);
    do_test_finished();
    run_next_test();
  });
});

add_test(function test_get50x() {
  do_test_pending();

  // prepare a response with some server error - a 500 will do
  let serverErrorData = createTestData("/serverError");
  serverErrorData.response.status = 500;
  serverErrorData.response.statusText = "The server broke";
  serverErrorData.response.body = null;

  server.registerPathHandler(
    serverErrorData.testPath,
    function (aRequest, aResponse) {
      try {
        // send the error response
        writeDataToResponse(serverErrorData.response, aResponse);
      } catch (e) {
        do_report_unexpected_exception(e);
      }
    }
  );

  // fetch, via GET, the resource that creates a server error
  fetch(getBaseUrl() + serverErrorData.testPath).then(function (response) {
    Assert.equal(response.status, 500);
    do_test_finished();
    run_next_test();
  });
});

// test a failure to connect
add_test(function test_getTestFailedConnect() {
  do_test_pending();
  // try a server that's not there
  fetch("http://localhost:4/should/fail")
    .then(response => {
      do_throw("Request should not succeed");
    })
    .catch(err => {
      Assert.equal(true, err instanceof TypeError);
      do_test_finished();
      run_next_test();
    });
});

add_test(function test_mozError() {
  do_test_pending();
  // try a server that's not there
  fetch("http://localhost:4/should/fail", { mozErrors: true })
    .then(response => {
      do_throw("Request should not succeed");
    })
    .catch(err => {
      Assert.equal(err.result, Cr.NS_ERROR_CONNECTION_REFUSED);
      do_test_finished();
      run_next_test();
    });
});

add_test(function test_request_mozError() {
  do_test_pending();
  // try a server that's not there
  const r = new Request("http://localhost:4/should/fail", { mozErrors: true });
  fetch(r)
    .then(response => {
      do_throw("Request should not succeed");
    })
    .catch(err => {
      Assert.equal(err.result, Cr.NS_ERROR_CONNECTION_REFUSED);
      do_test_finished();
      run_next_test();
    });
});

// test POSTing some JSON data
add_test(function test_PostJSONData() {
  do_test_pending();

  let testData = createTestData("/postJSONData");
  testData.request.body = '{"foo": "bar"}';

  server.registerPathHandler(testData.testPath, function (aRequest, aResponse) {
    try {
      let requestData = readDataFromRequest(aRequest);

      // Check the request body is OK
      Assert.equal(requestData.body, testData.request.body);

      // Check the content type is as expected
      Assert.equal(requestData.contentType, testData.request.contentType);

      writeDataToResponse(testData.response, aResponse);
    } catch (e) {
      Assert.ok(false);
    }
  });

  fetch(getBaseUrl() + testData.testPath, {
    method: "POST",
    body: testData.request.body,
    headers: {
      "Content-Type": "application/json",
    },
  })
    .then(function (aResponse) {
      Assert.ok(aResponse.ok);
      Assert.equal(aResponse.status, testData.response.status);
      Assert.equal(aResponse.statusText, testData.response.statusText);

      do_test_finished();
      run_next_test();
    })
    .catch(function (e) {
      do_report_unexpected_exception(e);
      do_test_finished();
      run_next_test();
    });
});

// test POSTing some text
add_test(function test_PostTextData() {
  do_test_pending();

  let testData = createTestData("/postTextData");
  testData.request.body = "A plain text body";
  testData.request.contentType = "text/plain";
  let responseHeaderName = "some-response-header";
  testData.response.headers[responseHeaderName] = "some header value";

  server.registerPathHandler(testData.testPath, function (aRequest, aResponse) {
    try {
      let requestData = readDataFromRequest(aRequest);

      // Check the request body is OK
      Assert.equal(requestData.body, testData.request.body);

      // Check the content type is as expected
      Assert.equal(requestData.contentType, testData.request.contentType);

      writeDataToResponse(testData.response, aResponse);
    } catch (e) {
      Assert.ok(false);
    }
  });

  fetch(getBaseUrl() + testData.testPath, {
    method: "POST",
    body: testData.request.body,
    headers: {
      "Content-Type": testData.request.contentType,
    },
  })
    .then(function (aResponse) {
      Assert.ok(aResponse.ok);
      Assert.equal(aResponse.status, testData.response.status);
      Assert.equal(aResponse.statusText, testData.response.statusText);

      // check the response header is set OK
      Assert.equal(
        aResponse.headers.get(responseHeaderName),
        testData.response.headers[responseHeaderName]
      );

      do_test_finished();
      run_next_test();
    })
    .catch(function (e) {
      do_report_unexpected_exception(e);
      do_test_finished();
      run_next_test();
    });
});

function run_test() {
  // Set up an HTTP Server
  server = new HttpServer();
  server.start(-1);

  run_next_test();

  registerCleanupFunction(function () {
    server.stop(function () {});
  });
}
