// 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.

// Used by replication test
CouchDB.host = (typeof window == 'undefined' || !window) ? 
                  "127.0.0.1:5984" : window.location.host;

var tests = {

  // Do some basic tests.
  basics: function(debug) {
    var result = JSON.parse(CouchDB.request("GET", "/").responseText);
    T(result.couchdb == "Welcome"); 
    
    var db = new CouchDB("test_suite_db");
    db.deleteDb();

    // bug COUCHDB-100: DELETE on non-existent DB returns 500 instead of 404
    db.deleteDb();
    
    db.createDb();

    // PUT on existing DB should return 409 instead of 500
    xhr = CouchDB.request("PUT", "/test_suite_db/");
    T(xhr.status == 409);
    if (debug) debugger;

    // Get the database info, check the db_name
    T(db.info().db_name == "test_suite_db");

    // Get the database info, check the doc_count
    T(db.info().doc_count == 0);

    // create a document and save it to the database
    var doc = {_id:"0",a:1,b:1};
    var result = db.save(doc);

    T(result.ok==true); // return object has an ok member with a value true
    T(result.id); // the _id of the document is set.
    T(result.rev); // the revision id of the document is set.

    // Verify the input doc is now set with the doc id and rev
    // (for caller convenience).
    T(doc._id == result.id && doc._rev == result.rev);

    var id = result.id; // save off the id for later

    // make sure the revs_info status is good
    var doc = db.open(id, {revs_info:true});
    T(doc._revs_info[0].status == "available");

    // Create some more documents.
    // Notice the use of the ok member on the return result.
    T(db.save({_id:"1",a:2,b:4}).ok);
    T(db.save({_id:"2",a:3,b:9}).ok);
    T(db.save({_id:"3",a:4,b:16}).ok);

    // Check the database doc count
    T(db.info().doc_count == 4);

    // Test a simple map functions

    // create a map function that selects all documents whose "a" member
    // has a value of 4, and then returns the document's b value.
    var mapFunction = function(doc){
      if (doc.a==4)
        emit(null, doc.b);
    };

    results = db.query(mapFunction);

    // verify only one document found and the result value (doc.b).
    T(results.total_rows == 1 && results.rows[0].value == 16);

    // reopen document we saved earlier
    existingDoc = db.open(id);

    T(existingDoc.a==1);

    //modify and save
    existingDoc.a=4;
    db.save(existingDoc);

    // redo the map query
    results = db.query(mapFunction);

    // the modified document should now be in the results.
    T(results.total_rows == 2);

    // write 2 more documents
    T(db.save({a:3,b:9}).ok);
    T(db.save({a:4,b:16}).ok);

    results = db.query(mapFunction);

    // 1 more document should now be in the result.
    T(results.total_rows == 3);
    T(db.info().doc_count == 6);

    var reduceFunction = function(keys, values){
      return sum(values);
    };

    results = db.query(mapFunction, reduceFunction);

    T(results.rows[0].value == 33);

    // delete a document
    T(db.deleteDoc(existingDoc).ok);

    // make sure we can't open the doc
    T(db.open(existingDoc._id) == null);

    results = db.query(mapFunction);

    // 1 less document should now be in the results.
    T(results.total_rows == 2);
    T(db.info().doc_count == 5);

    // make sure we can still open the old rev of the deleted doc
    T(db.open(existingDoc._id, {rev: existingDoc._rev}) != null);
    
    // make sure restart works
    restartServer();
  },
  all_docs: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;
    
    // Create some more documents.
    // Notice the use of the ok member on the return result.
    T(db.save({_id:"0",a:1,b:1}).ok);
    T(db.save({_id:"3",a:4,b:16}).ok);
    T(db.save({_id:"1",a:2,b:4}).ok);
    T(db.save({_id:"2",a:3,b:9}).ok);

    // Check the all docs
    var results = db.allDocs();
    var rows = results.rows;

    T(results.total_rows == results.rows.length);

    for(var i=0; i < rows.length; i++) {
      T(rows[i].id >= "0" && rows[i].id <= "4");
    }
    
    // Check _all_docs with descending=true
    var desc = db.allDocs({descending:true});
    T(desc.total_rows == desc.rows.length);
 
    // Check _all_docs offset
    var all = db.allDocs({startkey:"2"});
    T(all.offset == 2);
    
    // check that the docs show up in the seq view in the order they were created
    var all_seq = db.allDocsBySeq();
    var ids = ["0","3","1","2"];
    for (var i=0; i < all_seq.rows.length; i++) {
      var row = all_seq.rows[i];
      T(row.id == ids[i]);
    };
    
    // check that deletions also show up right
    var doc1 = db.open("1");
    var deleted = db.deleteDoc(doc1);
    T(deleted.ok);
    all_seq = db.allDocsBySeq();
    
    // the deletion should make doc id 1 have the last seq num
    T(all_seq.rows.length == 4);
    T(all_seq.rows[3].id == "1");
    T(all_seq.rows[3].value.deleted);

    // is this a bug?
    // T(all_seq.rows.length == all_seq.total_rows);
    
    // do an update
    var doc2 = db.open("3");
    doc2.updated = "totally";
    db.save(doc2);
    all_seq = db.allDocsBySeq();
    
    // the update should make doc id 3 have the last seq num
    T(all_seq.rows.length == 4);
    T(all_seq.rows[3].id == "3");

    // ok now lets see what happens with include docs
    all_seq = db.allDocsBySeq({include_docs: true});
    T(all_seq.rows.length == 4);
    T(all_seq.rows[3].id == "3");
    T(all_seq.rows[3].doc.updated == "totally");

    // and on the deleted one, no doc
    T(all_seq.rows[2].value.deleted);
    T(!all_seq.rows[2].doc);
  },
  
  // Do some edit conflict detection tests
  conflicts: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    // create a doc and save
    var doc = {_id:"foo",a:1,b:1};
    T(db.save(doc).ok);

    // reopen
    var doc2 = db.open(doc._id);

    // ensure the revisions are the same
    T(doc._id == doc2._id && doc._rev == doc2._rev);

    // edit the documents.
    doc.a = 2;
    doc2.a = 3;

    // save one document
    T(db.save(doc).ok);

    // save the other document
    try {
      db.save(doc2);  // this should generate a conflict exception
      T("no save conflict 1" && false); // we shouldn't hit here
    } catch (e) {
      T(e.error == "conflict");
    }

    // Now clear out the _rev member and save. This indicates this document is
    // new, not based on an existing revision.
    doc2._rev = undefined;
    try {
      db.save(doc2); // this should generate a conflict exception
      T("no save conflict 2" && false); // we shouldn't hit here
    } catch (e) {
      T(e.error == "conflict");
    }

    // Now delete the document from the database
    T(db.deleteDoc(doc).ok);

    T(db.save(doc2).ok);  // we can save a new document over a deletion without
                          // knowing the deletion rev.
  },

  recreate_doc: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    // First create a new document with the ID "foo", and delete it again
    var doc = {_id: "foo", a: "bar", b: 42};
    T(db.save(doc).ok);
    T(db.deleteDoc(doc).ok);

    // Now create a new document with the same ID, save it, and then modify it
    // This should work fine, but currently results in a conflict error, at
    // least "sometimes"
    for (var i = 0; i < 10; i++) {
      doc = {_id: "foo"};
      T(db.save(doc).ok);
      doc = db.open("foo");
      doc.a = "baz";
      try {
        T(db.save(doc).ok);
      } finally {
        // And now, we can't even delete the document anymore :/
        T(db.deleteDoc(doc).rev != undefined);
      }
    }
  },

  copy_move_doc: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    // copy a doc
    T(db.save({_id:"doc_to_be_copied",v:1}).ok);
    var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
      headers: {"Destination":"doc_that_was_copied"}
    });

    T(xhr.status == 201);
    T(db.open("doc_that_was_copied").v == 1);

    // move a doc

    // test error condition
    var xhr = CouchDB.request("MOVE", "/test_suite_db/doc_to_be_copied", {
      headers: {"Destination":"doc_that_was_moved"}
    });
    T(xhr.status == 400); // bad request, MOVE requires source rev.

    var rev = db.open("doc_to_be_copied")._rev;
    var xhr = CouchDB.request("MOVE", "/test_suite_db/doc_to_be_copied?rev=" + rev, {
      headers: {"Destination":"doc_that_was_moved"}
    });

    T(xhr.status == 201);
    T(db.open("doc_that_was_moved").v == 1);
    T(db.open("doc_to_be_copied") == null);

    // COPY with existing target
    T(db.save({_id:"doc_to_be_copied",v:1}).ok);
    var doc = db.save({_id:"doc_to_be_overwritten",v:2});
    T(doc.ok);

    // error condition
    var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
        headers: {"Destination":"doc_to_be_overwritten"}
    });
    T(xhr.status == 412); // conflict

    var rev = db.open("doc_to_be_overwritten")._rev;
    var xhr = CouchDB.request("COPY", "/test_suite_db/doc_to_be_copied", {
      headers: {"Destination":"doc_to_be_overwritten?rev=" + rev}
    });
    T(xhr.status == 201);

    var over = db.open("doc_to_be_overwritten");
    T(rev != over._rev);
    T(over.v == 1);
  },

  uuids: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;
    
    // a single UUID without an explicit count
    var xhr = CouchDB.request("POST", "/_uuids");
    T(xhr.status == 200);
    var result = JSON.parse(xhr.responseText);
    T(result.uuids.length == 1);
    var first = result.uuids[0];

    // a single UUID with an explicit count
    xhr = CouchDB.request("POST", "/_uuids?count=1");
    T(xhr.status == 200);
    result = JSON.parse(xhr.responseText);
    T(result.uuids.length == 1);
    var second = result.uuids[0];
    T(first != second);

    // no collisions with 1,000 UUIDs
    xhr = CouchDB.request("POST", "/_uuids?count=1000");
    T(xhr.status == 200);
    result = JSON.parse(xhr.responseText);
    T( result.uuids.length == 1000 );
    var seen = {};
    for(var i in result.uuids) {
      var id = result.uuids[i];
      T(seen[id] === undefined);
      seen[id] = 1;
    }
    
    // check our library
  },
  
  bulk_docs: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var docs = makeDocs(5);

    // Create the docs
    var result = db.bulkSave(docs);
    T(result.ok);
    T(result.new_revs.length == 5);
    for (var i = 0; i < 5; i++) {
      T(result.new_revs[i].id == docs[i]._id);
      T(result.new_revs[i].rev);
      docs[i].string = docs[i].string + ".00";
    }

    // Update the docs
    result = db.bulkSave(docs);
    T(result.ok);
    T(result.new_revs.length == 5);
    for (i = 0; i < 5; i++) {
      T(result.new_revs[i].id == i.toString());
      docs[i]._deleted = true;
    }

    // Delete the docs
    result = db.bulkSave(docs);
    T(result.ok);
    T(result.new_revs.length == 5);
    for (i = 0; i < 5; i++) {
      T(db.open(docs[i]._id) == null);
    }
    
    // verify creating a document with no id returns a new id
    var req = CouchDB.request("POST", "/test_suite_db/_bulk_docs", {
      body: JSON.stringify({"docs": [{"foo":"bar"}]})
    });
    result = JSON.parse(req.responseText);
    
    T(result.new_revs[0].id != "");
    T(result.new_revs[0].rev != "");
  },

  // test saving a semi-large quanitity of documents and do some view queries.
  lots_of_docs: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    // keep number lowish for now to keep tests fasts. Crank up manually to
    // to really test.
    var numDocsToCreate = 500;

    for(var i=0; i < numDocsToCreate; i += 100) {
        var createNow = Math.min(numDocsToCreate - i, 100);
        var docs = makeDocs(i, i + createNow);
        T(db.bulkSave(docs).ok);
    }

    // query all documents, and return the doc.integer member as a key.
    results = db.query(function(doc){ emit(doc.integer, null) });

    T(results.total_rows == numDocsToCreate);

    // validate the keys are ordered ascending
    for(var i=0; i<numDocsToCreate; i++) {
      T(results.rows[i].key==i);
    }

    // do the query again, but with descending output
    results = db.query(function(doc){ emit(doc.integer, null) }, null, {
      descending: true
    });

    T(results.total_rows == numDocsToCreate);

    // validate the keys are ordered descending
    for(var i=0; i<numDocsToCreate; i++) {
      T(results.rows[numDocsToCreate-1-i].key==i);
    }
  },

  reduce: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;
    var numDocs = 500
    var docs = makeDocs(1,numDocs + 1);
    T(db.bulkSave(docs).ok);
    var summate = function(N) {return (N+1)*N/2;};

    var map = function (doc) {
        emit(doc.integer, doc.integer);
        emit(doc.integer, doc.integer)};
    var reduce = function (keys, values) { return sum(values); };
    var result = db.query(map, reduce);
    T(result.rows[0].value == 2*summate(numDocs));

    result = db.query(map, reduce, {startkey: 4, endkey: 4});
    T(result.rows[0].value == 8);

    result = db.query(map, reduce, {startkey: 4, endkey: 5});
    T(result.rows[0].value == 18);

    result = db.query(map, reduce, {startkey: 4, endkey: 6});
    T(result.rows[0].value == 30);

    result = db.query(map, reduce, {group:true, count:3});
    T(result.rows[0].value == 2);
    T(result.rows[1].value == 4);
    T(result.rows[2].value == 6);

    for(var i=1; i<numDocs/2; i+=30) {
      result = db.query(map, reduce, {startkey: i, endkey: numDocs - i});
      T(result.rows[0].value == 2*(summate(numDocs-i) - summate(i-1)));
    }

    db.deleteDb();
    db.createDb();

    for(var i=1; i <= 5; i++) {

      for(var j=0; j < 10; j++) {
        // these docs are in the order of the keys collation, for clarity
        var docs = [];
        docs.push({keys:["a"]});
        docs.push({keys:["a"]});
        docs.push({keys:["a", "b"]});
        docs.push({keys:["a", "b"]});
        docs.push({keys:["a", "b", "c"]});
        docs.push({keys:["a", "b", "d"]});
        docs.push({keys:["a", "c", "d"]});
        docs.push({keys:["d"]});
        docs.push({keys:["d", "a"]});
        docs.push({keys:["d", "b"]});
        docs.push({keys:["d", "c"]});
        T(db.bulkSave(docs).ok);
        T(db.info().doc_count == ((i - 1) * 10 * 11) + ((j + 1) * 11));
      }

      map = function (doc) {emit(doc.keys, 1)};
      reduce = function (keys, values) { return sum(values); };

      var results = db.query(map, reduce, {group:true});

      //group by exact key match
      T(equals(results.rows[0], {key:["a"],value:20*i}));
      T(equals(results.rows[1], {key:["a","b"],value:20*i}));
      T(equals(results.rows[2], {key:["a", "b", "c"],value:10*i}));
      T(equals(results.rows[3], {key:["a", "b", "d"],value:10*i}));

      // test to make sure group reduce and count params provide valid json
      var results = db.query(map, reduce, {group: true, count: 2});
      T(equals(results.rows[0], {key: ["a"], value: 20*i}));
      T(equals(results.rows.length, 2));

      //group by the first element in the key array
      var results = db.query(map, reduce, {group_level:1});
      T(equals(results.rows[0], {key:["a"],value:70*i}));
      T(equals(results.rows[1], {key:["d"],value:40*i}));

      //group by the first 2 elements in the key array
      var results = db.query(map, reduce, {group_level:2});
      T(equals(results.rows[0], {key:["a"],value:20*i}));
      T(equals(results.rows[1], {key:["a","b"],value:40*i}));
      T(equals(results.rows[2], {key:["a","c"],value:10*i}));
      T(equals(results.rows[3], {key:["d"],value:10*i}));
      T(equals(results.rows[4], {key:["d","a"],value:10*i}));
      T(equals(results.rows[5], {key:["d","b"],value:10*i}));
      T(equals(results.rows[6], {key:["d","c"],value:10*i}));
    }

    // now test out more complex reductions that need to use the combine option.

    db.deleteDb();
    db.createDb();


    var map = function (doc) {emit(doc.val, doc.val)};
    var reduceCombine = function (keys, values, rereduce) {
        // This computes the standard deviation of the mapped results
        var stdDeviation=0.0;
        var count=0;
        var total=0.0;
        var sqrTotal=0.0;

        if (!rereduce) {
          // This is the reduce phase, we are reducing over emitted values from
          // the map functions.
          for(var i in values) {
            total = total + values[i];
            sqrTotal = sqrTotal + (values[i] * values[i]);
          }
          count = values.length;
        }
        else {
          // This is the rereduce phase, we are re-reducing previosuly
          // reduced values.
          for(var i in values) {
            count = count + values[i].count;
            total = total + values[i].total;
            sqrTotal = sqrTotal + values[i].sqrTotal;
          }
        }

        var variance =  (sqrTotal - ((total * total)/count)) / count;
        stdDeviation = Math.sqrt(variance);

        // the reduce result. It contains enough information to be rereduced
        // with other reduce results.
        return {"stdDeviation":stdDeviation,"count":count,
            "total":total,"sqrTotal":sqrTotal};
      };

      // Save a bunch a docs.

    for(var i=0; i < 10; i++) {
      var docs = [];
      docs.push({val:10});
      docs.push({val:20});
      docs.push({val:30});
      docs.push({val:40});
      docs.push({val:50});
      docs.push({val:60});
      docs.push({val:70});
      docs.push({val:80});
      docs.push({val:90});
      docs.push({val:100});
      T(db.bulkSave(docs).ok);
    }
    
    var results = db.query(map, reduceCombine);
    
    var difference = results.rows[0].value.stdDeviation - 28.722813232690143;
    // account for floating point rounding error
    T(Math.abs(difference) < 0.0000000001);
    
  },

  reduce_false: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var numDocs = 5;
    var docs = makeDocs(1,numDocs + 1);
    T(db.bulkSave(docs).ok);
    var summate = function(N) {return (N+1)*N/2;};

    var designDoc = {
      _id:"_design/test",
      language: "javascript",
      views: {
        summate: {map:"function (doc) {emit(doc.integer, doc.integer)};",
                  reduce:"function (keys, values) { return sum(values); };"},
      }
    };
    T(db.save(designDoc).ok);

    // Test that the reduce works
    var res = db.view('test/summate');
    T(res.rows.length == 1 && res.rows[0].value == summate(5));
    
    //Test that we get our docs back
    res = db.view('test/summate', {reduce: false});
    T(res.rows.length == 5);
    for(var i=0; i<5; i++)
    {
      T(res.rows[i].value == i+1);
    }
  },

  multiple_rows: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var nc = {_id:"NC", cities:["Charlotte", "Raleigh"]};
    var ma = {_id:"MA", cities:["Boston", "Lowell", "Worcester", "Cambridge", "Springfield"]};
    var fl = {_id:"FL", cities:["Miami", "Tampa", "Orlando", "Springfield"]};

    T(db.save(nc).ok);
    T(db.save(ma).ok);
    T(db.save(fl).ok);

    var generateListOfCitiesAndState = "function(doc) {" +
    " for (var i = 0; i < doc.cities.length; i++)" +
    "  emit(doc.cities[i] + \", \" + doc._id, null);" +
    "}";

    var results = db.query(generateListOfCitiesAndState);
    var rows = results.rows;

    T(rows[0].key == "Boston, MA");
    T(rows[1].key == "Cambridge, MA");
    T(rows[2].key == "Charlotte, NC");
    T(rows[3].key == "Lowell, MA");
    T(rows[4].key == "Miami, FL");
    T(rows[5].key == "Orlando, FL");
    T(rows[6].key == "Raleigh, NC");
    T(rows[7].key == "Springfield, FL");
    T(rows[8].key == "Springfield, MA");
    T(rows[9].key == "Tampa, FL");
    T(rows[10].key == "Worcester, MA");

    // add another city to NC
    nc.cities.push("Wilmington");
    T(db.save(nc).ok);

    var results = db.query(generateListOfCitiesAndState);
    var rows = results.rows;

    T(rows[0].key == "Boston, MA");
    T(rows[1].key == "Cambridge, MA");
    T(rows[2].key == "Charlotte, NC");
    T(rows[3].key == "Lowell, MA");
    T(rows[4].key == "Miami, FL");
    T(rows[5].key == "Orlando, FL");
    T(rows[6].key == "Raleigh, NC");
    T(rows[7].key == "Springfield, FL");
    T(rows[8].key == "Springfield, MA");
    T(rows[9].key == "Tampa, FL");
    T(rows[10].key == "Wilmington, NC");
    T(rows[11].key == "Worcester, MA");

    // now delete MA
    T(db.deleteDoc(ma).ok);

    var results = db.query(generateListOfCitiesAndState);
    var rows = results.rows;

    T(rows[0].key == "Charlotte, NC");
    T(rows[1].key == "Miami, FL");
    T(rows[2].key == "Orlando, FL");
    T(rows[3].key == "Raleigh, NC");
    T(rows[4].key == "Springfield, FL");
    T(rows[5].key == "Tampa, FL");
    T(rows[6].key == "Wilmington, NC");
  },

  large_docs: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var longtext = "0123456789\n";

    for (var i=0; i<10; i++) {
      longtext = longtext + longtext
    }
    T(db.save({"longtest":longtext}).ok);
    T(db.save({"longtest":longtext}).ok);
    T(db.save({"longtest":longtext}).ok);
    T(db.save({"longtest":longtext}).ok);

    // query all documents, and return the doc.foo member as a key.
    results = db.query(function(doc){
        emit(null, doc.longtest);
    });
  },

  utf8: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var texts = [];

    texts[0] = "1. Ascii: hello"
    texts[1] = "2. Russian: На берегу пустынных волн"
    texts[2] = "3. Math: ∮ E⋅da = Q,  n → ∞, ∑ f(i) = ∏ g(i),"
    texts[3] = "4. Geek: STARGΛ̊TE SG-1"
    texts[4] = "5. Braille: ⡌⠁⠧⠑ ⠼⠁⠒  ⡍⠜⠇⠑⠹⠰⠎ ⡣⠕⠌"

    // check that we can save a reload with full fidelity
    for (var i=0; i<texts.length; i++) {
      T(db.save({_id:i.toString(), text:texts[i]}).ok);
    }

    for (var i=0; i<texts.length; i++) {
      T(db.open(i.toString()).text == texts[i]);
    }

    // check that views and key collation don't blow up
    var rows = db.query(function(doc) { emit(null, doc.text) }).rows;
    for (var i=0; i<texts.length; i++) {
      T(rows[i].value == texts[i]);
    }
  },

  attachments: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var binAttDoc = {
      _id: "bin_doc",
      _attachments:{
        "foo.txt": {
          content_type:"text/plain",
          data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
        }
      }
    }

    T(db.save(binAttDoc).ok);

    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt");
    T(xhr.responseText == "This is a base64 encoded text");
    T(xhr.getResponseHeader("Content-Type") == "text/plain");

    // empty attachment
    var binAttDoc2 = {
      _id: "bin_doc2",
      _attachments:{
        "foo.txt": {
          content_type:"text/plain",
          data: ""
        }
      }
    }

    T(db.save(binAttDoc2).ok);

    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc2/foo.txt");
    T(xhr.responseText.length == 0);
    T(xhr.getResponseHeader("Content-Type") == "text/plain");

    // test RESTful doc API

    var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc2/foo2.txt?rev=" + binAttDoc2._rev, {
      body:"This is no base64 encoded text",
      headers:{"Content-Type": "text/plain;charset=utf-8"}
    });
    T(xhr.status == 201);
    var rev = JSON.parse(xhr.responseText).rev;

    binAttDoc2 = db.open("bin_doc2");

    T(binAttDoc2._attachments["foo.txt"] !== undefined);
    T(binAttDoc2._attachments["foo2.txt"] !== undefined);
    T(binAttDoc2._attachments["foo2.txt"].content_type == "text/plain;charset=utf-8");
    T(binAttDoc2._attachments["foo2.txt"].length == 30);

    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc2/foo2.txt");
    T(xhr.responseText == "This is no base64 encoded text");
    T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");
    
    // test without rev, should fail
    var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc2/foo2.txt");
    T(xhr.status == 412);

    // test with rev, should not fail
    var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc2/foo2.txt?rev=" + rev);
    T(xhr.status == 200);
    
    
    // test binary data
    var bin_data = "JHAPDO*AU£PN ){(3u[d 93DQ9¡€])}    ææøo'∂ƒæ≤çæππ•¥∫¶®#†π¶®¥π€ª®˙π8np";
    var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt", {
      headers:{"Content-Type":"text/plain;charset=utf-8"},
      body:bin_data
    });
    T(xhr.status == 201);
    var rev = JSON.parse(xhr.responseText).rev;
    
    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt");
    T(xhr.responseText == bin_data);
    T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");
    
    var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt", {
      headers:{"Content-Type":"text/plain;charset=utf-8"},
      body:bin_data
    });
    T(xhr.status == 412);

    var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev, {
      headers:{"Content-Type":"text/plain;charset=utf-8"},
      body:bin_data
    });
    T(xhr.status == 201);
    var rev = JSON.parse(xhr.responseText).rev;

    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt");
    T(xhr.responseText == bin_data);
    T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");

    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev);
    T(xhr.responseText == bin_data);
    T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");

    var xhr = CouchDB.request("DELETE", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev);
    T(xhr.status == 200);
    
    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc3/attachment.txt?rev=" + rev);
    T(xhr.status == 404);

    // empty attachments
    var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc4/attachment.txt", {
      headers:{"Content-Type":"text/plain;charset=utf-8"},
      body:""
    });
    T(xhr.status == 201);
    var rev = JSON.parse(xhr.responseText).rev;

    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc4/attachment.txt");
    T(xhr.status == 200);
    T(xhr.responseText.length == 0);
    
    // overwrite previsously empty attachment
    var xhr = CouchDB.request("PUT", "/test_suite_db/bin_doc4/attachment.txt?rev=" + rev, {
      headers:{"Content-Type":"text/plain;charset=utf-8"},
      body:"This is a string"
    });
    T(xhr.status == 201);

    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc4/attachment.txt");
    T(xhr.status == 200);
    T(xhr.responseText == "This is a string");
    
  },

  content_negotiation: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;
    var xhr;

    xhr = CouchDB.request("GET", "/test_suite_db/");
    T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");

    xhr = CouchDB.request("GET", "/test_suite_db/", {
      headers: {"Accept": "text/html;text/plain;*/*"}
    });
    T(xhr.getResponseHeader("Content-Type") == "text/plain;charset=utf-8");

    xhr = CouchDB.request("GET", "/test_suite_db/", {
      headers: {"Accept": "application/json"}
    });
    T(xhr.getResponseHeader("Content-Type") == "application/json");
  },

  design_docs: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var numDocs = 500;

    function makebigstring(power) {
      var str = "a";
      while(power-- > 0) {
        str = str + str;
      }
      return str;
    }

    var designDoc = {
      _id:"_design/test",
      language: "javascript",
      views: {
        all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"},
        no_docs: {map: "function(doc) {}"},
        single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"},
        summate: {map:"function (doc) {emit(doc.integer, doc.integer)};",
                  reduce:"function (keys, values) { return sum(values); };"},
        summate2: {map:"function (doc) {emit(doc.integer, doc.integer)};",
                  reduce:"function (keys, values) { return sum(values); };"},
        huge_src_and_results: {map: "function(doc) { if (doc._id == \"1\") { emit(\"" + makebigstring(16) + "\", null) }}",
                  reduce:"function (keys, values) { return \"" + makebigstring(16) + "\"; };"}
      }
    }
    T(db.save(designDoc).ok);

    T(db.bulkSave(makeDocs(1, numDocs + 1)).ok);

    // test that the _all_docs view returns correctly with keys
    var results = db.allDocs({startkey:"_design%2F", endkey:"_design%2FZZZ"});
    T(results.rows.length == 1);

    for (var loop = 0; loop < 2; loop++) {
      var rows = db.view("test/all_docs_twice").rows;
      for (var i = 0; i < numDocs; i++) {
        T(rows[2*i].key == i+1);
        T(rows[(2*i)+1].key == i+1);
      }
      T(db.view("test/no_docs").total_rows == 0)
      T(db.view("test/single_doc").total_rows == 1)
      restartServer();
    };
    
    // test when language not specified, Javascript is implied
    var designDoc2 = {
      _id:"_design/test2",
      // language: "javascript", 
      views: {
        single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"}
      }
    };
    
    T(db.save(designDoc2).ok);
    T(db.view("test2/single_doc").total_rows == 1);

    var summate = function(N) {return (N+1)*N/2;};
    var result = db.view("test/summate");
    T(result.rows[0].value == summate(numDocs));

    result = db.view("test/summate", {startkey:4,endkey:4});
    T(result.rows[0].value == 4);

    result = db.view("test/summate", {startkey:4,endkey:5});
    T(result.rows[0].value == 9);

    result = db.view("test/summate", {startkey:4,endkey:6});
    T(result.rows[0].value == 15);

    // Verify that a shared index (view def is an exact copy of "summate")
    // does not confuse the reduce stage
    result = db.view("test/summate2", {startkey:4,endkey:6});
    T(result.rows[0].value == 15);

    for(var i=1; i<numDocs/2; i+=30) {
      result = db.view("test/summate", {startkey:i,endkey:numDocs-i});
      T(result.rows[0].value == summate(numDocs-i) - summate(i-1));
    }

    T(db.deleteDoc(designDoc).ok);
    T(db.open(designDoc._id) == null);
    T(db.view("test/no_docs") == null);

    restartServer();
    T(db.open(designDoc._id) == null);
    T(db.view("test/no_docs") == null);
  },

  view_collation: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    // NOTE, the values are already in their correct sort order. Consider this
    // a specification of collation of json types.

    var values = []

    // special values sort before all other types
    values.push(null)
    values.push(false)
    values.push(true)

    // then numbers
    values.push(1)
    values.push(2)
    values.push(3.0)
    values.push(4)

    // then text, case sensitive
    values.push("a")
    values.push("A")
    values.push("aa")
    values.push("b")
    values.push("B")
    values.push("ba")
    values.push("bb")

    // then arrays. compared element by element until different.
    // Longer arrays sort after their prefixes
    values.push(["a"])
    values.push(["b"])
    values.push(["b","c"])
    values.push(["b","c", "a"])
    values.push(["b","d"])
    values.push(["b","d", "e"])

    // then object, compares each key value in the list until different.
    // larger objects sort after their subset objects.
    values.push({a:1})
    values.push({a:2})
    values.push({b:1})
    values.push({b:2})
    values.push({b:2, a:1}) // Member order does matter for collation.
                            // CouchDB preserves member order
                            // but doesn't require that clients will.
                            // (this test might fail if used with a js engine
                            // that doesn't preserve order)
    values.push({b:2, c:2})

    for (var i=0; i<values.length; i++) {
      db.save({_id:(i).toString(), foo:values[i]});
    }

    var queryFun = function(doc) { emit(doc.foo, null); }
    var rows = db.query(queryFun).rows;
    for (i=0; i<values.length; i++) {
      T(equals(rows[i].key, values[i]))
    }

    // everything has collated correctly. Now to check the descending output
    rows = db.query(queryFun, null, {descending: true}).rows
    for (i=0; i<values.length; i++) {
      T(equals(rows[i].key, values[values.length - 1 -i]))
    }

    // now check the key query args
    for (i=1; i<values.length; i++) {
      var queryOptions = {key:values[i]}
      rows = db.query(queryFun, null, queryOptions).rows;
      T(rows.length == 1 && equals(rows[0].key, values[i]))
    }
  },

  view_conflicts: function(debug) {
    var dbA = new CouchDB("test_suite_db_a");
    dbA.deleteDb();
    dbA.createDb();
    var dbB = new CouchDB("test_suite_db_b");
    dbB.deleteDb();
    dbB.createDb();
    if (debug) debugger;

    var docA = {_id: "foo", bar: 42};
    T(dbA.save(docA).ok);
    CouchDB.replicate(dbA.name, dbB.name);

    var docB = dbB.open("foo");
    docB.bar = 43;
    dbB.save(docB);
    docA.bar = 41;
    dbA.save(docA);
    CouchDB.replicate(dbA.name, dbB.name);

    var doc = dbB.open("foo", {conflicts: true});
    T(doc._conflicts.length == 1);
    var conflictRev = doc._conflicts[0];
    if (doc.bar == 41) { // A won
      T(conflictRev == docB._rev);
    } else { // B won
      T(doc.bar == 43);
      T(conflictRev == docA._rev);
    }

    var results = dbB.query(function(doc) {
      if (doc._conflicts) {
        emit(doc._id, doc._conflicts);
      }
    });
    T(results.rows[0].value[0] == conflictRev);
  },

  view_errors: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var doc = {integer: 1, string: "1", array: [1, 2, 3]};
    T(db.save(doc).ok);

    // emitting a key value that is undefined should result in that row not
    // being included in the view results
    var results = db.query(function(doc) {
      emit(doc.undef, null);
    });
    T(results.total_rows == 0);

    // if a view function throws an exception, its results are not included in
    // the view index, but the view does not itself raise an error
    var results = db.query(function(doc) {
      doc.undef(); // throws an error
    });
    T(results.total_rows == 0);

    // if a view function includes an undefined value in the emitted key or
    // value, an error is logged and the result is not included in the view
    // index, and the view itself does not raise an error
    var results = db.query(function(doc) {
      emit([doc._id, doc.undef], null);
    });
    T(results.total_rows == 0);
  },

  view_include_docs: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var docs = makeDocs(0, 100);
    T(db.bulkSave(docs).ok);

    var designDoc = {
      _id:"_design/test",
      language: "javascript",
      views: {
        all_docs: {
          map: "function(doc) { emit(doc.integer, doc.string) }"
        },
        with_prev: {
          map: "function(doc){if(doc.prev) emit(doc._id,{'_rev':doc.prev}); else emit(doc._id,{'_rev':doc._rev});}"
        },
        summate: {
          map:"function (doc) {emit(doc.integer, doc.integer)};",
          reduce:"function (keys, values) { return sum(values); };"
        }
      }
    }
    T(db.save(designDoc).ok);

    var resp = db.view('test/all_docs', {include_docs: true, count: 2});
    T(resp.rows.length == 2);
    T(resp.rows[0].id == "0");
    T(resp.rows[0].doc._id == "0");
    T(resp.rows[1].id == "1");
    T(resp.rows[1].doc._id == "1");

    resp = db.view('test/all_docs', {include_docs: true}, [29, 74]);
    T(resp.rows.length == 2);
    T(resp.rows[0].doc._id == "29");
    T(resp.rows[1].doc.integer == 74);

    resp = db.allDocs({count: 2, skip: 1, include_docs: true});
    T(resp.rows.length == 2);
    T(resp.rows[0].doc.integer == 1);
    T(resp.rows[1].doc.integer == 10);

    resp = db.allDocs({include_docs: true}, ['not_a_doc']);
    T(resp.rows.length == 1);
    T(!resp.rows[0].doc);

    resp = db.allDocs({include_docs: true}, ["1", "foo"]);
    T(resp.rows.length == 2);
    T(resp.rows[0].doc.integer == 1);
    T(!resp.rows[1].doc);

    resp = db.allDocs({include_docs: true, count: 0});
    T(resp.rows.length == 0);

    // No reduce support
    try {
        resp = db.view('test/summate', {include_docs: true});
        alert(JSON.stringify(resp));
        T(0==1);
    } catch (e) {
        T(e.error == 'query_parse_error');
    }

    // Reduce support when reduce=false
    resp = db.view('test/summate', {reduce: false, include_docs: true});
    T(resp.rows.length == 100);

    // Check emitted _rev controls things
    resp = db.allDocs({include_docs: true}, ["0"]);
    var before = resp.rows[0].doc;
    var after = db.open("0");
    after.integer = 100
    after.prev = after._rev;
    db.save(after);
    after = db.open("0");
    T(after._rev != after.prev);
    T(after.integer == 100);

    // should emit the previous revision
    resp = db.view("test/with_prev", {include_docs: true}, ["0"]);
    T(resp.rows[0].doc._id == "0");
    T(resp.rows[0].doc._rev == before._rev);
    T(!resp.rows[0].doc.prev);
    T(resp.rows[0].doc.integer == 0);

    var xhr = CouchDB.request("POST", "/test_suite_db/_compact");
    T(xhr.status == 202)
    while (db.info().compact_running) {}

    resp = db.view("test/with_prev", {include_docs: true}, ["0", "23"]);
    T(resp.rows.length == 2);
    T(resp.rows[0].key == "0");
    T(resp.rows[0].id == "0");
    T(!resp.rows[0].doc);
    T(resp.rows[0].error == "missing");
    T(resp.rows[1].doc.integer == 23);
  },

  view_multi_key_all_docs: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var docs = makeDocs(0, 100);
    T(db.bulkSave(docs).ok);

    var keys = ["10","15","30","37","50"];
    var rows = db.allDocs({},keys).rows;
    T(rows.length == keys.length);
    for(var i=0; i<rows.length; i++)
      T(rows[i].id == keys[i]);

    rows = db.allDocs({count: 1}, keys).rows;
    T(rows.length == 1);
    T(rows[0].id == keys[0]);

    rows = db.allDocs({skip: 2}, keys).rows;
    T(rows.length == 3);
    for(var i=0; i<rows.length; i++)
        T(rows[i].id == keys[i+2]);

    rows = db.allDocs({descending: "true"}, keys).rows;
    T(rows.length == keys.length);
    for(var i=0; i<rows.length; i++)
        T(rows[i].id == keys[keys.length-i-1]);

    rows = db.allDocs({descending: "true", skip: 3, count:1}, keys).rows;
    T(rows.length == 1);
    T(rows[0].id == keys[1]);

    // Check we get invalid rows when the key doesn't exist
    rows = db.allDocs({}, [1, "i_dont_exist", "0"]).rows;
    T(rows.length == 3);
    T(rows[0].error == "not_found");
    T(!rows[0].id);
    T(rows[1].error == "not_found");
    T(!rows[1].id);
    T(rows[2].id == rows[2].key && rows[2].key == "0");
  },

  view_multi_key_design: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var docs = makeDocs(0, 100);
    T(db.bulkSave(docs).ok);

    var designDoc = {
      _id:"_design/test",
      language: "javascript",
      views: {
        all_docs: {
          map: "function(doc) { emit(doc.integer, doc.string) }"
        },
        multi_emit: {
          map: "function(doc) {for(var i = 0 ; i < 3 ; i++) { emit(i, doc.integer) ; } }"
        },
        summate: {
          map:"function (doc) {emit(doc.integer, doc.integer)};",
          reduce:"function (keys, values) { return sum(values); };"
        }
      }
    }
    T(db.save(designDoc).ok);

    // First, the goods:
    var keys = [10,15,30,37,50];
    var rows = db.view("test/all_docs",{},keys).rows;
    for(var i=0; i<rows.length; i++) {
      T(keys.indexOf(rows[i].key) != -1);
      T(rows[i].key == rows[i].value);
    }
    
    var reduce = db.view("test/summate",{group:true},keys).rows;
    T(reduce.length == keys.length);
    for(var i=0; i<reduce.length; i++) {
      T(keys.indexOf(reduce[i].key) != -1);
      T(rows[i].key == rows[i].value);
    }

    // Test that invalid parameter combinations get rejected
    var badargs = [{startkey:0}, {endkey:0}, {key: 0}, {group_level: 2}];
    for(var i in badargs)
    {
        try {
            db.view("test/all_docs",badargs[i],keys);
            T(0==1);
        } catch (e) {
            T(e.error == "query_parse_error");
        }
    }

    try {
        db.view("test/summate",{},keys);
        T(0==1);
    } catch (e) {
        T(e.error == "query_parse_error");
    }

    // Test that a map & reduce containing func support keys when reduce=false
    resp = db.view("test/summate", {reduce: false}, keys);
    T(resp.rows.length == 5);

    // Check that limiting by startkey_docid and endkey_docid get applied
    // as expected.
    var curr = db.view("test/multi_emit", {startkey_docid: 21, endkey_docid: 23}, [0, 2]).rows;
    var exp_key = [ 0,  0,  0,  2,  2,  2] ;
    var exp_val = [21, 22, 23, 21, 22, 23] ;
    T(curr.length == 6);
    for( var i = 0 ; i < 6 ; i++)
    {
        T(curr[i].key == exp_key[i]);
        T(curr[i].value == exp_val[i]);
    }

    // Check count works
    curr = db.view("test/all_docs", {count: 1}, keys).rows;
    T(curr.length == 1);
    T(curr[0].key == 10);

    // Check offset works
    curr = db.view("test/multi_emit", {skip: 1}, [0]).rows;
    T(curr.length == 99);
    T(curr[0].value == 1);

    // Check that dir works
    curr = db.view("test/multi_emit", {descending: "true"}, [1]).rows;
    T(curr.length == 100);
    T(curr[0].value == 99);
    T(curr[99].value == 0);

    // Check a couple combinations
    curr = db.view("test/multi_emit", {descending: "true", skip: 3, count: 2}, [2]).rows;
    T(curr.length, 2);
    T(curr[0].value == 96);
    T(curr[1].value == 95);

    curr = db.view("test/multi_emit", {skip: 2, count: 3, startkey_docid: "13"}, [0]).rows;
    T(curr.length == 3);
    T(curr[0].value == 15);
    T(curr[1].value == 16);
    T(curr[2].value == 17);

    curr = db.view("test/multi_emit",
            {skip: 1, count: 5, startkey_docid: "25", endkey_docid: "27"}, [1]).rows;
    T(curr.length == 2);
    T(curr[0].value == 26);
    T(curr[1].value == 27);

    curr = db.view("test/multi_emit",
            {skip: 1, count: 5, startkey_docid: "28", endkey_docid: "26", descending: "true"}, [1]).rows;
    T(curr.length == 2);
    T(curr[0].value == 27);
    T(curr[1].value == 26);
  },

  view_multi_key_temp: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var docs = makeDocs(0, 100);
    T(db.bulkSave(docs).ok);

    var queryFun = function(doc) { emit(doc.integer, doc.integer) };
    var reduceFun = function (keys, values) { return sum(values); };

    var keys = [10,15,30,37,50];
    var rows = db.query(queryFun, null, {}, keys).rows;
    for(var i=0; i<rows.length; i++) {
      T(keys.indexOf(rows[i].key) != -1);
      T(rows[i].key == rows[i].value);
    }
    
    var reduce = db.query(queryFun, reduceFun, {group:true}, keys).rows;
    for(var i=0; i<reduce.length; i++) {
      T(keys.indexOf(reduce[i].key) != -1);
      T(reduce[i].key == reduce[i].value);
    }
  },

  view_pagination: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var docs = makeDocs(0, 100);
    T(db.bulkSave(docs).ok);

    var queryFun = function(doc) { emit(doc.integer, null) };
    var i;

    // page through the view ascending
    for (i = 0; i < docs.length; i += 10) {
      var queryResults = db.query(queryFun, null, {
        startkey: i,
        startkey_docid: i,
        count: 10
      });
      T(queryResults.rows.length == 10)
      T(queryResults.total_rows == docs.length)
      T(queryResults.offset == i)
      var j;
      for (j = 0; j < 10;j++) {
        T(queryResults.rows[j].key == i + j);
      }
    }

    // page through the view descending
    for (i = docs.length - 1; i >= 0; i -= 10) {
      var queryResults = db.query(queryFun, null, {
        startkey: i,
        startkey_docid: i,
        descending: true,
        count: 10
      });
      T(queryResults.rows.length == 10)
      T(queryResults.total_rows == docs.length)
      T(queryResults.offset == docs.length - i - 1)
      var j;
      for (j = 0; j < 10; j++) {
        T(queryResults.rows[j].key == i - j);
      }
    }

    // ignore decending=false. CouchDB should just ignore that.
    for (i = 0; i < docs.length; i += 10) {
      var queryResults = db.query(queryFun, null, {
        startkey: i,
        startkey_docid: i,
        descending: false,
        count: 10
      });
      T(queryResults.rows.length == 10)
      T(queryResults.total_rows == docs.length)
      T(queryResults.offset == i)
      var j;
      for (j = 0; j < 10;j++) {
        T(queryResults.rows[j].key == i + j);
      }
    }
    
    // test endkey_docid
    var queryResults = db.query(function(doc) { emit(null, null);}, null, {
      startkey: null,
      startkey_docid: 1,
      endkey: null,
      endkey_docid: 40
    });
    
    T(queryResults.rows.length == 35)
    T(queryResults.total_rows == docs.length)
    T(queryResults.offset == 1)
    T(queryResults.rows[0].id == "1");
    T(queryResults.rows[1].id == "10");
    T(queryResults.rows[2].id == "11");
    T(queryResults.rows[3].id == "12");
    T(queryResults.rows[4].id == "13");
    T(queryResults.rows[5].id == "14");
    T(queryResults.rows[6].id == "15");
    T(queryResults.rows[7].id == "16");
    T(queryResults.rows[8].id == "17");
    T(queryResults.rows[9].id == "18");
    T(queryResults.rows[10].id == "19");
    T(queryResults.rows[11].id == "2");
    T(queryResults.rows[12].id == "20");
    T(queryResults.rows[13].id == "21");
    T(queryResults.rows[14].id == "22");
    T(queryResults.rows[15].id == "23");
    T(queryResults.rows[16].id == "24");
    T(queryResults.rows[17].id == "25");
    T(queryResults.rows[18].id == "26");
    T(queryResults.rows[19].id == "27");
    T(queryResults.rows[20].id == "28");
    T(queryResults.rows[21].id == "29");
    T(queryResults.rows[22].id == "3");
    T(queryResults.rows[23].id == "30");
    T(queryResults.rows[24].id == "31");
    T(queryResults.rows[25].id == "32");
    T(queryResults.rows[26].id == "33");
    T(queryResults.rows[27].id == "34");
    T(queryResults.rows[28].id == "35");
    T(queryResults.rows[29].id == "36");
    T(queryResults.rows[30].id == "37");
    T(queryResults.rows[31].id == "38");
    T(queryResults.rows[32].id == "39");
    T(queryResults.rows[33].id == "4");
    T(queryResults.rows[34].id == "40");

  },

  view_sandboxing: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var doc = {integer: 1, string: "1", array: [1, 2, 3]};
    T(db.save(doc).ok);
