"use strict"

const { Constructor: CC } = Components;

Cu.import("resource://testing-common/httpd.js");

const { Kinto } = Cu.import("resource://services-common/kinto-offline-client.js", {});
const { FirefoxAdapter } = Cu.import("resource://services-common/kinto-storage-adapter.js", {});

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

const PREF_BLOCKLIST_PINNING_COLLECTION = "services.blocklist.pinning.collection";
const COLLECTION_NAME = "pins";
const KINTO_STORAGE_PATH    = "kinto.sqlite";

// First, we need to setup appInfo or we can't do version checks
var id = "xpcshell@tests.mozilla.org";
var appName = "XPCShell";
var version = "1";
var platformVersion = "1.9.2";
Cu.import("resource://testing-common/AppInfo.jsm", this);

updateAppInfo({
  name: appName,
  ID: id,
  version,
  platformVersion: platformVersion ? platformVersion : "1.0",
  crashReporter: true,
});

let server;


function do_get_kinto_collection(connection, collectionName) {
  let config = {
    // Set the remote to be some server that will cause test failure when
    // hit since we should never hit the server directly (any non-local
    // request causes failure in tests), only via maybeSync()
    remote: "https://firefox.settings.services.mozilla.com/v1/",
    // Set up the adapter and bucket as normal
    adapter: FirefoxAdapter,
    adapterOptions: {sqliteHandle: connection},
    bucket: "pinning"
  };
  let kintoClient = new Kinto(config);
  return kintoClient.collection(collectionName);
}

// Some simple tests to demonstrate that the core preload sync operations work
// correctly and that simple kinto operations are working as expected.
add_task(function* test_something() {
  // set the collection name explicitly - since there will be version
  // specific collection names in prefs
  Services.prefs.setCharPref(PREF_BLOCKLIST_PINNING_COLLECTION,
                             COLLECTION_NAME);

  const { PinningPreloadClient } = Cu.import("resource://services-common/blocklist-clients.js", {});

  const configPath = "/v1/";
  const recordsPath = "/v1/buckets/pinning/collections/pins/records";

  Services.prefs.setCharPref("services.settings.server",
                             `http://localhost:${server.identity.primaryPort}/v1`);

  // register a handler
  function handleResponse(request, response) {
    try {
      const sample = getSampleResponse(request, server.identity.primaryPort);
      if (!sample) {
        do_throw(`unexpected ${request.method} request for ${request.path}?${request.queryString}`);
      }

      response.setStatusLine(null, sample.status.status,
                             sample.status.statusText);
      // send the headers
      for (let headerLine of sample.sampleHeaders) {
        let headerElements = headerLine.split(":");
        response.setHeader(headerElements[0], headerElements[1].trimLeft());
      }
      response.setHeader("Date", (new Date()).toUTCString());

      response.write(sample.responseBody);
    } catch (e) {
      do_print(e);
    }
  }
  server.registerPathHandler(configPath, handleResponse);
  server.registerPathHandler(recordsPath, handleResponse);

  let sss = Cc["@mozilla.org/ssservice;1"]
              .getService(Ci.nsISiteSecurityService);

  // ensure our pins are all missing before we start
  ok(!sss.isSecureURI(sss.HEADER_HPKP,
                      Services.io.newURI("https://one.example.com"), 0));
  ok(!sss.isSecureURI(sss.HEADER_HPKP,
                      Services.io.newURI("https://two.example.com"), 0));
  ok(!sss.isSecureURI(sss.HEADER_HPKP,
                      Services.io.newURI("https://three.example.com"), 0));
  ok(!sss.isSecureURI(sss.HEADER_HSTS,
                      Services.io.newURI("https://four.example.com"), 0));
  ok(!sss.isSecureURI(sss.HEADER_HSTS,
                      Services.io.newURI("https://five.example.com"), 0));

  // Test an empty db populates
  yield PinningPreloadClient.maybeSync(2000, Date.now());

  let connection = yield FirefoxAdapter.openConnection({path: KINTO_STORAGE_PATH});

  // Open the collection, verify it's been populated:
  // Our test data has a single record; it should be in the local collection
  let collection = do_get_kinto_collection(connection, COLLECTION_NAME);
  let list = yield collection.list();
  do_check_eq(list.data.length, 1);

  // check that a pin exists for one.example.com
  ok(sss.isSecureURI(sss.HEADER_HPKP,
                     Services.io.newURI("https://one.example.com"), 0));

  // Test the db is updated when we call again with a later lastModified value
  yield PinningPreloadClient.maybeSync(4000, Date.now());

  // Open the collection, verify it's been updated:
  // Our data now has four new records; all should be in the local collection
  collection = do_get_kinto_collection(connection, COLLECTION_NAME);
  list = yield collection.list();
  do_check_eq(list.data.length, 5);
  yield connection.close();

  // check that a pin exists for two.example.com and three.example.com
  ok(sss.isSecureURI(sss.HEADER_HPKP,
                     Services.io.newURI("https://two.example.com"), 0));
  ok(sss.isSecureURI(sss.HEADER_HPKP,
                     Services.io.newURI("https://three.example.com"), 0));

  // check that a pin does not exist for four.example.com - it's in the
  // collection but the version should not match
  ok(!sss.isSecureURI(sss.HEADER_HPKP,
                      Services.io.newURI("https://four.example.com"), 0));

  // Try to maybeSync with the current lastModified value - no connection
  // should be attempted.
  // Clear the kinto base pref so any connections will cause a test failure
  Services.prefs.clearUserPref("services.settings.server");
  yield PinningPreloadClient.maybeSync(4000, Date.now());

  // Try again with a lastModified value at some point in the past
  yield PinningPreloadClient.maybeSync(3000, Date.now());

  // Check the pinning check time pref is modified, even if the collection
  // hasn't changed
  Services.prefs.setIntPref("services.blocklist.onecrl.checked", 0);
  yield PinningPreloadClient.maybeSync(3000, Date.now());
  let newValue = Services.prefs.getIntPref("services.blocklist.pinning.checked");
  do_check_neq(newValue, 0);

  // Check that the HSTS preload added to the collection works...
  ok(sss.isSecureURI(sss.HEADER_HSTS,
                     Services.io.newURI("https://five.example.com"), 0));
  // ...and that includeSubdomains is honored
  ok(!sss.isSecureURI(sss.HEADER_HSTS,
                      Services.io.newURI("https://subdomain.five.example.com"),
                      0));

  // Check that a sync completes even when there's bad data in the
  // collection. This will throw on fail, so just calling maybeSync is an
  // acceptible test (the data below with last_modified of 300 is nonsense).
  Services.prefs.setCharPref("services.settings.server",
                             `http://localhost:${server.identity.primaryPort}/v1`);
  yield PinningPreloadClient.maybeSync(5000, Date.now());

  // The STS entry for five.example.com now has includeSubdomains set;
  // ensure that the new includeSubdomains value is honored.
  ok(sss.isSecureURI(sss.HEADER_HSTS,
                     Services.io.newURI("https://subdomain.five.example.com"),
                     0));
});

