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

let { PostQueue } = Cu.import("resource://services-sync/record.js", {});

initTestLogging("Trace");

function makeRecord(nbytes) {
  // make a string 2-bytes less - the added quotes will make it correct.
  return {
    toJSON: () => "x".repeat(nbytes - 2),
  }
}

function makePostQueue(config, lastModTime, responseGenerator) {
  let stats = {
    posts: [],
  }
  let poster = (data, headers, batch, commit) => {
    let thisPost = { nbytes: data.length, batch, commit };
    if (headers.length) {
      thisPost.headers = headers;
    }
    stats.posts.push(thisPost);
    return Promise.resolve(responseGenerator.next().value);
  }

  let done = () => {}
  let pq = new PostQueue(poster, lastModTime, config, getTestLogger(), done);
  return { pq, stats };
}

add_task(async function test_simple() {
  let config = {
    max_post_bytes: 1000,
    max_post_records: 100,
    max_batch_bytes: Infinity,
    max_batch_records: Infinity,
  }

  const time = 11111111;

  function* responseGenerator() {
    yield { success: true, status: 200, headers: { "x-weave-timestamp": time + 100, "x-last-modified": time + 100 } };
  }

  let { pq, stats } = makePostQueue(config, time, responseGenerator());
  await pq.enqueue(makeRecord(10));
  await pq.flush(true);

  deepEqual(stats.posts, [{
    nbytes: 12, // expect our 10 byte record plus "[]" to wrap it.
    commit: true, // we don't know if we have batch semantics, so committed.
    headers: [["x-if-unmodified-since", time]],
    batch: "true"}]);
});

// Test we do the right thing when we need to make multiple posts when there
// are no batch semantics
add_task(async function test_max_post_bytes_no_batch() {
  let config = {
    max_post_bytes: 50,
    max_post_records: 4,
    max_batch_bytes: Infinity,
    max_batch_records: Infinity,
  }

  const time = 11111111;
  function* responseGenerator() {
    yield { success: true, status: 200, headers: { "x-weave-timestamp": time + 100, "x-last-modified": time + 100 } };
    yield { success: true, status: 200, headers: { "x-weave-timestamp": time + 200, "x-last-modified": time + 200 } };
  }

  let { pq, stats } = makePostQueue(config, time, responseGenerator());
  await pq.enqueue(makeRecord(20)); // total size now 22 bytes - "[" + record + "]"
  await pq.enqueue(makeRecord(20)); // total size now 43 bytes - "[" + record + "," + record + "]"
  await pq.enqueue(makeRecord(20)); // this will exceed our byte limit, so will be in the 2nd POST.
  await pq.flush(true);

  deepEqual(stats.posts, [
    {
      nbytes: 43, // 43 for the first post
      commit: false,
      headers: [["x-if-unmodified-since", time]],
      batch: "true",
    }, {
      nbytes: 22,
      commit: false, // we know we aren't in a batch, so never commit.
      headers: [["x-if-unmodified-since", time + 100]],
      batch: null,
    }
  ]);
  equal(pq.lastModified, time + 200);
});

// Similar to the above, but we've hit max_records instead of max_bytes.
add_task(async function test_max_post_records_no_batch() {
  let config = {
    max_post_bytes: 100,
    max_post_records: 2,
    max_batch_bytes: Infinity,
    max_batch_records: Infinity,
  }

  const time = 11111111;

  function* responseGenerator() {
    yield { success: true, status: 200, headers: { "x-weave-timestamp": time + 100, "x-last-modified": time + 100 } };
    yield { success: true, status: 200, headers: { "x-weave-timestamp": time + 200, "x-last-modified": time + 200 } };
  }

  let { pq, stats } = makePostQueue(config, time, responseGenerator());
  await pq.enqueue(makeRecord(20)); // total size now 22 bytes - "[" + record + "]"
  await pq.enqueue(makeRecord(20)); // total size now 43 bytes - "[" + record + "," + record + "]"
  await pq.enqueue(makeRecord(20)); // this will exceed our records limit, so will be in the 2nd POST.
  await pq.flush(true);

  deepEqual(stats.posts, [
    {
      nbytes: 43, // 43 for the first post
      commit: false,
      batch: "true",
      headers: [["x-if-unmodified-since", time]],
    }, {
      nbytes: 22,
      commit: false, // we know we aren't in a batch, so never commit.
      batch: null,
      headers: [["x-if-unmodified-since", time + 100]],
    }
  ]);
  equal(pq.lastModified, time + 200);
});

// Batch tests.

