// 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.replication = function(debug) {
  if (debug) debugger;
  var host = CouchDB.host;
  var dbPairs = [
    {source:"test_suite_db_a",
      target:"test_suite_db_b"},
    {source:"test_suite_db_a",
      target:"http://" + host + "/test_suite_db_b"},
    {source:"http://" + host + "/test_suite_db_a",
      target:"test_suite_db_b"},
    {source:"http://" + host + "/test_suite_db_a",
      target:"http://" + host + "/test_suite_db_b"}
  ];
  var dbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"});
  var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"});
  var numDocs = 10;
  var xhr;
  for (var testPair = 0; testPair < dbPairs.length; testPair++) {
    var A = dbPairs[testPair].source;
    var B = dbPairs[testPair].target;

    dbA.deleteDb();
    dbA.createDb();
    dbB.deleteDb();
    dbB.createDb();

    var repTests = {
      // copy and paste and put your code in. delete unused steps.
      test_template: new function () {
        this.init = function(dbA, dbB) {
          // before anything has happened
        };
        this.afterAB1 = function(dbA, dbB) {
          // called after replicating src=A  tgt=B first time.
        };
        this.afterBA1 = function(dbA, dbB) {
          // called after replicating src=B  tgt=A first time.
        };
        this.afterAB2 = function(dbA, dbB) {
          // called after replicating src=A  tgt=B second time.
        };
        this.afterBA2 = function(dbA, dbB) {
          // etc...
        };
      },

      simple_test: new function () {
        this.init = function(dbA, dbB) {
          var docs = makeDocs(0, numDocs);
          dbA.bulkSave(docs);
        };

        this.afterAB1 = function(dbA, dbB) {
          for (var j = 0; j < numDocs; j++) {
            var docA = dbA.open("" + j);
            var docB = dbB.open("" + j);
            T(docA._rev == docB._rev);
          }
        };
      },

     deletes_test: new function () {
        // make sure deletes are replicated
        this.init = function(dbA, dbB) {
          T(dbA.save({_id:"foo1",value:"a"}).ok);
        };

        this.afterAB1 = function(dbA, dbB) {
          var docA = dbA.open("foo1");
          var docB = dbB.open("foo1");
          T(docA._rev == docB._rev);

          dbA.deleteDoc(docA);
        };

        this.afterAB2 = function(dbA, dbB) {
          T(dbA.open("foo1") == null);
          T(dbB.open("foo1") == null);
        };
      },

      deleted_test : new function() {
        // docs created and deleted on a single node are also replicated
        this.init = function(dbA, dbB) {
          T(dbA.save({_id:"del1",value:"a"}).ok);
          var docA = dbA.open("del1");
          dbA.deleteDoc(docA);
        };

        this.afterAB1 = function(dbA, dbB) {
          var rows = dbB.changes().results;
          var rowCnt = 0;
          for (var i=0; i < rows.length; i++) {
            if (rows[i].id == "del1") {
              rowCnt += 1;
              T(rows[i].deleted == true);
            }
          };
          T(rowCnt == 1);
        };
      },

      slashes_in_ids_test: new function () {
        // make sure docs with slashes in id replicate properly
        this.init = function(dbA, dbB) {
          dbA.save({ _id:"abc/def", val:"one" });
        };

        this.afterAB1 = function(dbA, dbB) {
          var docA = dbA.open("abc/def");
          var docB = dbB.open("abc/def");
          T(docA._rev == docB._rev);
        };
      },

      design_docs_test: new function() {
        // make sure design docs replicate properly
        this.init = function(dbA, dbB) {
          dbA.save({ _id:"_design/test" });
        };

        this.afterAB1 = function() {
          var docA = dbA.open("_design/test");
          var docB = dbB.open("_design/test");
          T(docA._rev == docB._rev);
        };
      },

      attachments_test: new function () {
        // Test attachments
        this.init = function(dbA, dbB) {
          dbA.save({
            _id:"bin_doc",
            _attachments:{
              "foo+bar.txt": {
                "type":"base64",
                "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
              }
            }
          });
          // make sure on design docs as well
          dbA.save({
            _id:"_design/with_bin",
            _attachments:{
              "foo+bar.txt": {
                "type":"base64",
                "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
              }
            }
          });
        };

        this.afterAB1 = function(dbA, dbB) {
          var xhr = CouchDB.request("GET",
            "/test_suite_db_a/bin_doc/foo%2Bbar.txt");
          T(xhr.responseText == "This is a base64 encoded text");

          xhr = CouchDB.request("GET",
            "/test_suite_db_b/bin_doc/foo%2Bbar.txt");
          T(xhr.responseText == "This is a base64 encoded text");

          // and the design-doc
          xhr = CouchDB.request("GET",
            "/test_suite_db_a/_design/with_bin/foo%2Bbar.txt");
          T(xhr.responseText == "This is a base64 encoded text");

          xhr = CouchDB.request("GET",
            "/test_suite_db_b/_design/with_bin/foo%2Bbar.txt");
          T(xhr.responseText == "This is a base64 encoded text");
        };
      },

      conflicts_test: new function () {
        // test conflicts
        this.init = function(dbA, dbB) {
          dbA.save({_id:"foo",value:"a"});
          dbB.save({_id:"foo",value:"b"});
        };

        this.afterBA1 = function(dbA, dbB) {
          var docA = dbA.open("foo", {conflicts: true});
          var docB = dbB.open("foo", {conflicts: true});

          // make sure the same rev is in each db
          T(docA._rev === docB._rev);

          // make sure the conflicts are the same in each db
          T(docA._conflicts[0] === docB._conflicts[0]);

          // delete a conflict.
          dbA.deleteDoc({_id:"foo", _rev:docA._conflicts[0]});
        };

        this.afterBA2 = function(dbA, dbB) {
          // open documents and include the conflict meta data
          var docA = dbA.open("foo", {conflicts: true, deleted_conflicts: true});
          var docB = dbB.open("foo", {conflicts: true, deleted_conflicts: true});

          // We should have no conflicts this time
          T(typeof docA._conflicts === "undefined");
          T(typeof docB._conflicts === "undefined");

          // They show up as deleted conflicts instead
          T(docA._deleted_conflicts[0] == docB._deleted_conflicts[0]);
        };
      }
    };

    var test;
    for(test in repTests) {
      if(repTests[test].init) {
        repTests[test].init(dbA, dbB);
      }
    }

    var result = CouchDB.replicate(A, B);

    var seqA = result.source_last_seq;
    T(0 == result.history[0].start_last_seq);
    T(typeof result.history[1] === "undefined");

    for(test in repTests) {
      if(repTests[test].afterAB1) repTests[test].afterAB1(dbA, dbB);
    }

    result = CouchDB.replicate(B, A);

    var seqB = result.source_last_seq;
    T(0 == result.history[0].start_last_seq);
    T(typeof result.history[1] === "undefined");

    for(test in repTests) {
      if(repTests[test].afterBA1) repTests[test].afterBA1(dbA, dbB);
    }

    var result2 = CouchDB.replicate(A, B);

    // each successful replication produces a new session id
    T(result2.session_id != result.session_id);

    T(seqA < result2.source_last_seq);
    T(seqA == result2.history[0].start_last_seq);
    T(result2.history[1].end_last_seq == seqA);

    seqA = result2.source_last_seq;

    for(test in repTests) {
      if(repTests[test].afterAB2) repTests[test].afterAB2(dbA, dbB);
    }

    result = CouchDB.replicate(B, A);

    T(seqB < result.source_last_seq);
    T(seqB == result.history[0].start_last_seq);
    T(result.history[1].end_last_seq == seqB);

    seqB = result.source_last_seq;

    for(test in repTests) {
      if(repTests[test].afterBA2) repTests[test].afterBA2(dbA, dbB);
    }

    // do an replication where nothing has changed
    result2 = CouchDB.replicate(B, A);
    T(result2.no_changes == true);
    T(result2.session_id == result.session_id);
  }

  // test optional automatic creation of the target db

  var dbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"});
  var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"});
  
  dbA.deleteDb();
  dbA.createDb();
  dbB.deleteDb();
  
  // local
  CouchDB.replicate(dbA.name, "test_suite_db_b", {
    body: {"create_target": true}
  });
  TEquals("test_suite_db_b", dbB.info().db_name,
    "Target database should exist");

  // remote
  dbB.deleteDb();
  CouchDB.replicate(dbA.name, "http://" + CouchDB.host + "/test_suite_db_b", {
    body: {"create_target": true}
  });
  TEquals("test_suite_db_b", dbB.info().db_name,
    "Target database should exist");

  // continuous
  var continuousResult = CouchDB.replicate(dbA.name, "test_suite_db_b", {
    body: {"continuous": true}
  });
  T(continuousResult.ok);
  T(continuousResult._local_id);

  var cancelResult = CouchDB.replicate(dbA.name, "test_suite_db_b", {
    body: {"cancel": true}
  });
  T(cancelResult.ok);
  T(continuousResult._local_id == cancelResult._local_id);

  try {
   var cancelResult2 = CouchDB.replicate(dbA.name, "test_suite_db_b", {
     body: {"cancel": true}
   });
  } catch (e) {
    T(e.error == "not_found");
  }
  // test replication object option doc_ids

  var dbA = new CouchDB("test_suite_rep_docs_db_a", {"X-Couch-Full-Commit":"false"});
  var dbB = new CouchDB("test_suite_rep_docs_db_b", {"X-Couch-Full-Commit":"false"});

  dbA.deleteDb();
  dbA.createDb();

  var all_docs = [
    {
      _id: "foo1",
      value: "a"
    },
    {
      _id: "foo2",
      value: "b"
    },
    {
      _id: "foo3",
      value: "c"
    },
    {
      _id: "slashed/foo",
      value: "s"
    },
    {
      _id: "_design/foobar",
      language: "javascript",
      value: "I am a design doc",
      filters: {
        idfilter: (function(doc, req) {
          return doc.value == Number(req.filter_value);
        }).toString()
      },
      views: {
        countview: (function(doc) {
          emit(doc.value, 1);
        }).toString()
      }
    }
  ];

  for (var i = 0; i < all_docs.length; i++) {
    T(dbA.save(all_docs[i]).ok);
  }

  var dbPairs = [
    {source:"test_suite_rep_docs_db_a",
      target:"test_suite_rep_docs_db_b"},
    {source:"test_suite_rep_docs_db_a",
      target:"http://" + host + "/test_suite_rep_docs_db_b"},
    {source:"http://" + host + "/test_suite_rep_docs_db_a",
      target:"test_suite_rep_docs_db_b"},
    {source:"http://" + host + "/test_suite_rep_docs_db_a",
      target:"http://" + host + "/test_suite_rep_docs_db_b"}
  ];

  var target_doc_ids = [
    ["foo1", "foo3", "foo666"],
    ["foo1", "foo666"],
    ["foo666", "foo2"],
    ["foo2", "foo9999", "foo1"],
    ["foo3", "slashed/foo"],
    ["foo3", "slashed%2Ffoo"],
    ["foo1", "_design/foobar"],
    ["foo1", "foo1001", "_design%2Ffoobar"]
  ];

  for (var i = 0; i < dbPairs.length; i++) {
    var src_db = dbPairs[i].source;
    var tgt_db = dbPairs[i].target;

    for (var j = 0; j < target_doc_ids.length; j++) {
      var doc_ids = target_doc_ids[j];
      var valid_doc_ids = [];
      var invalid_doc_ids = [];

      $.each(doc_ids, function(index, id) {
        var found = false;

        for (var k = 0; k < all_docs.length; k++) {
          var doc = all_docs[k];

          if (id === doc._id) {
            found = true;
            break;
          }
        }

        if (found) {
          valid_doc_ids.push(id);
        } else {
          invalid_doc_ids.push(id);
        }
      });

      dbB.deleteDb();
      dbB.createDb();

      var repResult = CouchDB.replicate(src_db, tgt_db, {
        body: {"doc_ids": doc_ids}
      });

      T(repResult.ok);
      T(repResult.docs_written === valid_doc_ids.length);
      T(repResult.docs_read === valid_doc_ids.length);
      T(repResult.doc_write_failures === 0);

      for (var k = 0; k < all_docs.length; k++) {
        var doc = all_docs[k];
        var tgt_doc = dbB.open(doc._id);

        if ($.inArray(doc._id, doc_ids) >= 0) {
          T(tgt_doc !== null);
          T(tgt_doc.value === doc.value);
        } else {
          T(tgt_doc === null);
        }
      }

      for (var k = 0; k < invalid_doc_ids.length; k++) {
        var tgt_doc = dbB.open(invalid_doc_ids[k]);

        T(tgt_doc === null);
      }
    }
  }

  // test filtered replication

  var sourceDb = new CouchDB(
    "test_suite_filtered_rep_db_a", {"X-Couch-Full-Commit":"false"}
  );

  sourceDb.deleteDb();
  sourceDb.createDb();

  T(sourceDb.save({_id:"foo1",value:1}).ok);
  T(sourceDb.save({_id:"foo2",value:2}).ok);
  T(sourceDb.save({_id:"foo3",value:3}).ok);
  T(sourceDb.save({_id:"foo4",value:4}).ok);
  T(sourceDb.save({
    "_id": "_design/mydesign",
    "language" : "javascript",
    "filters" : {
      "myfilter" : (function(doc, req) {
        if (doc.value < Number(req.query.maxvalue)) {
          return true;
        } else {
          return false;
        }
      }).toString()
    }
  }).ok);

  var dbPairs = [
    {source:"test_suite_filtered_rep_db_a",
      target:"test_suite_filtered_rep_db_b"},
    {source:"test_suite_filtered_rep_db_a",
      target:"http://" + host + "/test_suite_filtered_rep_db_b"},
    {source:"http://" + host + "/test_suite_filtered_rep_db_a",
      target:"test_suite_filtered_rep_db_b"},
    {source:"http://" + host + "/test_suite_filtered_rep_db_a",
      target:"http://" + host + "/test_suite_filtered_rep_db_b"}
  ];

  for (var i = 0; i < dbPairs.length; i++) {
    var targetDb = new CouchDB("test_suite_filtered_rep_db_b");
    targetDb.deleteDb();
    targetDb.createDb();

    var dbA = dbPairs[i].source;
    var dbB = dbPairs[i].target;

    var repResult = CouchDB.replicate(dbA, dbB, {
      body: {
        "filter" : "mydesign/myfilter",
        "query_params" : {
          "maxvalue": "3"
        }
      }
    });

    T(repResult.ok);
    T($.isArray(repResult.history));
    T(repResult.history.length === 1);
    T(repResult.history[0].docs_written === 2);
    T(repResult.history[0].docs_read === 2);
    T(repResult.history[0].doc_write_failures === 0);

    var docFoo1 = targetDb.open("foo1");
    T(docFoo1 !== null);
    T(docFoo1.value === 1);

    var docFoo2 = targetDb.open("foo2");
    T(docFoo2 !== null);
    T(docFoo2.value === 2);

    var docFoo3 = targetDb.open("foo3");
    T(docFoo3 === null);

    var docFoo4 = targetDb.open("foo4");
    T(docFoo4 === null);
  }

  // test for COUCHDB-868 - design docs' attachments not getting replicated
  // when doing a pull replication with HTTP basic auth
  dbA = new CouchDB("test_suite_db_a");
  dbB = new CouchDB("test_suite_db_b");
  var usersDb = new CouchDB("test_suite_auth");
  var lorem = CouchDB.request(
    "GET", "/_utils/script/test/lorem.txt").responseText;
  var lorem_b64 = CouchDB.request(
    "GET", "/_utils/script/test/lorem_b64.txt").responseText;

  usersDb.deleteDb();
  usersDb.createDb();
  dbA.deleteDb();
  dbA.createDb();
  dbB.deleteDb();
  dbB.createDb();

  var atts_ddoc = {
    _id: "_design/i_have_atts",
    language: "javascript"
  };
  T(dbA.save(atts_ddoc).ok);

  var rev = atts_ddoc._rev;
  var att_1_name = "lorem.txt";
  var att_2_name = "lorem.dat";
  var xhr = CouchDB.request(
    "PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_1_name + "?rev=" + rev, {
      headers: {"Content-Type": "text/plain;charset=utf-8"},
      body: lorem
  });
  rev = JSON.parse(xhr.responseText).rev;
  T(xhr.status === 201);
  xhr = CouchDB.request(
    "PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_2_name + "?rev=" + rev, {
      headers: {"Content-Type": "application/data"},
      body: lorem_b64
  });
  T(xhr.status === 201);

  var fdmananaUserDoc = CouchDB.prepareUserDoc({
    name: "fdmanana",
    roles: ["reader"]
  }, "qwerty");
  T(usersDb.save(fdmananaUserDoc).ok);

  T(dbA.setSecObj({
    admins: {
      names: [],
      roles: ["admin"]
    },
    readers: {
      names: [],
      roles: ["reader"]
    }
  }).ok);
  T(dbB.setSecObj({
    admins: {
      names: ["fdmanana"],
      roles: []
    }
  }).ok);

  var server_config = [
    {
      section: "couch_httpd_auth",
      key: "authentication_db",
      value: usersDb.name
    },
    // to prevent admin party mode
    {
      section: "admins",
      key: "joe",
      value: "erlang"
    }
  ];

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

    var repResult = CouchDB.replicate(
      "http://fdmanana:qwerty@" + host + "/" + dbA.name,
      dbB.name
    );
    T(repResult.ok === true);
    T(repResult.history instanceof Array);
    T(repResult.history.length === 1);
    T(repResult.history[0].docs_written === 1);
    T(repResult.history[0].docs_read === 1);
    T(repResult.history[0].doc_write_failures === 0);

    var atts_ddoc_copy = dbB.open(atts_ddoc._id);
    T(atts_ddoc_copy !== null);
    T(typeof atts_ddoc_copy._attachments === "object");
    T(atts_ddoc_copy._attachments !== null);
    T(att_1_name in atts_ddoc_copy._attachments);
    T(att_2_name in atts_ddoc_copy._attachments);

    var xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_1_name);
    T(xhr.status === 200);
    T(xhr.responseText === lorem);

    xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_2_name);
    T(xhr.status === 200);
    T(xhr.responseText === lorem_b64);

    CouchDB.logout();
    T(CouchDB.login("joe", "erlang").ok);
    T(dbA.setSecObj({
      admins: {
        names: [],
        roles: ["bar"]
      },
      readers: {
        names: [],
        roles: ["foo"]
      }
    }).ok);
    T(dbB.deleteDb().ok === true);
    T(dbB.createDb().ok === true);
    T(dbB.setSecObj({
      admins: {
        names: ["fdmanana"],
        roles: []
      }
    }).ok);
    CouchDB.logout();

    T(CouchDB.login("fdmanana", "qwerty").ok);
    T(CouchDB.session().userCtx.name === "fdmanana");
    T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1);
    try {
      repResult = CouchDB.replicate(
        "http://fdmanana:qwerty@" + host + "/" + dbA.name,
        dbB.name
      );
      T(false, "replication should have failed");
    } catch(x) {
      T(x.error === "unauthorized");
    }

    atts_ddoc_copy = dbB.open(atts_ddoc._id);
    T(atts_ddoc_copy === null);

    CouchDB.logout();
    T(CouchDB.login("joe", "erlang").ok);
  };

  run_on_modified_server(server_config, test_fun);

  // cleanup
  dbA.deleteDb();
  dbB.deleteDb();
  usersDb.deleteDb();
};
