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

const { Kinto } = ChromeUtils.importESModule(
  "resource://services-common/kinto-offline-client.sys.mjs"
);
const { FirefoxAdapter } = ChromeUtils.importESModule(
  "resource://services-common/kinto-storage-adapter.sys.mjs"
);

var server;

// set up what we need to make storage adapters
const kintoFilename = "kinto.sqlite";

function do_get_kinto_sqliteHandle() {
  return FirefoxAdapter.openConnection({ path: kintoFilename });
}

function do_get_kinto_collection(sqliteHandle, collection = "test_collection") {
  let config = {
    remote: `http://localhost:${server.identity.primaryPort}/v1/`,
    headers: { Authorization: "Basic " + btoa("user:pass") },
    adapter: FirefoxAdapter,
    adapterOptions: { sqliteHandle },
  };
  return new Kinto(config).collection(collection);
}

async function clear_collection() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);
    await collection.clear();
  } finally {
    await sqliteHandle.close();
  }
}

// test some operations on a local collection
add_task(async function test_kinto_add_get() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);

    let newRecord = { foo: "bar" };
    // check a record is created
    let createResult = await collection.create(newRecord);
    Assert.equal(createResult.data.foo, newRecord.foo);
    // check getting the record gets the same info
    let getResult = await collection.get(createResult.data.id);
    deepEqual(createResult.data, getResult.data);
    // check what happens if we create the same item again (it should throw
    // since you can't create with id)
    try {
      await collection.create(createResult.data);
      do_throw("Creation of a record with an id should fail");
    } catch (err) {}
    // try a few creates without waiting for the first few to resolve
    let promises = [];
    promises.push(collection.create(newRecord));
    promises.push(collection.create(newRecord));
    promises.push(collection.create(newRecord));
    await collection.create(newRecord);
    await Promise.all(promises);
  } finally {
    await sqliteHandle.close();
  }
});

add_task(clear_collection);

// test some operations on multiple connections
add_task(async function test_kinto_add_get() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection1 = do_get_kinto_collection(sqliteHandle);
    const collection2 = do_get_kinto_collection(
      sqliteHandle,
      "test_collection_2"
    );

    let newRecord = { foo: "bar" };

    // perform several write operations alternately without waiting for promises
    // to resolve
    let promises = [];
    for (let i = 0; i < 10; i++) {
      promises.push(collection1.create(newRecord));
      promises.push(collection2.create(newRecord));
    }

    // ensure subsequent operations still work
    await Promise.all([
      collection1.create(newRecord),
      collection2.create(newRecord),
    ]);
    await Promise.all(promises);
  } finally {
    await sqliteHandle.close();
  }
});

add_task(clear_collection);

add_task(async function test_kinto_update() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);
    const newRecord = { foo: "bar" };
    // check a record is created
    let createResult = await collection.create(newRecord);
    Assert.equal(createResult.data.foo, newRecord.foo);
    Assert.equal(createResult.data._status, "created");
    // check we can update this OK
    let copiedRecord = Object.assign(createResult.data, {});
    deepEqual(createResult.data, copiedRecord);
    copiedRecord.foo = "wibble";
    let updateResult = await collection.update(copiedRecord);
    // check the field was updated
    Assert.equal(updateResult.data.foo, copiedRecord.foo);
    // check the status is still "created", since we haven't synced
    // the record
    Assert.equal(updateResult.data._status, "created");
  } finally {
    await sqliteHandle.close();
  }
});

add_task(clear_collection);

add_task(async function test_kinto_clear() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);

    // create an expected number of records
    const expected = 10;
    const newRecord = { foo: "bar" };
    for (let i = 0; i < expected; i++) {
      await collection.create(newRecord);
    }
    // check the collection contains the correct number
    let list = await collection.list();
    Assert.equal(list.data.length, expected);
    // clear the collection and check again - should be 0
    await collection.clear();
    list = await collection.list();
    Assert.equal(list.data.length, 0);
  } finally {
    await sqliteHandle.close();
  }
});

add_task(clear_collection);

add_task(async function test_kinto_delete() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);
    const newRecord = { foo: "bar" };
    // check a record is created
    let createResult = await collection.create(newRecord);
    Assert.equal(createResult.data.foo, newRecord.foo);
    // check getting the record gets the same info
    let getResult = await collection.get(createResult.data.id);
    deepEqual(createResult.data, getResult.data);
    // delete that record
    let deleteResult = await collection.delete(createResult.data.id);
    // check the ID is set on the result
    Assert.equal(getResult.data.id, deleteResult.data.id);
    // and check that get no longer returns the record
    try {
      getResult = await collection.get(createResult.data.id);
      do_throw("there should not be a result");
    } catch (e) {}
  } finally {
    await sqliteHandle.close();
  }
});

add_task(async function test_kinto_list() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);
    const expected = 10;
    const created = [];
    for (let i = 0; i < expected; i++) {
      let newRecord = { foo: "test " + i };
      let createResult = await collection.create(newRecord);
      created.push(createResult.data);
    }
    // check the collection contains the correct number
    let list = await collection.list();
    Assert.equal(list.data.length, expected);

    // check that all created records exist in the retrieved list
    for (let createdRecord of created) {
      let found = false;
      for (let retrievedRecord of list.data) {
        if (createdRecord.id == retrievedRecord.id) {
          deepEqual(createdRecord, retrievedRecord);
          found = true;
        }
      }
      Assert.ok(found);
    }
  } finally {
    await sqliteHandle.close();
  }
});

add_task(clear_collection);