// Test making a single post when batch semantics are in place.
add_task(async function test_single_batch() {
  let config = {
    max_post_bytes: 1000,
    max_post_records: 100,
    max_batch_bytes: 2000,
    max_batch_records: 200,
  }
  const time = 11111111;
  function* responseGenerator() {
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time, "x-weave-timestamp": time + 100 },
    };
  }

  let { pq, stats } = makePostQueue(config, time, responseGenerator());
  ok((await pq.enqueue(makeRecord(10))).enqueued);
  await pq.flush(true);

  deepEqual(stats.posts, [
    {
      nbytes: 12, // expect our 10 byte record plus "[]" to wrap it.
      commit: true, // we don't know if we have batch semantics, so committed.
      batch: "true",
      headers: [["x-if-unmodified-since", time]],
    }
  ]);
});

// Test we do the right thing when we need to make multiple posts when there
// are batch semantics in place.
add_task(async function test_max_post_bytes_batch() {
  let config = {
    max_post_bytes: 50,
    max_post_records: 4,
    max_batch_bytes: 5000,
    max_batch_records: 100,
  }

  const time = 11111111;
  function* responseGenerator() {
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time, "x-weave-timestamp": time + 100 },
    };
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time + 200, "x-weave-timestamp": time + 200 },
   };
  }

  let { pq, stats } = makePostQueue(config, time, responseGenerator());
  ok((await pq.enqueue(makeRecord(20))).enqueued); // total size now 22 bytes - "[" + record + "]"
  ok((await pq.enqueue(makeRecord(20))).enqueued); // total size now 43 bytes - "[" + record + "," + record + "]"
  ok((await pq.enqueue(makeRecord(20))).enqueued); // this will exceed our byte limit, so will be in the 2nd POST.
  await pq.flush(true);

  deepEqual(stats.posts, [
    {
      nbytes: 43, // 43 for the first post
      commit: false,
      batch: "true",
      headers: [["x-if-unmodified-since", time]],
    }, {
      nbytes: 22,
      commit: true,
      batch: 1234,
      headers: [["x-if-unmodified-since", time]],
    }
  ]);

  equal(pq.lastModified, time + 200);
});

// Test we do the right thing when the batch bytes limit is exceeded.
add_task(async function test_max_post_bytes_batch() {
  let config = {
    max_post_bytes: 50,
    max_post_records: 20,
    max_batch_bytes: 70,
    max_batch_records: 100,
  }

  const time0 = 11111111;
  const time1 = 22222222;
  function* responseGenerator() {
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time0, "x-weave-timestamp": time0 + 100 },
    };
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time1, "x-weave-timestamp": time1 },
    };
    yield { success: true, status: 202, obj: { batch: 5678 },
            headers: { "x-last-modified": time1, "x-weave-timestamp": time1 + 100 },
    };
    yield { success: true, status: 202, obj: { batch: 5678 },
            headers: { "x-last-modified": time1 + 200, "x-weave-timestamp": time1 + 200 },
    };
  }

  let { pq, stats } = makePostQueue(config, time0, responseGenerator());
  ok((await pq.enqueue(makeRecord(20))).enqueued); // total size now 22 bytes - "[" + record + "]"
  ok((await pq.enqueue(makeRecord(20))).enqueued); // total size now 43 bytes - "[" + record + "," + record + "]"
  // this will exceed our POST byte limit, so will be in the 2nd POST - but still in the first batch.
  ok((await pq.enqueue(makeRecord(20))).enqueued); // 22 bytes for 2nd post, 55 bytes in the batch.
  // this will exceed our batch byte limit, so will be in a new batch.
  ok((await pq.enqueue(makeRecord(20))).enqueued); // 22 bytes in 3rd post/2nd batch
  ok((await pq.enqueue(makeRecord(20))).enqueued); // 43 bytes in 3rd post/2nd batch
  // This will exceed POST byte limit, so will be in the 4th post, part of the 2nd batch.
  ok((await pq.enqueue(makeRecord(20))).enqueued); // 22 bytes for 4th post/2nd batch
  await pq.flush(true);

  deepEqual(stats.posts, [
    {
      nbytes: 43, // 43 for the first post
      commit: false,
      batch: "true",
      headers: [["x-if-unmodified-since", time0]],
    }, {
      // second post of 22 bytes in the first batch, committing it.
      nbytes: 22,
      commit: true,
      batch: 1234,
      headers: [["x-if-unmodified-since", time0]],
    }, {
      // 3rd post of 43 bytes in a new batch, not yet committing it.
      nbytes: 43,
      commit: false,
      batch: "true",
      headers: [["x-if-unmodified-since", time1]],
    }, {
      // 4th post of 22 bytes in second batch, committing it.
      nbytes: 22,
      commit: true,
      batch: 5678,
      headers: [["x-if-unmodified-since", time1]],
    },
  ]);

  equal(pq.lastModified, time1 + 200);
});

