// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

couchTests.replicator_db = function(debug) {

  if (debug) debugger;

  var wait_rep_doc = 500; // number of millisecs to wait after saving a Rep Doc
  var dbA = new CouchDB("test_suite_rep_db_a", {"X-Couch-Full-Commit":"false"});
  var dbB = new CouchDB("test_suite_rep_db_b", {"X-Couch-Full-Commit":"false"});
  var repDb = new CouchDB("test_suite_rep_db", {"X-Couch-Full-Commit":"false"});
  var usersDb = new CouchDB("test_suite_auth", {"X-Couch-Full-Commit":"false"});

  var docs1 = [
    {
      _id: "foo1",
      value: 11
    },
    {
      _id: "foo2",
      value: 22
    },
    {
      _id: "foo3",
      value: 33
    }
  ];

  function waitForRep(repDb, repDoc, state) {
    var newRep,
        t0 = new Date(),
        t1,
        ms = 3000;

    do {
      newRep = repDb.open(repDoc._id);
      t1 = new Date();
    } while (((t1 - t0) <= ms) && newRep._replication_state !== state);
  }

  function waitForSeq(sourceDb, targetDb) {
    var targetSeq,
        sourceSeq = sourceDb.info().update_seq,
        t0 = new Date(),
        t1,
        ms = 3000;

    do {
      targetSeq = targetDb.info().update_seq;
      t1 = new Date();
    } while (((t1 - t0) <= ms) && targetSeq < sourceSeq);
  }

  function waitForDocPos(db, docId, pos) {
    var doc, curPos, t0, t1,
        maxWait = 3000;

    doc = db.open(docId);
    curPos = Number(doc._rev.split("-", 1));
    t0 = t1 = new Date();

    while ((curPos < pos) && ((t1 - t0) <= maxWait)) {
       doc = db.open(docId);
       curPos = Number(doc._rev.split("-", 1));
       t1 = new Date();
    }

    return doc;
  }

  function wait(ms) {
    var t0 = new Date(), t1;
    do {
      CouchDB.request("GET", "/");
      t1 = new Date();
    } while ((t1 - t0) <= ms);
  }


  function populate_db(db, docs) {
    if (db.name !== usersDb.name) {
      db.deleteDb();
      db.createDb();
    }
    for (var i = 0; i < docs.length; i++) {
      var d = docs[i];
      delete d._rev;
      T(db.save(d).ok);
    }
  }

  function simple_replication() {
    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var repDoc = {
      _id: "foo_simple_rep",
      source: dbA.name,
      target: dbB.name
    };
    T(repDb.save(repDoc).ok);

    waitForRep(repDb, repDoc, "completed");
    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);
      T(copy !== null);
      T(copy.value === doc.value);
    }

    var repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    T(repDoc1.source === repDoc.source);
    T(repDoc1.target === repDoc.target);
    T(repDoc1._replication_state === "completed", "simple");
    T(typeof repDoc1._replication_state_time === "string");
    T(typeof repDoc1._replication_id  === "string");
    T(typeof repDoc1._replication_stats === "object", "doc has stats");
    var stats = repDoc1._replication_stats;
    TEquals(docs1.length, stats.revisions_checked,
       "right # of revisions_checked");
    TEquals(docs1.length, stats.missing_revisions_found,
      "right # of missing_revisions_found");
    TEquals(docs1.length, stats.docs_read, "right # of docs_read");
    TEquals(docs1.length, stats.docs_written, "right # of docs_written");
    TEquals(0, stats.doc_write_failures, "right # of doc_write_failures");
    TEquals(dbA.info().update_seq, stats.checkpointed_source_seq,
      "right checkpointed_source_seq");
  }


  function filtered_replication() {
    var docs2 = docs1.concat([
      {
        _id: "_design/mydesign",
        language : "javascript",
        filters : {
          myfilter : (function(doc, req) {
            return (doc.value % 2) !== Number(req.query.myparam);
          }).toString()
        }
      }
    ]);

    populate_db(dbA, docs2);
    populate_db(dbB, []);

    var repDoc = {
      _id: "foo_filt_rep_doc",
      source: "http://" + CouchDB.host + "/" + dbA.name,
      target: dbB.name,
      filter: "mydesign/myfilter",
      query_params: {
        myparam: 1
      }
    };
    T(repDb.save(repDoc).ok);

    waitForRep(repDb, repDoc, "completed");
    for (var i = 0; i < docs2.length; i++) {
      var doc = docs2[i];
      var copy = dbB.open(doc._id);

      if (typeof doc.value === "number") {
        if ((doc.value % 2) !== 1) {
          T(copy !== null);
          T(copy.value === doc.value);
        } else {
          T(copy === null);
        }
      }
    }

    var repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    T(repDoc1.source === repDoc.source);
    T(repDoc1.target === repDoc.target);
    T(repDoc1._replication_state === "completed", "filtered");
    T(typeof repDoc1._replication_state_time === "string");
    T(typeof repDoc1._replication_id  === "string");
    T(typeof repDoc1._replication_stats === "object", "doc has stats");
    var stats = repDoc1._replication_stats;
    TEquals(2, stats.revisions_checked, "right # of revisions_checked");
    TEquals(2, stats.missing_revisions_found, "right # of missing_revisions_found");
    TEquals(2, stats.docs_read, "right # of docs_read");
    TEquals(1, stats.docs_written, "right # of docs_written");
    TEquals(1, stats.doc_write_failures, "right # of doc_write_failures");
    TEquals(dbA.info().update_seq, stats.checkpointed_source_seq,
      "right checkpointed_source_seq");
  }


  function continuous_replication() {
    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var repDoc = {
      _id: "foo_cont_rep_doc",
      source: "http://" + CouchDB.host + "/" + dbA.name,
      target: dbB.name,
      continuous: true,
      user_ctx: {
        roles: ["_admin"]
      }
    };

    T(repDb.save(repDoc).ok);

    waitForSeq(dbA, dbB);
    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);
      T(copy !== null);
      T(copy.value === doc.value);
    }

    var tasks = JSON.parse(CouchDB.request("GET", "/_active_tasks").responseText);
    TEquals(1, tasks.length, "1 active task");
    TEquals(repDoc._id, tasks[0].doc_id, "replication doc id in active tasks");

    // add another doc to source, it will be replicated to target
    var docX = {
      _id: "foo1000",
      value: 1001
    };

    T(dbA.save(docX).ok);

    waitForSeq(dbA, dbB);
    var copy = dbB.open("foo1000");
    T(copy !== null);
    T(copy.value === 1001);

    var repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    T(repDoc1.source === repDoc.source);
    T(repDoc1.target === repDoc.target);
    T(repDoc1._replication_state === "triggered");
    T(typeof repDoc1._replication_state_time === "string");
    T(typeof repDoc1._replication_id  === "string");

    // Design documents are only replicated to local targets if the respective
    // replication document has a user_ctx filed with the "_admin" role in it.
    var ddoc = {
      _id: "_design/foobar",
      language: "javascript"
    };

    T(dbA.save(ddoc).ok);

    waitForSeq(dbA, dbB);
    var ddoc_copy = dbB.open("_design/foobar");
    T(ddoc_copy !== null);
    T(ddoc.language === "javascript");

    // update the design doc on source, test that the new revision is replicated
    ddoc.language = "erlang";
    T(dbA.save(ddoc).ok);
    T(ddoc._rev.indexOf("2-") === 0);

    waitForSeq(dbA, dbB);
    ddoc_copy = dbB.open("_design/foobar");
    T(ddoc_copy !== null);
    T(ddoc_copy._rev === ddoc._rev);
    T(ddoc.language === "erlang");

    // stop replication by deleting the replication document
    T(repDb.deleteDoc(repDoc1).ok);

    // add another doc to source, it will NOT be replicated to target
    var docY = {
      _id: "foo666",
      value: 999
    };

    T(dbA.save(docY).ok);

    wait(200); // is there a way to avoid wait here?
    var copy = dbB.open("foo666");
    T(copy === null);
  }


  function by_doc_ids_replication() {
    // to test that we can replicate docs with slashes in their IDs
    var docs2 = docs1.concat([
      {
        _id: "_design/mydesign",
        language : "javascript"
      }
    ]);

    populate_db(dbA, docs2);
    populate_db(dbB, []);

    var repDoc = {
      _id: "foo_cont_rep_doc",
      source: "http://" + CouchDB.host + "/" + dbA.name,
      target: dbB.name,
      doc_ids: ["foo666", "foo3", "_design/mydesign", "foo999", "foo1"]
    };
    T(repDb.save(repDoc).ok);

    waitForRep(repDb, repDoc, "completed");
    var copy = dbB.open("foo1");
    T(copy !== null);
    T(copy.value === 11);

    copy = dbB.open("foo2");
    T(copy === null);

    copy = dbB.open("foo3");
    T(copy !== null);
    T(copy.value === 33);

    copy = dbB.open("foo666");
    T(copy === null);

    copy = dbB.open("foo999");
    T(copy === null);

    copy = dbB.open("_design/mydesign");
    T(copy === null);

    repDoc = repDb.open(repDoc._id);
    T(typeof repDoc._replication_stats === "object", "doc has stats");
    var stats = repDoc._replication_stats;
    TEquals(3, stats.revisions_checked, "right # of revisions_checked");
    TEquals(3, stats.missing_revisions_found, "right # of missing_revisions_found");
    TEquals(3, stats.docs_read, "right # of docs_read");
    TEquals(2, stats.docs_written, "right # of docs_written");
    TEquals(1, stats.doc_write_failures, "right # of doc_write_failures");
    TEquals(dbA.info().update_seq, stats.checkpointed_source_seq,
      "right checkpointed_source_seq");
  }


  function successive_identical_replications() {
    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var repDoc1 = {
      _id: "foo_ident_rep_1",
      source: dbA.name,
      target: dbB.name
    };
    T(repDb.save(repDoc1).ok);

    waitForRep(repDb, repDoc1, "completed");
    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);
      T(copy !== null);
      T(copy.value === doc.value);
    }

    var repDoc1_copy = repDb.open(repDoc1._id);
    T(repDoc1_copy !== null);
    T(repDoc1_copy.source === repDoc1.source);
    T(repDoc1_copy.target === repDoc1.target);
    T(repDoc1_copy._replication_state === "completed");
    T(typeof repDoc1_copy._replication_state_time === "string");
    T(typeof repDoc1_copy._replication_id  === "string");
    T(typeof repDoc1_copy._replication_stats === "object", "doc has stats");
    var stats = repDoc1_copy._replication_stats;
    TEquals(docs1.length, stats.revisions_checked,
      "right # of revisions_checked");
    TEquals(docs1.length, stats.missing_revisions_found,
      "right # of missing_revisions_found");
    TEquals(docs1.length, stats.docs_read, "right # of docs_read");
    TEquals(docs1.length, stats.docs_written, "right # of docs_written");
    TEquals(0, stats.doc_write_failures, "right # of doc_write_failures");
    TEquals(dbA.info().update_seq, stats.checkpointed_source_seq,
      "right checkpointed_source_seq");

    var newDoc = {
      _id: "doc666",
      value: 666
    };
    T(dbA.save(newDoc).ok);

    wait(200);
    var newDoc_copy = dbB.open(newDoc._id);
    // not replicated because first replication is complete (not continuous)
    T(newDoc_copy === null);

    var repDoc2 = {
      _id: "foo_ident_rep_2",
      source: dbA.name,
      target: dbB.name
    };
    T(repDb.save(repDoc2).ok);

    waitForRep(repDb, repDoc2, "completed");
    var newDoc_copy = dbB.open(newDoc._id);
    T(newDoc_copy !== null);
    T(newDoc_copy.value === newDoc.value);

    var repDoc2_copy = repDb.open(repDoc2._id);
    T(repDoc2_copy !== null);
    T(repDoc2_copy.source === repDoc1.source);
    T(repDoc2_copy.target === repDoc1.target);
    T(repDoc2_copy._replication_state === "completed");
    T(typeof repDoc2_copy._replication_state_time === "string");
    T(typeof repDoc2_copy._replication_id === "string");
    T(repDoc2_copy._replication_id === repDoc1_copy._replication_id);
    T(typeof repDoc2_copy._replication_stats === "object", "doc has stats");
    stats = repDoc2_copy._replication_stats;
    TEquals(1, stats.revisions_checked, "right # of revisions_checked");
    TEquals(1, stats.missing_revisions_found,
      "right # of missing_revisions_found");
    TEquals(1, stats.docs_read, "right # of docs_read");
    TEquals(1, stats.docs_written, "right # of docs_written");
    TEquals(0, stats.doc_write_failures, "right # of doc_write_failures");
    TEquals(dbA.info().update_seq, stats.checkpointed_source_seq,
      "right checkpointed_source_seq");
  }


  // test the case where multiple replication docs (different IDs)
  // describe in fact the same replication (source, target, etc)
  function identical_rep_docs() {
    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var repDoc1 = {
      _id: "foo_dup_rep_doc_1",
      source: "http://" + CouchDB.host + "/" + dbA.name,
      target: dbB.name
    };
    var repDoc2 = {
      _id: "foo_dup_rep_doc_2",
      source: "http://" + CouchDB.host + "/" + dbA.name,
      target: dbB.name
    };

    T(repDb.save(repDoc1).ok);
    T(repDb.save(repDoc2).ok);

    waitForRep(repDb, repDoc1, "completed");
    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);
      T(copy !== null);
      T(copy.value === doc.value);
    }

    repDoc1 = repDb.open("foo_dup_rep_doc_1");
    T(repDoc1 !== null);
    T(repDoc1._replication_state === "completed", "identical");
    T(typeof repDoc1._replication_state_time === "string");
    T(typeof repDoc1._replication_id  === "string");

    repDoc2 = repDb.open("foo_dup_rep_doc_2");
    T(repDoc2 !== null);
    T(typeof repDoc2._replication_state === "undefined");
    T(typeof repDoc2._replication_state_time === "undefined");
    T(repDoc2._replication_id === repDoc1._replication_id);
  }


  // test the case where multiple replication docs (different IDs)
  // describe in fact the same continuous replication (source, target, etc)
  function identical_continuous_rep_docs() {
    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var repDoc1 = {
      _id: "foo_dup_cont_rep_doc_1",
      source: "http://" + CouchDB.host + "/" + dbA.name,
      target: dbB.name,
      continuous: true
    };
    var repDoc2 = {
      _id: "foo_dup_cont_rep_doc_2",
      source: "http://" + CouchDB.host + "/" + dbA.name,
      target: dbB.name,
      continuous: true
    };

    T(repDb.save(repDoc1).ok);
    T(repDb.save(repDoc2).ok);

    waitForSeq(dbA, dbB);
    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);
      T(copy !== null);
      T(copy.value === doc.value);
    }

    repDoc1 = repDb.open("foo_dup_cont_rep_doc_1");
    T(repDoc1 !== null);
    T(repDoc1._replication_state === "triggered");
    T(typeof repDoc1._replication_state_time === "string");
    T(typeof repDoc1._replication_id  === "string");

    repDoc2 = repDb.open("foo_dup_cont_rep_doc_2");
    T(repDoc2 !== null);
    T(typeof repDoc2._replication_state === "undefined");
    T(typeof repDoc2._replication_state_time === "undefined");
    T(repDoc2._replication_id === repDoc1._replication_id);

    var newDoc = {
      _id: "foo666",
      value: 999
    };
    T(dbA.save(newDoc).ok);

    waitForSeq(dbA, dbB);
    var copy = dbB.open("foo666");
    T(copy !== null);
    T(copy.value === 999);

    // deleting second replication doc, doesn't affect the 1st one and
    // neither it stops the replication
    T(repDb.deleteDoc(repDoc2).ok);
    repDoc1 = repDb.open("foo_dup_cont_rep_doc_1");
    T(repDoc1 !== null);
    T(repDoc1._replication_state === "triggered");
    T(typeof repDoc1._replication_state_time === "string");

    var newDoc2 = {
        _id: "foo5000",
        value: 5000
    };
    T(dbA.save(newDoc2).ok);

    waitForSeq(dbA, dbB);
    var copy = dbB.open("foo5000");
    T(copy !== null);
    T(copy.value === 5000);

    // deleting the 1st replication document stops the replication
    T(repDb.deleteDoc(repDoc1).ok);
    var newDoc3 = {
        _id: "foo1983",
        value: 1983
    };
    T(dbA.save(newDoc3).ok);

    wait(wait_rep_doc); //how to remove wait?
    var copy = dbB.open("foo1983");
    T(copy === null);
  }


  function test_replication_credentials_delegation() {
    populate_db(usersDb, []);

    var joeUserDoc = CouchDB.prepareUserDoc({
      name: "joe",
      roles: ["god", "erlanger"]
    }, "erly");
    T(usersDb.save(joeUserDoc).ok);

    var ddoc = {
      _id: "_design/beer",
      language: "javascript"
    };
    populate_db(dbA, docs1.concat([ddoc]));
    populate_db(dbB, []);

    T(dbB.setSecObj({
      admins: {
        names: [],
        roles: ["god"]
      }
    }).ok);

    var server_admins_config = [
      {
        section: "couch_httpd_auth",
        key: "iterations",
        value: "1"
      },
      {
        section: "admins",
        key: "fdmanana",
        value: "qwerty"
      }
    ];

    run_on_modified_server(server_admins_config, function() {

      T(CouchDB.login("fdmanana", "qwerty").ok);
      T(CouchDB.session().userCtx.name === "fdmanana");
      T(CouchDB.session().userCtx.roles.indexOf("_admin") !== -1);

      var repDoc = {
        _id: "foo_rep_del_doc_1",
        source: dbA.name,
        target: dbB.name,
        user_ctx: {
          name: "joe",
          roles: ["erlanger"]
        }
      };

      T(repDb.save(repDoc).ok);

      waitForRep(repDb, repDoc, "completed");
      for (var i = 0; i < docs1.length; i++) {
        var doc = docs1[i];
        var copy = dbB.open(doc._id);
        T(copy !== null);
        T(copy.value === doc.value);
      }

      // design doc was not replicated, because joe is not an admin of db B
      var doc = dbB.open(ddoc._id);
      T(doc === null);

      // now test the same replication but putting the role "god" in the
      // delegation user context property
      var repDoc2 = {
        _id: "foo_rep_del_doc_2",
        source: dbA.name,
        target: dbB.name,
        user_ctx: {
          name: "joe",
          roles: ["erlanger", "god"]
        }
      };
      T(repDb.save(repDoc2).ok);

      waitForRep(repDb, repDoc2, "completed");
      for (var i = 0; i < docs1.length; i++) {
        var doc = docs1[i];
        var copy = dbB.open(doc._id);
        T(copy !== null);
        T(copy.value === doc.value);
      }

      // because anyone with a 'god' role is an admin of db B, a replication
      // that is delegated to a 'god' role can write design docs to db B
      doc = dbB.open(ddoc._id);
      T(doc !== null);
      T(doc.language === ddoc.language);
    });
  }


  function continuous_replication_survives_restart() {
    var origRepDbName = CouchDB.request(
      "GET", "/_config/replicator/db").responseText;

    repDb.deleteDb();

    var xhr = CouchDB.request("PUT", "/_config/replicator/db", {
      body : JSON.stringify(repDb.name),
      headers: {"X-Couch-Persist": "false"}
    });
    T(xhr.status === 200);

    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var repDoc = {
      _id: "foo_cont_rep_survives_doc",
      source: dbA.name,
      target: dbB.name,
      continuous: true
    };

    T(repDb.save(repDoc).ok);

    waitForSeq(dbA, dbB);
    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);
      T(copy !== null);
      T(copy.value === doc.value);
    }

    repDb.ensureFullCommit();
    dbA.ensureFullCommit();

    restartServer();

    xhr = CouchDB.request("PUT", "/_config/replicator/db", {
      body : JSON.stringify(repDb.name),
      headers: {"X-Couch-Persist": "false"}
    });

    T(xhr.status === 200);

    // add another doc to source, it will be replicated to target
    var docX = {
      _id: "foo1000",
      value: 1001
    };

    T(dbA.save(docX).ok);

    waitForSeq(dbA, dbB);
    var copy = dbB.open("foo1000");
    T(copy !== null);
    T(copy.value === 1001);

    repDoc = waitForDocPos(repDb, "foo_cont_rep_survives_doc", 3);
    T(repDoc !== null);
    T(repDoc.continuous === true);

    // stop replication
    T(repDb.deleteDoc(repDoc).ok);

    xhr = CouchDB.request("PUT", "/_config/replicator/db", {
      body : origRepDbName,
      headers: {"X-Couch-Persist": "false"}
    });
    T(xhr.status === 200);
  }


  function rep_db_write_authorization() {
    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var server_admins_config = [
      {
        section: "admins",
        key: "fdmanana",
        value: "qwerty"
      }
    ];

    run_on_modified_server(server_admins_config, function() {
      var repDoc = {
        _id: "foo_rep_doc",
        source: dbA.name,
        target: dbB.name,
        continuous: true
      };

      T(CouchDB.login("fdmanana", "qwerty").ok);
      T(CouchDB.session().userCtx.name === "fdmanana");
      T(CouchDB.session().userCtx.roles.indexOf("_admin") !== -1);

      T(repDb.save(repDoc).ok);

      waitForRep(repDb, repDoc, "completed");

      for (var i = 0; i < docs1.length; i++) {
        var doc = docs1[i];
        var copy = dbB.open(doc._id);

        T(copy !== null);
        T(copy.value === doc.value);
      }

      repDoc = repDb.open("foo_rep_doc");
      T(repDoc !== null);
      repDoc.target = "test_suite_foo_db";
      repDoc.create_target = true;

      // Only the replicator can update replication documents.
      // Admins can only add and delete replication documents.
      try {
        repDb.save(repDoc);
        T(false && "Should have thrown an exception");
      } catch (x) {
        T(x["error"] === "forbidden");
      }
    });
  }


  function test_user_ctx_validation() {
    populate_db(dbA, docs1);
    populate_db(dbB, []);
    populate_db(usersDb, []);

    var joeUserDoc = CouchDB.prepareUserDoc({
      name: "joe",
      roles: ["erlanger", "bar"]
    }, "erly");
    var fdmananaUserDoc = CouchDB.prepareUserDoc({
      name: "fdmanana",
      roles: ["a", "b", "c"]
    }, "qwerty");

    TEquals(true, usersDb.save(joeUserDoc).ok);
    TEquals(true, usersDb.save(fdmananaUserDoc).ok);

    T(dbB.setSecObj({
      admins: {
        names: [],
        roles: ["god"]
      },
      readers: {
        names: [],
        roles: ["foo"]
      }
    }).ok);

    TEquals(true, CouchDB.login("joe", "erly").ok);
    TEquals("joe", CouchDB.session().userCtx.name);
    TEquals(-1, CouchDB.session().userCtx.roles.indexOf("_admin"));

    var repDoc = {
      _id: "foo_rep",
      source: CouchDB.protocol + CouchDB.host + "/" + dbA.name,
      target: dbB.name
    };

    try {
      repDb.save(repDoc);
      T(false, "Should have failed, user_ctx missing.");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc.user_ctx = {
      name: "john",
      roles: ["erlanger"]
    };

    try {
      repDb.save(repDoc);
      T(false, "Should have failed, wrong user_ctx.name.");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc.user_ctx = {
      name: "joe",
      roles: ["bar", "god", "erlanger"]
    };

    try {
      repDb.save(repDoc);
      T(false, "Should have failed, a bad role in user_ctx.roles.");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    // user_ctx.roles might contain only a subset of the user's roles
    repDoc.user_ctx = {
      name: "joe",
      roles: ["erlanger"]
    };

    TEquals(true, repDb.save(repDoc).ok);
    CouchDB.logout();

    waitForRep(repDb, repDoc, "error");
    var repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    TEquals(repDoc.source, repDoc1.source);
    TEquals(repDoc.target, repDoc1.target);
    TEquals("error", repDoc1._replication_state);
    TEquals("string", typeof repDoc1._replication_id);
    TEquals("string", typeof repDoc1._replication_state_time);

    TEquals(true, CouchDB.login("fdmanana", "qwerty").ok);
    TEquals("fdmanana", CouchDB.session().userCtx.name);
    TEquals(-1, CouchDB.session().userCtx.roles.indexOf("_admin"));

    try {
      T(repDb.deleteDoc(repDoc1).ok);
      T(false, "Shouldn't be able to delete replication document.");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    CouchDB.logout();
    TEquals(true, CouchDB.login("joe", "erly").ok);
    TEquals("joe", CouchDB.session().userCtx.name);
    TEquals(-1, CouchDB.session().userCtx.roles.indexOf("_admin"));

    T(repDb.deleteDoc(repDoc1).ok);
    CouchDB.logout();

    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);

      TEquals(null, copy);
    }

    T(dbB.setSecObj({
      admins: {
        names: [],
        roles: ["god", "erlanger"]
      },
      readers: {
        names: [],
        roles: ["foo"]
      }
    }).ok);

    TEquals(true, CouchDB.login("joe", "erly").ok);
    TEquals("joe", CouchDB.session().userCtx.name);
    TEquals(-1, CouchDB.session().userCtx.roles.indexOf("_admin"));

    repDoc = {
      _id: "foo_rep_2",
      source: CouchDB.protocol + CouchDB.host + "/" + dbA.name,
      target: dbB.name,
      user_ctx: {
        name: "joe",
        roles: ["erlanger"]
      }
    };

    TEquals(true, repDb.save(repDoc).ok);
    CouchDB.logout();

    waitForRep(repDb, repDoc, "complete");
    repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    TEquals(repDoc.source, repDoc1.source);
    TEquals(repDoc.target, repDoc1.target);
    TEquals("completed", repDoc1._replication_state);
    TEquals("string", typeof repDoc1._replication_id);
    TEquals("string", typeof repDoc1._replication_state_time);

    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);

      T(copy !== null);
      TEquals(doc.value, copy.value);
    }

    // Admins don't need to supply a user_ctx property in replication docs.
    // If they do not, the implicit user_ctx "user_ctx": {name: null, roles: []}
    // is used, meaning that design documents will not be replicated into
    // local targets
    T(dbB.setSecObj({
      admins: {
        names: [],
        roles: []
      },
      readers: {
        names: [],
        roles: []
      }
    }).ok);

    var ddoc = { _id: "_design/foo" };
    TEquals(true, dbA.save(ddoc).ok);

    repDoc = {
      _id: "foo_rep_3",
      source: CouchDB.protocol + CouchDB.host + "/" + dbA.name,
      target: dbB.name
    };

    TEquals(true, repDb.save(repDoc).ok);
    waitForRep(repDb, repDoc, "complete");
    repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    TEquals(repDoc.source, repDoc1.source);
    TEquals(repDoc.target, repDoc1.target);
    TEquals("completed", repDoc1._replication_state);
    TEquals("string", typeof repDoc1._replication_id);
    TEquals("string", typeof repDoc1._replication_state_time);

    var ddoc_copy = dbB.open(ddoc._id);
    T(ddoc_copy === null);

    repDoc = {
      _id: "foo_rep_4",
      source: CouchDB.protocol + CouchDB.host + "/" + dbA.name,
      target: dbB.name,
      user_ctx: {
        roles: ["_admin"]
      }
    };

    TEquals(true, repDb.save(repDoc).ok);
    waitForRep(repDb, repDoc, "complete");
    repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    TEquals(repDoc.source, repDoc1.source);
    TEquals(repDoc.target, repDoc1.target);
    TEquals("completed", repDoc1._replication_state);
    TEquals("string", typeof repDoc1._replication_id);
    TEquals("string", typeof repDoc1._replication_state_time);

    ddoc_copy = dbB.open(ddoc._id);
    T(ddoc_copy !== null);
  }


  function rep_doc_with_bad_rep_id() {
    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var repDoc = {
      _id: "foo_rep",
      source: dbA.name,
      target: dbB.name,
      replication_id: "1234abc"
    };
    T(repDb.save(repDoc).ok);

    waitForRep(repDb, repDoc, "completed");
    for (var i = 0; i < docs1.length; i++) {
      var doc = docs1[i];
      var copy = dbB.open(doc._id);
      T(copy !== null);
      T(copy.value === doc.value);
    }

    var repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    T(repDoc1.source === repDoc.source);
    T(repDoc1.target === repDoc.target);
    T(repDoc1._replication_state === "completed",
      "replication document with bad replication id failed");
    T(typeof repDoc1._replication_state_time === "string");
    T(typeof repDoc1._replication_id  === "string");
    T(repDoc1._replication_id !== "1234abc");
  }


  function swap_rep_db() {
    var repDb2 = new CouchDB("test_suite_rep_db_2");
    var dbA = new CouchDB("test_suite_rep_db_a");
    var dbA_copy = new CouchDB("test_suite_rep_db_a_copy");
    var dbB = new CouchDB("test_suite_rep_db_b");
    var dbB_copy = new CouchDB("test_suite_rep_db_b_copy");
    var dbC = new CouchDB("test_suite_rep_db_c");
    var dbC_copy = new CouchDB("test_suite_rep_db_c_copy");
    var repDoc1, repDoc2, repDoc3;
    var xhr, i, doc, copy, new_doc;

    populate_db(dbA, docs1);
    populate_db(dbB, docs1);
    populate_db(dbC, docs1);
    populate_db(dbA_copy, []);
    populate_db(dbB_copy, []);
    populate_db(dbC_copy, []);
    populate_db(repDb2, []);

    repDoc1 = {
      _id: "rep1",
      source: CouchDB.protocol + CouchDB.host + "/" + dbA.name,
      target: dbA_copy.name,
      continuous: true
    };
    repDoc2 = {
      _id: "rep2",
      source: CouchDB.protocol + CouchDB.host + "/" + dbB.name,
      target: dbB_copy.name,
      continuous: true
    };
    repDoc3 = {
      _id: "rep3",
      source: CouchDB.protocol + CouchDB.host + "/" + dbC.name,
      target: dbC_copy.name,
      continuous: true
    };

    TEquals(true, repDb.save(repDoc1).ok);
    TEquals(true, repDb.save(repDoc2).ok);

    waitForSeq(dbA, dbA_copy);
    waitForSeq(dbB, dbB_copy);

    xhr = CouchDB.request("PUT", "/_config/replicator/db",{
      body : JSON.stringify(repDb2.name),
      headers: {"X-Couch-Persist": "false"}
    });
    TEquals(200, xhr.status);

    // Temporary band-aid, give the replicator db some
    // time to make the switch
    wait(500);

    new_doc = {
      _id: "foo666",
      value: 666
    };

    TEquals(true, dbA.save(new_doc).ok);
    TEquals(true, dbB.save(new_doc).ok);
    waitForSeq(dbA, dbA_copy);
    waitForSeq(dbB, dbB_copy);

    TEquals(true, repDb2.save(repDoc3).ok);
    waitForSeq(dbC, dbC_copy);

    for (i = 0; i < docs1.length; i++) {
      doc = docs1[i];
      copy = dbA_copy.open(doc._id);
      T(copy !== null);
      TEquals(doc.value, copy.value);
      copy = dbB_copy.open(doc._id);
      T(copy !== null);
      TEquals(doc.value, copy.value);
      copy = dbC_copy.open(doc._id);
      T(copy !== null);
      TEquals(doc.value, copy.value);
    }

    // replications rep1 and rep2 should have been stopped when the replicator
    // database was swapped
    copy = dbA_copy.open(new_doc._id);
    TEquals(null, copy);
    copy = dbB_copy.open(new_doc._id);
    TEquals(null, copy);

    xhr = CouchDB.request("PUT", "/_config/replicator/db",{
      body : JSON.stringify(repDb.name),
      headers: {"X-Couch-Persist": "false"}
    });
    TEquals(200, xhr.status);

    // after setting the replicator database to the former, replications rep1
    // and rep2 should have been resumed, while rep3 was stopped
    TEquals(true, dbC.save(new_doc).ok);
    wait(1000);

    waitForSeq(dbA, dbA_copy);
    waitForSeq(dbB, dbB_copy);

    copy = dbA_copy.open(new_doc._id);
    T(copy !== null);
    TEquals(new_doc.value, copy.value);
    copy = dbB_copy.open(new_doc._id);
    T(copy !== null);
    TEquals(new_doc.value, copy.value);
    copy = dbC_copy.open(new_doc._id);
    TEquals(null, copy);
  }


  function compact_rep_db() {
    var dbA_copy = new CouchDB("test_suite_rep_db_a_copy");
    var dbB_copy = new CouchDB("test_suite_rep_db_b_copy");
    var repDoc1, repDoc2;
    var xhr, i, doc, copy, new_doc;
    var docs = makeDocs(1, 50);

    populate_db(dbA, docs);
    populate_db(dbB, docs);
    populate_db(dbA_copy, []);
    populate_db(dbB_copy, []);

    repDoc1 = {
      _id: "rep1",
      source: CouchDB.protocol + CouchDB.host + "/" + dbA.name,
      target: dbA_copy.name,
      continuous: true
    };
    repDoc2 = {
      _id: "rep2",
      source: CouchDB.protocol + CouchDB.host + "/" + dbB.name,
      target: dbB_copy.name,
      continuous: true
    };

    TEquals(true, repDb.save(repDoc1).ok);
    TEquals(true, repDb.save(repDoc2).ok);

    TEquals(true, repDb.compact().ok);
    TEquals(202, repDb.last_req.status);

    waitForSeq(dbA, dbA_copy);
    waitForSeq(dbB, dbB_copy);

    while (repDb.info().compact_running) {};

    for (i = 0; i < docs.length; i++) {
      copy = dbA_copy.open(docs[i]._id);
      T(copy !== null);
      copy = dbB_copy.open(docs[i]._id);
      T(copy !== null);
    }

    new_doc = {
      _id: "foo666",
      value: 666
    };

    TEquals(true, dbA.save(new_doc).ok);
    TEquals(true, dbB.save(new_doc).ok);

    waitForSeq(dbA, dbA_copy);
    waitForSeq(dbB, dbB_copy);

    copy = dbA.open(new_doc._id);
    T(copy !== null);
    TEquals(666, copy.value);
    copy = dbB.open(new_doc._id);
    T(copy !== null);
    TEquals(666, copy.value);
  }


  function error_state_replication() {
    populate_db(dbA, docs1);

    var repDoc = {
      _id: "foo_error_rep",
      source: dbA.name,
      target: "nonexistent_test_db"
    };
    T(repDb.save(repDoc).ok);

    waitForRep(repDb, repDoc, "error");
    var repDoc1 = repDb.open(repDoc._id);
    T(repDoc1 !== null);
    T(repDoc1._replication_state === "error");
    T(typeof repDoc1._replication_state_time === "string");
    T(typeof repDoc1._replication_id  === "string");
  }


  function rep_doc_field_validation() {
    var docs = makeDocs(1, 5);

    populate_db(dbA, docs);
    populate_db(dbB, []);

    var repDoc = {
       _id: "rep1",
       target: dbB.name
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because source field is missing");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc = {
       _id: "rep1",
       source: 123,
       target: dbB.name
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because source field is a number");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc = {
       _id: "rep1",
       source: dbA.name
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because target field is missing");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc = {
       _id: "rep1",
       source: dbA.name,
       target: null
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because target field is null");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc = {
       _id: "rep1",
       source: dbA.name,
       target: { url: 123 }
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because target.url field is not a string");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc = {
       _id: "rep1",
       source: dbA.name,
       target: { url: dbB.name, auth: null }
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because target.auth field is null");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc = {
       _id: "rep1",
       source: dbA.name,
       target: { url: dbB.name, auth: "foo:bar" }
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because target.auth field is not an object");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc = {
       _id: "rep1",
       source: dbA.name,
       target: dbB.name,
       continuous: "true"
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because continuous is not a boolean");
    } catch (x) {
      TEquals("forbidden", x.error);
    }

    repDoc = {
       _id: "rep1",
       source: dbA.name,
       target: dbB.name,
       filter: 123
    };

    try {
      repDb.save(repDoc);
      T(false, "should have failed because filter is not a string");
    } catch (x) {
      TEquals("forbidden", x.error);
    }
  }


  function test_invalid_filter() {
    // COUCHDB-1199 - replication document with a filter field that was invalid
    // crashed the CouchDB server.
    var repDoc1 = {
       _id: "rep1",
       source: "couch_foo_test_db",
       target: "couch_bar_test_db",
       filter: "test/foofilter"
    };

    TEquals(true, repDb.save(repDoc1).ok);

    waitForRep(repDb, repDoc1, "error");
    repDoc1 = repDb.open(repDoc1._id);
    TEquals("undefined", typeof repDoc1._replication_id);
    TEquals("error", repDoc1._replication_state);

    populate_db(dbA, docs1);
    populate_db(dbB, []);

    var repDoc2 = {
       _id: "rep2",
       source: dbA.name,
       target: dbB.name,
       filter: "test/foofilter"
    };

    TEquals(true, repDb.save(repDoc2).ok);

    waitForRep(repDb, repDoc2, "error");
    repDoc2 = repDb.open(repDoc2._id);
    TEquals("undefined", typeof repDoc2._replication_id);
    TEquals("error", repDoc2._replication_state);

    var ddoc = {
      _id: "_design/mydesign",
      language : "javascript",
      filters : {
        myfilter : (function(doc, req) {
          return true;
        }).toString()
      }
    };

    TEquals(true, dbA.save(ddoc).ok);

    var repDoc3 = {
       _id: "rep3",
       source: dbA.name,
       target: dbB.name,
       filter: "mydesign/myfilter"
    };

    TEquals(true, repDb.save(repDoc3).ok);

    waitForRep(repDb, repDoc3, "completed");
    repDoc3 = repDb.open(repDoc3._id);
    TEquals("string", typeof repDoc3._replication_id);
    TEquals("completed", repDoc3._replication_state);
  }


  function test_rep_db_update_security() {
    var dbA_copy = new CouchDB("test_suite_rep_db_a_copy");
    var dbB_copy = new CouchDB("test_suite_rep_db_b_copy");
    var repDoc1, repDoc2;
    var xhr, i, doc, copy, new_doc;
    var docs = makeDocs(1, 3);

    populate_db(dbA, docs);
    populate_db(dbB, docs);
    populate_db(dbA_copy, []);
    populate_db(dbB_copy, []);

    repDoc1 = {
      _id: "rep1",
      source: CouchDB.protocol + CouchDB.host + "/" + dbA.name,
      target: dbA_copy.name
    };
    repDoc2 = {
      _id: "rep2",
      source: CouchDB.protocol + CouchDB.host + "/" + dbB.name,
      target: dbB_copy.name
    };

    TEquals(true, repDb.save(repDoc1).ok);
    waitForRep(repDb, repDoc1, "completed");

    T(repDb.setSecObj({
      readers: {
        names: ["joe"]
      }
    }).ok);

    TEquals(true, repDb.save(repDoc2).ok);
    waitForRep(repDb, repDoc2, "completed");
  }

  // run all the tests
  var server_config = [
    {
      section: "couch_httpd_auth",
      key: "iterations",
      value: "1"
    },
    {
      section: "replicator",
      key: "db",
      value: repDb.name
    }
  ];

  repDb.deleteDb();
  run_on_modified_server(server_config, simple_replication);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, filtered_replication);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, continuous_replication);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, by_doc_ids_replication);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, successive_identical_replications);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, identical_rep_docs);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, identical_continuous_rep_docs);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, rep_db_write_authorization);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, rep_doc_with_bad_rep_id);

  var server_config_2 = server_config.concat([
    {
      section: "couch_httpd_auth",
      key: "authentication_db",
      value: usersDb.name
    }
  ]);

  repDb.deleteDb();
  usersDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config_2, test_user_ctx_validation);

  repDb.deleteDb();
  usersDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config_2, test_replication_credentials_delegation);

  repDb.deleteDb();
  restartServer();
  continuous_replication_survives_restart();

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, swap_rep_db);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, compact_rep_db);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, rep_doc_field_validation);


  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, test_invalid_filter);

  repDb.deleteDb();
  restartServer();
  run_on_modified_server(server_config, test_rep_db_update_security);

/*
 * Disabled, since error state would be set on the document only after
 * the exponential backoff retry done by the replicator database listener
 * terminates, which takes too much time for a unit test.
 */
/*
 * repDb.deleteDb();
 * restartServer();
 * run_on_modified_server(server_config, error_state_replication);
 */


  // cleanup
  repDb.deleteDb();
  usersDb.deleteDb();
  dbA.deleteDb();
  dbB.deleteDb();
  (new CouchDB("test_suite_rep_db_2")).deleteDb();
  (new CouchDB("test_suite_rep_db_c")).deleteDb();
  (new CouchDB("test_suite_rep_db_a_copy")).deleteDb();
  (new CouchDB("test_suite_rep_db_b_copy")).deleteDb();
  (new CouchDB("test_suite_rep_db_c_copy")).deleteDb();
};