add_task(async function test_importBulk_ignores_already_imported_records() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);
    const record = {
      id: "41b71c13-17e9-4ee3-9268-6a41abf9730f",
      title: "foo",
      last_modified: 1457896541,
    };
    await collection.importBulk([record]);
    let impactedRecords = await collection.importBulk([record]);
    Assert.equal(impactedRecords.length, 0);
  } finally {
    await sqliteHandle.close();
  }
});

add_task(clear_collection);

add_task(async function test_loadDump_should_overwrite_old_records() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);
    const record = {
      id: "41b71c13-17e9-4ee3-9268-6a41abf9730f",
      title: "foo",
      last_modified: 1457896541,
    };
    await collection.loadDump([record]);
    const updated = Object.assign({}, record, { last_modified: 1457896543 });
    let impactedRecords = await collection.loadDump([updated]);
    Assert.equal(impactedRecords.length, 1);
  } finally {
    await sqliteHandle.close();
  }
});

add_task(clear_collection);

add_task(async function test_loadDump_should_not_overwrite_unsynced_records() {
  let sqliteHandle;
  try {
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);
    const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
    await collection.create(
      { id: recordId, title: "foo" },
      { useRecordId: true }
    );
    const record = { id: recordId, title: "bar", last_modified: 1457896541 };
    let impactedRecords = await collection.loadDump([record]);
    Assert.equal(impactedRecords.length, 0);
  } finally {
    await sqliteHandle.close();
  }
});

add_task(clear_collection);

add_task(
  async function test_loadDump_should_not_overwrite_records_without_last_modified() {
    let sqliteHandle;
    try {
      sqliteHandle = await do_get_kinto_sqliteHandle();
      const collection = do_get_kinto_collection(sqliteHandle);
      const recordId = "41b71c13-17e9-4ee3-9268-6a41abf9730f";
      await collection.create({ id: recordId, title: "foo" }, { synced: true });
      const record = { id: recordId, title: "bar", last_modified: 1457896541 };
      let impactedRecords = await collection.loadDump([record]);
      Assert.equal(impactedRecords.length, 0);
    } finally {
      await sqliteHandle.close();
    }
  }
);

add_task(clear_collection);

// Now do some sanity checks against a server - we're not looking to test
// core kinto.js functionality here (there is excellent test coverage in
// kinto.js), more making sure things are basically working as expected.
add_task(async function test_kinto_sync() {
  const configPath = "/v1/";
  const metadataPath = "/v1/buckets/default/collections/test_collection";
  const recordsPath = "/v1/buckets/default/collections/test_collection/records";
  // register a handler
  function handleResponse(request, response) {
    try {
      const sampled = getSampleResponse(request, server.identity.primaryPort);
      if (!sampled) {
        do_throw(
          `unexpected ${request.method} request for ${request.path}?${request.queryString}`
        );
      }

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

      response.write(sampled.responseBody);
    } catch (e) {
      dump(`${e}\n`);
    }
  }
  server.registerPathHandler(configPath, handleResponse);
  server.registerPathHandler(metadataPath, handleResponse);
  server.registerPathHandler(recordsPath, handleResponse);

  // create an empty collection, sync to populate
  let sqliteHandle;
  try {
    let result;
    sqliteHandle = await do_get_kinto_sqliteHandle();
    const collection = do_get_kinto_collection(sqliteHandle);

    result = await collection.sync();
    Assert.ok(result.ok);

    // our test data has a single record; it should be in the local collection
    let list = await collection.list();
    Assert.equal(list.data.length, 1);

    // now sync again; we should now have 2 records
    result = await collection.sync();
    Assert.ok(result.ok);
    list = await collection.list();
    Assert.equal(list.data.length, 2);

    // sync again; the second records should have been modified
    const before = list.data[0].title;
    result = await collection.sync();
    Assert.ok(result.ok);
    list = await collection.list();
    const after = list.data[1].title;
    Assert.notEqual(before, after);

    const manualID = list.data[0].id;
    Assert.equal(list.data.length, 3);
    Assert.equal(manualID, "some-manually-chosen-id");
  } finally {
    await sqliteHandle.close();
  }
});

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

  run_next_test();

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

// get a response for a given request from sample data
function getSampleResponse(req, port) {
  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/default/collections/test_collection": {
      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: "1234"',
      ],
      status: { status: 200, statusText: "OK" },
      responseBody: JSON.stringify({
        data: {
          id: "test_collection",
          last_modified: 1234,
        },
      }),
    },
    "GET:/v1/buckets/default/collections/test_collection/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: "1445606341071"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: JSON.stringify({
          data: [
            {
              last_modified: 1445606341071,
              done: false,
              id: "68db8313-686e-4fff-835e-07d78ad6f2af",
              title: "New test",
            },
          ],
        }),
      },
    "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445606341071":
      {
        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: "1445607941223"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: JSON.stringify({
          data: [
            {
              last_modified: 1445607941223,
              done: false,
              id: "901967b0-f729-4b30-8d8d-499cba7f4b1d",
              title: "Another new test",
            },
          ],
        }),
      },
    "GET:/v1/buckets/default/collections/test_collection/records?_sort=-last_modified&_since=1445607941223":
      {
        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: "1445607541267"',
        ],
        status: { status: 200, statusText: "OK" },
        responseBody: JSON.stringify({
          data: [
            {
              last_modified: 1445607541265,
              done: false,
              id: "901967b0-f729-4b30-8d8d-499cba7f4b1d",
              title: "Modified title",
            },
            {
              last_modified: 1445607541267,
              done: true,
              id: "some-manually-chosen-id",
              title: "New record with custom ID",
            },
          ],
        }),
      },
  };
  return (
    responses[`${req.method}:${req.path}?${req.queryString}`] ||
    responses[`${req.method}:${req.path}`] ||
    responses[req.method]
  );
}