/*
    // make sure that attempting to change the document throws an error
    var results = db.query(function(doc) {
      doc.integer = 2;
      emit(null, doc);
    });
    T(results.total_rows == 0);

    var results = db.query(function(doc) {
      doc.array[0] = 0;
      emit(null, doc);
    });
    T(results.total_rows == 0);
*/
    // make sure that a view cannot invoke interpreter internals such as the
    // garbage collector
    var results = db.query(function(doc) {
      gc();
      emit(null, doc);
    });
    T(results.total_rows == 0);

    // make sure that a view cannot access the map_funs array defined used by
    // the view server
    var results = db.query(function(doc) { map_funs.push(1); emit(null, doc) });
    T(results.total_rows == 0);

    // make sure that a view cannot access the map_results array defined used by
    // the view server
    var results = db.query(function(doc) { map_results.push(1); emit(null, doc) });
    T(results.total_rows == 0);
  },

  view_xml: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    db.save({content: "<doc><title id='xml'>Testing XML</title></doc>"});
    db.save({content: "<doc><title id='e4x'>Testing E4X</title></doc>"});

    var results = db.query(
      "function(doc) {\n" +
      "  var xml = new XML(doc.content);\n" +
      "  emit(xml.title.text(), null);\n" +
      "}");
    T(results.total_rows == 2);
    T(results.rows[0].key == "Testing E4X");
    T(results.rows[1].key == "Testing XML");

    var results = db.query(
      "function(doc) {\n" +
      "  var xml = new XML(doc.content);\n" +
      "  emit(xml.title.@id, null);\n" +
      "}");
    T(results.total_rows == 2);
    T(results.rows[0].key == "e4x");
    T(results.rows[1].key == "xml");
  },

  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");
    var dbB = new CouchDB("test_suite_db_b");
    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 docs = makeDocs(0, numDocs);
      T(dbA.bulkSave(docs).ok);

      T(CouchDB.replicate(A, B).ok);

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

      // check documents with a '/' in the ID
      // need to re-encode the slash when replicating from a remote source
      dbA.save({ _id:"abc/def", val:"one" });
      
      T(CouchDB.replicate(A, B).ok);
      T(CouchDB.replicate(B, A).ok);
      
      docA = dbA.open("abc/def");
      docB = dbB.open("abc/def");
      T(docA._rev == docB._rev);
      
      // now check binary attachments
      var binDoc = {
        _id:"bin_doc",
        _attachments:{
          "foo.txt": {
            "type":"base64",
            "data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
          }
        }
      }

      dbA.save(binDoc);

      T(CouchDB.replicate(A, B).ok);
      T(CouchDB.replicate(B, A).ok);

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

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

      dbA.save({_id:"foo1",value:"a"});

      T(CouchDB.replicate(A, B).ok);
      T(CouchDB.replicate(B, A).ok);

      docA = dbA.open("foo1");
      docB = dbB.open("foo1");
      T(docA._rev == docB._rev);

      dbA.deleteDoc(docA);

      T(CouchDB.replicate(A, B).ok);
      T(CouchDB.replicate(B, A).ok);

      T(dbA.open("foo1") == null);
      T(dbB.open("foo1") == null);

      dbA.save({_id:"foo",value:"a"});
      dbB.save({_id:"foo",value:"b"});

      T(CouchDB.replicate(A, B).ok);
      T(CouchDB.replicate(B, A).ok);

      // open documents and include the conflict meta data
      docA = dbA.open("foo", {conflicts: true});
      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]});

      // replicate the change
      T(CouchDB.replicate(A, B).ok);

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

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

  etags_head: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;

    var xhr;

    // create a new doc
    xhr = CouchDB.request("PUT", "/test_suite_db/1", {
      body: "{}"
    });
    T(xhr.status == 201);

    // extract the ETag header values
    var etag = xhr.getResponseHeader("etag")

    // get the doc and verify the headers match
    xhr = CouchDB.request("GET", "/test_suite_db/1");
    T(etag == xhr.getResponseHeader("etag"));

    // 'head' the doc and verify the headers match
    xhr = CouchDB.request("HEAD", "/test_suite_db/1", {
      headers: {"if-none-match": "s"}
    });
    T(etag == xhr.getResponseHeader("etag"));

    // replace a doc
    xhr = CouchDB.request("PUT", "/test_suite_db/1", {
      body: "{}",
      headers: {"if-match": etag}
    });
    T(xhr.status == 201);

    // extract the new ETag value
    var etagOld= etag;
    etag = xhr.getResponseHeader("etag")

    // fail to replace a doc
    xhr = CouchDB.request("PUT", "/test_suite_db/1", {
      body: "{}"
    });
    T(xhr.status == 412)

    // verify get w/Etag
    xhr = CouchDB.request("GET", "/test_suite_db/1", {
      headers: {"if-none-match": etagOld}
    });
    T(xhr.status == 200);
    xhr = CouchDB.request("GET", "/test_suite_db/1", {
      headers: {"if-none-match": etag}
    });
    T(xhr.status == 304);

    // fail to delete a doc
    xhr = CouchDB.request("DELETE", "/test_suite_db/1", {
      headers: {"if-match": etagOld}
    });
    T(xhr.status == 412);

    //now do it for real
    xhr = CouchDB.request("DELETE", "/test_suite_db/1", {
      headers: {"if-match": etag}
    });
    T(xhr.status == 200)
  },

  compact: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;
    var docs = makeDocs(0, 10);
    var saveResult = db.bulkSave(docs);
    T(saveResult.ok);

    var binAttDoc = {
      _id: "bin_doc",
      _attachments:{
        "foo.txt": {
          content_type:"text/plain",
          data: "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
        }
      }
    }

    T(db.save(binAttDoc).ok);

    var originalsize = db.info().disk_size;

    for(var i in docs) {
        db.deleteDoc(docs[i]);
    }
    db.setAdmins(["Foo bar"]);
    var deletesize = db.info().disk_size;
    T(deletesize > originalsize);

    var xhr = CouchDB.request("POST", "/test_suite_db/_compact");
    T(xhr.status == 202);
    // compaction isn't instantaneous, loop until done
    while (db.info().compact_running) {};
    
    restartServer();
    var xhr = CouchDB.request("GET", "/test_suite_db/bin_doc/foo.txt");
    T(xhr.responseText == "This is a base64 encoded text")
    T(xhr.getResponseHeader("Content-Type") == "text/plain")
    T(db.info().doc_count == 1);
    T(db.info().disk_size < deletesize);
    
  },
  
  purge: function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;
    
    /*
     purge is not to be confused with a document deletion.  It removes the
     document and all edit history from the local instance of the database.
    */

    var numDocs = 10;

    var designDoc = {
      _id:"_design/test",
      language: "javascript",
      views: {
        all_docs_twice: {map: "function(doc) { emit(doc.integer, null); emit(doc.integer, null) }"},
        single_doc: {map: "function(doc) { if (doc._id == \"1\") { emit(1, null) }}"}
      }
    }
    
    T(db.save(designDoc).ok);

    T(db.bulkSave(makeDocs(1, numDocs + 1)).ok);

    // go ahead and validate the views before purging
    var rows = db.view("test/all_docs_twice").rows;
    for (var i = 0; i < numDocs; i++) {
      T(rows[2*i].key == i+1);
      T(rows[(2*i)+1].key == i+1);
    }
    T(db.view("test/single_doc").total_rows == 1);
    
    var doc1 = db.open("1");
    var doc2 = db.open("2");
    
    // purge the documents
    var xhr = CouchDB.request("POST", "/test_suite_db/_purge", {
      body: JSON.stringify({"1":[doc1._rev], "2":[doc2._rev]}),
    });
    T(xhr.status == 200);
    
    var result = JSON.parse(xhr.responseText);
    T(result.purged["1"][0] == doc1._rev);
    T(result.purged["2"][0] == doc2._rev);
    
    T(db.open("1") == null);
    T(db.open("2") == null);
    
    var rows = db.view("test/all_docs_twice").rows;
    for (var i = 2; i < numDocs; i++) {
      T(rows[2*(i-2)].key == i+1);
      T(rows[(2*(i-2))+1].key == i+1);
    }
    T(db.view("test/single_doc").total_rows == 0);
    
    // purge documents twice in a row without loading views
    // (causes full view rebuilds)
    
    var doc3 = db.open("3");
    var doc4 = db.open("4");
    
    xhr = CouchDB.request("POST", "/test_suite_db/_purge", {
      body: JSON.stringify({"3":[doc3._rev]}),
    });
    
    T(xhr.status == 200);
    
    xhr = CouchDB.request("POST", "/test_suite_db/_purge", {
      body: JSON.stringify({"4":[doc4._rev]}),
    });
    
    T(xhr.status == 200);
    
    var rows = db.view("test/all_docs_twice").rows;
    for (var i = 4; i < numDocs; i++) {
      T(rows[2*(i-4)].key == i+1);
      T(rows[(2*(i-4))+1].key == i+1);
    }
    T(db.view("test/single_doc").total_rows == 0);
  },
  
  config : function(debug) {
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;
    
    // test that /_config returns all the settings
    var xhr = CouchDB.request("GET", "/_config");
    var config = JSON.parse(xhr.responseText);
    var port = CouchDB.host.split(':').pop()
    T(config.couchdb.database_dir);
    T(config.httpd.port == port);
    T(config.daemons.httpd);
    T(config.httpd_global_handlers._config);
    T(config.log.level);
    T(config.query_servers.javascript);
    
    // test that settings can be altered
    xhr = CouchDB.request("PUT", "/_config/test/foo",{
      body : JSON.stringify("bar")
    });
    T(xhr.status == 200);
    xhr = CouchDB.request("GET", "/_config/test");
    config = JSON.parse(xhr.responseText);
    T(config.foo == "bar");

    // you can get a single key
    xhr = CouchDB.request("GET", "/_config/test/foo");
    T(xhr.responseText == '"bar"');
  },
  
  security_validation : function(debug) {
    // This tests couchdb's security and validation features. This does
    // not test authentication, except to use test authentication code made
    // specifically for this testing. It is a WWWW-Authenticate scheme named
    // X-Couch-Test-Auth, and the user names and passwords are hard coded
    // on the server-side.
    // 
    // We could have used Basic authentication, however the XMLHttpRequest
    // implementation for Firefox and Safari, and probably other browsers are
    // broken (Firefox always prompts the user on 401 failures, Safari gives
    // odd security errors when using different name/passwords, perhaps due
    // to cross site scripting prevention).  These problems essentially make Basic
    // authentication testing in the browser impossible. But while hard to
    // test automated in the browser, Basic auth may still useful for real
    // world use where these bugs/behaviors don't matter.
    //
    // So for testing purposes we are using this custom X-Couch-Test-Auth.
    // It's identical to Basic auth, except it doesn't even base64 encode
    // the "username:password" string, it's sent completely plain text.
    // Firefox and Safari both deal with this correctly (which is to say
    // they correctly do nothing special).
    
    
    var db = new CouchDB("test_suite_db");
    db.deleteDb();
    db.createDb();
    if (debug) debugger;
    
    run_on_modified_server(
      [{section: "httpd",
        key: "authentication_handler",
        value: "{couch_httpd, special_test_authentication_handler}"},
      {section:"httpd",
        key: "WWW-Authenticate",
        value:  "X-Couch-Test-Auth"}],
        
      function () {
    
        // try saving document usin the wrong credentials
        var wrongPasswordDb = new CouchDB("test_suite_db",
          {"WWW-Authenticate": "X-Couch-Test-Auth Damien Katz:foo"}
        );
    
        try {
          wrongPasswordDb.save({foo:1,author:"Damien Katz"});
          T(false && "Can't get here. Should have thrown an error 1");
        } catch (e) {
          T(e.error == "unauthorized");
          T(wrongPasswordDb.last_req.status == 401);
        }
        
        
        // Create the design doc that will run custom validation code
        var designDoc = {
          _id:"_design/test",
          language: "javascript",
          validate_doc_update: "(" + (function (newDoc, oldDoc, userCtx) {
            log("newDoc: " + newDoc.toSource());
            if (oldDoc) {
              log("oldDoc: " + oldDoc.toSource());
            }
            // docs should have an author field.
            if (!newDoc.author) {
              throw {forbidden:
                  "Documents must have an author field"};
            }
            if (oldDoc && oldDoc.author != userCtx.name) {
                throw {unauthorized:
                    "You are not the author of this document. You jerk."};
            }
          }).toString() + ")"
        }

        // Save a document normally
        var userDb = new CouchDB("test_suite_db",
          {"WWW-Authenticate": "X-Couch-Test-Auth Damien Katz:pecan pie"}
        );
        
        T(userDb.save({_id:"testdoc", foo:1, author:"Damien Katz"}).ok);
        
        // Attempt to save the design as a non-admin
        try {
          userDb.save(designDoc);
          T(false && "Can't get here. Should have thrown an error on design doc");
        } catch (e) {
          T(e.error == "unauthorized");
          T(userDb.last_req.status == 401);
        }
        
        // add user as admin
        db.setAdmins(["Damien Katz"]);
        
        T(userDb.save(designDoc).ok);
    
        // update the document
        var doc = userDb.open("testdoc");
        doc.foo=2;
        T(userDb.save(doc).ok);
        
        // Save a document that's missing an author field.
        try {
          userDb.save({foo:1});
          T(false && "Can't get here. Should have thrown an error 2");
        } catch (e) {
          T(e.error == "forbidden");
          T(userDb.last_req.status == 403);
        }
    
        // Now attempt to update the document as a different user, Jan 
        var user2Db = new CouchDB("test_suite_db",
          {"WWW-Authenticate": "X-Couch-Test-Auth Jan Lehnardt:apple"}
        );
    
        var doc = user2Db.open("testdoc");
        doc.foo=3;
        try {
          user2Db.save(doc);
          T(false && "Can't get here. Should have thrown an error 3");
        } catch (e) {
          T(e.error == "unauthorized");
          T(user2Db.last_req.status == 401);
        }
        
        // Now have Damien change the author to Jan
        doc = userDb.open("testdoc");
        doc.author="Jan Lehnardt";
        T(userDb.save(doc).ok);
        
        // Now update the document as Jan
        doc = user2Db.open("testdoc");
        doc.foo = 3;
        T(user2Db.save(doc).ok);
      });
  }
};