// Test we split up the posts when we exceed the record limit when batch semantics
// are in place.
add_task(async function test_max_post_bytes_batch() {
  let config = {
    max_post_bytes: 1000,
    max_post_records: 2,
    max_batch_bytes: 5000,
    max_batch_records: 100,
  }

  const time = 11111111;
  function* responseGenerator() {
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time, "x-weave-timestamp": time + 100 },
    };
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time + 200, "x-weave-timestamp": time + 200 },
   };
  }

  let { pq, stats } = makePostQueue(config, time, responseGenerator());
  ok((await pq.enqueue(makeRecord(20))).enqueued); // total size now 22 bytes - "[" + record + "]"
  ok((await pq.enqueue(makeRecord(20))).enqueued); // total size now 43 bytes - "[" + record + "," + record + "]"
  ok((await pq.enqueue(makeRecord(20))).enqueued); // will exceed record limit, so will be in 2nd post.
  await pq.flush(true);

  deepEqual(stats.posts, [
    {
      nbytes: 43, // 43 for the first post
      commit: false,
      batch: "true",
      headers: [["x-if-unmodified-since", time]],
    }, {
      nbytes: 22,
      commit: true,
      batch: 1234,
      headers: [["x-if-unmodified-since", time]],
    }
  ]);

  equal(pq.lastModified, time + 200);
});

// Test that a single huge record fails to enqueue
add_task(async function test_huge_record() {
  let config = {
    max_post_bytes: 50,
    max_post_records: 100,
    max_batch_bytes: 5000,
    max_batch_records: 100,
  }

  const time = 11111111;
  function* responseGenerator() {
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time, "x-weave-timestamp": time + 100 },
    };
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time + 200, "x-weave-timestamp": time + 200 },
   };
  }

  let { pq, stats } = makePostQueue(config, time, responseGenerator());
  ok((await pq.enqueue(makeRecord(20))).enqueued);

  let { enqueued, error } = await pq.enqueue(makeRecord(1000));
  ok(!enqueued);
  notEqual(error, undefined);

  // make sure that we keep working, skipping the bad record entirely
  // (handling the error the queue reported is left up to caller)
  ok((await pq.enqueue(makeRecord(20))).enqueued);
  ok((await pq.enqueue(makeRecord(20))).enqueued);

  await pq.flush(true);

  deepEqual(stats.posts, [
    {
      nbytes: 43, // 43 for the first post
      commit: false,
      batch: "true",
      headers: [["x-if-unmodified-since", time]],
    }, {
      nbytes: 22,
      commit: true,
      batch: 1234,
      headers: [["x-if-unmodified-since", time]],
    }
  ]);

  equal(pq.lastModified, time + 200);
});

// Test we do the right thing when the batch record limit is exceeded.
add_task(async function test_max_records_batch() {
  let config = {
    max_post_bytes: 1000,
    max_post_records: 3,
    max_batch_bytes: 10000,
    max_batch_records: 5,
  }

  const time0 = 11111111;
  const time1 = 22222222;
  function* responseGenerator() {
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time0, "x-weave-timestamp": time0 + 100 },
    };
    yield { success: true, status: 202, obj: { batch: 1234 },
            headers: { "x-last-modified": time1, "x-weave-timestamp": time1 },
    };
    yield { success: true, status: 202, obj: { batch: 5678 },
            headers: { "x-last-modified": time1, "x-weave-timestamp": time1 + 100 },
    };
    yield { success: true, status: 202, obj: { batch: 5678 },
            headers: { "x-last-modified": time1 + 200, "x-weave-timestamp": time1 + 200 },
    };
  }

  let { pq, stats } = makePostQueue(config, time0, responseGenerator());

  ok((await pq.enqueue(makeRecord(20))).enqueued);
  ok((await pq.enqueue(makeRecord(20))).enqueued);
  ok((await pq.enqueue(makeRecord(20))).enqueued);

  ok((await pq.enqueue(makeRecord(20))).enqueued);
  ok((await pq.enqueue(makeRecord(20))).enqueued);

  ok((await pq.enqueue(makeRecord(20))).enqueued);
  ok((await pq.enqueue(makeRecord(20))).enqueued);
  ok((await pq.enqueue(makeRecord(20))).enqueued);

  ok((await pq.enqueue(makeRecord(20))).enqueued);

  await pq.flush(true);

  deepEqual(stats.posts, [
    { // 3 records
      nbytes: 64,
      commit: false,
      batch: "true",
      headers: [["x-if-unmodified-since", time0]],
    }, { // 2 records -- end batch1
      nbytes: 43,
      commit: true,
      batch: 1234,
      headers: [["x-if-unmodified-since", time0]],
    }, { // 3 records
      nbytes: 64,
      commit: false,
      batch: "true",
      headers: [["x-if-unmodified-since", time1]],
    }, { // 1 record -- end batch2
      nbytes: 22,
      commit: true,
      batch: 5678,
      headers: [["x-if-unmodified-since", time1]],
    },
  ]);

  equal(pq.lastModified, time1 + 200);
});