function run_test() {
  // Ensure that signature verification is disabled to prevent interference
  // with basic certificate sync tests
  Services.prefs.setBoolPref("services.blocklist.signing.enforced", false);

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

  run_next_test();

  do_register_cleanup(function() {
    server.stop(() => { });
  });
}

// get a response for a given request from sample data
function getSampleResponse(req, port) {
  const appInfo = Cc["@mozilla.org/xre/app-info;1"]
                     .getService(Ci.nsIXULAppInfo);

  const responses = {
    "OPTIONS": {
      "sampleHeaders": [
        "Access-Control-Allow-Headers: Content-Length,Expires,Backoff,Retry-After,Last-Modified,Total-Records,ETag,Pragma,Cache-Control,authorization,content-type,if-none-match,Alert,Next-Page",
        "Access-Control-Allow-Methods: GET,HEAD,OPTIONS,POST,DELETE,OPTIONS",
        "Access-Control-Allow-Origin: *",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress"
      ],
      "status": {status: 200, statusText: "OK"},
      "responseBody": "null"
    },
    "GET:/v1/?": {
      "sampleHeaders": [
        "Access-Control-Allow-Origin: *",
        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress"
      ],
      "status": {status: 200, statusText: "OK"},
      "responseBody": JSON.stringify({
        "settings": {
          "batch_max_requests": 25
        },
        "url": `http://localhost:${port}/v1/`,
        "documentation": "https://kinto.readthedocs.org/",
        "version": "1.5.1",
        "commit": "cbc6f58",
        "hello": "kinto"
      })
    },
    "GET:/v1/buckets/pinning/collections/pins/records?_sort=-last_modified": {
      "sampleHeaders": [
        "Access-Control-Allow-Origin: *",
        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress",
        "Etag: \"3000\""
      ],
      "status": {status: 200, statusText: "OK"},
      "responseBody": JSON.stringify({"data": [{
        "pinType": "KeyPin",
        "hostName": "one.example.com",
        "includeSubdomains": false,
        "expires": new Date().getTime() + 1000000,
        "pins": ["cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
                  "M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE="],
        "versions": [appInfo.version],
        "id": "78cf8900-fdea-4ce5-f8fb-b78710617718",
        "last_modified": 3000
      }]})
    },
    "GET:/v1/buckets/pinning/collections/pins/records?_sort=-last_modified&_since=3000": {
      "sampleHeaders": [
        "Access-Control-Allow-Origin: *",
        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress",
        "Etag: \"4000\""
      ],
      "status": {status: 200, statusText: "OK"},
      "responseBody": JSON.stringify({"data": [{
        "pinType": "KeyPin",
        "hostName": "two.example.com",
        "includeSubdomains": false,
        "expires": new Date().getTime() + 1000000,
        "pins": ["cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
                  "M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE="],
        "versions": [appInfo.version],
        "id": "dabafde9-df4a-ddba-2548-748da04cc02c",
        "last_modified": 4000
      }, {
        "pinType": "KeyPin",
        "hostName": "three.example.com",
        "includeSubdomains": false,
        "expires": new Date().getTime() + 1000000,
        "pins": ["cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
                  "M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE="],
        "versions": [appInfo.version, "some other version that won't match"],
        "id": "dabafde9-df4a-ddba-2548-748da04cc02d",
        "last_modified": 4000
      }, {
        "pinType": "KeyPin",
        "hostName": "four.example.com",
        "includeSubdomains": false,
        "expires": new Date().getTime() + 1000000,
        "pins": ["cUPcTAZWKaASuYWhhneDttWpY3oBAkE3h2+soZS7sWs=",
                  "M8HztCzM3elUxkcjR2S5P4hhyBNf6lHkmjAHKhpGPWE="],
        "versions": ["some version that won't match"],
        "id": "dabafde9-df4a-ddba-2548-748da04cc02e",
        "last_modified": 4000
      }, {
        "pinType": "STSPin",
        "hostName": "five.example.com",
        "includeSubdomains": false,
        "expires": new Date().getTime() + 1000000,
        "versions": [appInfo.version, "some version that won't match"],
        "id": "dabafde9-df4a-ddba-2548-748da04cc032",
        "last_modified": 4000
      }]})
    },
    "GET:/v1/buckets/pinning/collections/pins/records?_sort=-last_modified&_since=4000": {
      "sampleHeaders": [
        "Access-Control-Allow-Origin: *",
        "Access-Control-Expose-Headers: Retry-After, Content-Length, Alert, Backoff",
        "Content-Type: application/json; charset=UTF-8",
        "Server: waitress",
        "Etag: \"5000\""
      ],
      "status": {status: 200, statusText: "OK"},
      "responseBody": JSON.stringify({"data": [{
        "irrelevant": "this entry looks nothing whatsoever like a pin preload",
        "pinType": "KeyPin",
        "id": "dabafde9-df4a-ddba-2548-748da04cc02f",
        "last_modified": 5000
      }, {
        "irrelevant": "this entry has data of the wrong type",
        "pinType": "KeyPin",
        "hostName": 3,
        "includeSubdomains": "nonsense",
        "expires": "more nonsense",
        "pins": [1, 2, 3, 4],
        "id": "dabafde9-df4a-ddba-2548-748da04cc030",
        "last_modified": 5000
      }, {
        "irrelevant": "this entry is missing the actual pins",
        "pinType": "KeyPin",
        "hostName": "missingpins.example.com",
        "includeSubdomains": false,
        "expires": new Date().getTime() + 1000000,
        "versions": [appInfo.version],
        "id": "dabafde9-df4a-ddba-2548-748da04cc031",
        "last_modified": 5000
      }, {
        "pinType": "STSPin",
        "hostName": "five.example.com",
        "includeSubdomains": true,
        "expires": new Date().getTime() + 1000000,
        "versions": [appInfo.version, "some version that won't match"],
        "id": "dabafde9-df4a-ddba-2548-748da04cc032",
        "last_modified": 5000
      }]})
    }
  };
  return responses[`${req.method}:${req.path}?${req.queryString}`] ||
         responses[req.method];

}