function makeDocs(start, end, templateDoc) {
  var templateDocSrc = templateDoc ? templateDoc.toSource() : "{}"
  if (end === undefined) {
    end = start;
    start = 0;
  }
  var docs = []
  for (var i = start; i < end; i++) {
    var newDoc = eval("(" + templateDocSrc + ")");
    newDoc._id = (i).toString();
    newDoc.integer = i
    newDoc.string = (i).toString();
    docs.push(newDoc)
  }
  return docs;
}

function run_on_modified_server(settings, fun) {
  try {
    // set the settings
    for(var i=0; i < settings.length; i++) {
      var s = settings[i];
      var xhr = CouchDB.request("PUT", "/_config/" + s.section + "/" + s.key, {
        body: JSON.stringify(s.value),
        headers: {"X-Couch-Persist": "false"}
      });
      CouchDB.maybeThrowError(xhr);
      s.oldValue = xhr.responseText;
    }
    // run the thing
    fun();
  } finally {
    // unset the settings
    for(var j=0; j < i; j++) {
      var s = settings[j];
      CouchDB.request("PUT", "/_config/" + s.section + "/" + s.key, {
        body: s.oldValue,
        headers: {"X-Couch-Persist": "false"}
      });
    }
  }
}

function restartServer() {
  CouchDB.request("POST", "/_restart");
}