(function () { var thisfun = function(loader) {
    var JIO = loader.JIO;

// localStorage cleanup
var k;
for (k in localStorage) {
    if (/^jio\//.test(k)) {
        localStorage.removeItem(k);
    }
}
delete k;

//// Tools
var empty_fun = function (){},
contains = function (array,content) {
    var i;
    if (typeof array !== 'object') {
        return undefined;
    }
    for (i = 0; i < array.length || 0; i+= 1) {
        if (array[i] === content) {
            return true;
        }
    }
    return false;
},
clone = function (obj) {
  var tmp = JSON.stringify(obj);
  if (tmp !== undefined) {
    return JSON.parse(tmp);
  }
  return tmp;
},
// generates a revision hash from document metadata, revision history
// and the deleted_flag
generateRevisionHash = function (doc, revisions, deleted_flag) {
    var string = JSON.stringify(doc) + JSON.stringify(revisions) +
        JSON.stringify(deleted_flag? true: false);
    return hex_sha256(string);
},
// localStorage wrapper
localstorage = {
    clear: function () {
        return localStorage.clear();
    },
    getItem: function (item) {
        var value = localStorage.getItem(item);
        return value === null? null: JSON.parse(value);
    },
    setItem: function (item,value) {
        return localStorage.setItem(item,JSON.stringify (value));
    },
    removeItem: function (item) {
        return localStorage.removeItem(item);
    }
},
cleanUpLocalStorage = function(){
    var k, storageObject = localstorage.getAll();
    for (k in storageObject) {
        var splitk = k.split('/');
        if ( splitk[0] === 'jio' ) {
            localstorage.removeItem(k);
        }
    }
    var d = document.createElement ('div');
    d.setAttribute('id','log');
    document.querySelector ('body').appendChild(d);
    // remove everything
    localStorage.clear();
},
base_tick = 30000,
basicTestFunctionGenerator = function(o,res,value,message) {

    return function(err,val) {
        var jobstatus = (err?'fail':'done');

        switch (res) {
        case 'status':
            err = err || {}; val = err.status;
            break;
        case 'jobstatus':
            val = jobstatus;
            break;
        case 'value':
            val = err || val;
            break;
        default:
            ok(false, "Unknown case " + res);
        }
        deepEqual (val,value,message);
    };
},

/**
 * Prepare a specific test for jio and create a spy.
 * It creates a function [function_name] in [obj] which can be use as a
 * jio callback. To prepare the test, we need to know what kind of return
 * value you want -> [result_type]:
 * - "status": [value] is compared with err.status, the error code
 * - "jobstatus": [value] check if the request is "fail" or "done"
 * - "value": [value] is compared to the response
 * @method basicSpyFunction
 * @param  {object} obj The object to work with
 * @param  {string} result_type The result type
 * @param  {object} value The value to be compared
 * @param  {string} message The test message
 * @param  {string} function_name The callback name
 */
basicSpyFunction = function(obj, result_type, value, message, function_name) {
    function_name = function_name || 'f';
    obj[function_name] =
        basicTestFunctionGenerator(obj, result_type, value, message);
    obj.t.spy(obj, function_name);
},

/**
 * Advances in time and execute the test previously prepared.
 * The default function to test is "f" in [obj].
 * @method basicTickFunction
 * @param  {object} obj The object to work with
 * @param  {number} tick The time to advance in ms (optional)
 * @param  {function_name} function_name The callback to test (optional)
 */
basicTickFunction = function (obj) {
    var tick, fun, i = 1;
    tick = 10000;
    fun = "f";

    if (typeof arguments[i] === 'number') {
        tick = arguments[i]; i++;
    }
    if (typeof arguments[i] === 'string') {
        fun = arguments[i]; i++;
    }
    obj.clock.tick(tick);
    if (!obj[fun].calledOnce) {
        if (obj[fun].called) {
            ok(false, 'too much results (obj.' + fun +')');
        } else {
            ok(false, 'no response (obj.' + fun +')');
        }
    }
},
getXML = function (url) {
  var xml = $.ajax({url:url, async:false});
  return xml.responseText;
},
objectifyDocumentArray = function (array) {
    var obj = {}, k;
    for (k = 0; k < array.length; k += 1) {
        obj[array[k]._id] = array[k];
    }
    return obj;
},
getLastJob = function (id) {
    return (localstorage.getItem("jio/job_array/"+id) || [undefined]).pop();
},
generateTools = function (sinon) {
    var o = {};

    o.t = sinon;
    o.server = o.t.sandbox.useFakeServer();
    o.clock = o.t.sandbox.useFakeTimers();
    o.clock.tick(base_tick);
    o.spy = basicSpyFunction;
    o.tick = basicTickFunction;

    // test methods
    o.testLastJobLabel = function (label, mess) {
        var lastjob = getLastJob(o.jio.getId());
        if (lastjob) {
            deepEqual(lastjob.command.label, label, mess);
        } else {
            deepEqual("No job on the queue", "Job with label: "+label, mess);
        }
    };
    o.testLastJobId = function (id, mess) {
        var lastjob = getLastJob(o.jio.getId());
        if (lastjob) {
            deepEqual(lastjob.id, id, mess);
        } else {
            deepEqual("No job on the queue", "Job with id: "+id, mess);
        }
    };
    o.testLastJobWaitForTime = function (mess) {
        var lastjob = getLastJob(o.jio.getId());
        if (lastjob) {
            ok(lastjob.status.waitfortime > 0, mess);
        } else {
            deepEqual("No job on the queue", "Job waiting for time", mess);
        }
    };
    o.testLastJobWaitForJob = function (job_id_array, mess) {
        var lastjob = getLastJob(o.jio.getId());
        if (lastjob) {
            deepEqual(lastjob.status.waitforjob, job_id_array, mess);
        } else {
            deepEqual(
                "No job on the queue",
                "Job waiting for: " + JSON.stringify (job_id_array),
                mess
            );
        }
    };
    // wait method
    o.waitUntilAJobExists = function (timeout) {
        var cpt = 0
        while (true) {
            if (getLastJob(o.jio.getId()) !== undefined) {
                break;
            }
            if (timeout >= cpt) {
                ok(false, "No job were added to the queue");
                break;
            }
            o.clock.tick(25);
            cpt += 25;
        }
    };
    o.waitUntilLastJobIs = function (state) {
        while (true) {
            if (getLastJob(o.jio.getId()) === undefined) {
                ok(false, "No job have state: " + state);
                break;
            }
            if (getLastJob(o.jio.getId()).status.label === state) {
                break;
            }
            o.clock.tick(25);
        }
    };
    o.addFakeServerResponse = function (method, path, status, response) {
      var url = new RegExp('https:\\/\\/ca-davstorage:8080\\/' + path +
                      '(\\?.*|$)');
      o.server.respondWith(method, url,
        [status, { "Content-Type": 'application/xml' }, response]
      );
    }

    return o;
},
//// end tools

//// test function
isUuid = function (uuid) {
    var x = "[0-9a-fA-F]{4}";
    if (typeof uuid !== "string" ) {
        return false;
    }
    return uuid.match("^"+x+x+"-"+x+"-"+x+"-"+x+"-"+x+x+x+"$") === null?
        false: true;
};
//// QUnit Tests ////
module ('Jio Global tests');

test ( "Jio simple methods", function () {
    // Test Jio simple methods
    // It checks if we can create several instance of jio at the same
    // time. Checks if they don't overlap informations, if they are
    // started and stopped correctly and if they are ready when they
    // have to be ready.

    var o = generateTools(this);

    o.jio = JIO.newJio();
    ok ( o.jio, 'a new jio -> 1');

    o.jio2 = JIO.newJio();
    ok ( o.jio2, 'another new jio -> 2');

    JIO.addStorageType('qunit', empty_fun);

    ok ( o.jio2.getId() !== o.jio.getId(), '1 and 2 must be different');

    o.jio.stop();
    o.jio2.stop();

});

// test ( 'Jio Publish/Sububscribe/Unsubscribe methods', function () {
//     // Test the Publisher, Subscriber of a single jio.
//     // It is just testing if these function are working correctly.
//     // The test publishes an event, waits a little, and check if the
//     // event has been received by the callback of the previous
//     // subscribe. Then, the test unsubscribe the callback function from
//     // the event, and publish the same event. If it receives the event,
//     // the unsubscribe method is not working correctly.

//     var o = {};
//     o.jio = JIO.newJio();

//     var spy1 = this.spy();

//     // Subscribe the pubsub_test event.
//     o.callback = o.jio.subscribe('pubsub_test',spy1);
//     // And publish the event.
//     o.jio.publish('pubsub_test');
//     ok (spy1.calledOnce, 'subscribing & publishing, event called once');

//     o.jio.unsubscribe('pubsub_test',spy1);
//     o.jio.publish('pubsub_test');
//     ok (spy1.calledOnce, 'unsubscribing, same event not called twice');

//     o.jio.stop();
// });

module ( "Jio Dummy Storages" );

test ("All requests ok", function () {
    // Tests the request methods and the response with dummy storages

    var o = generateTools(this);

    // All Ok Dummy Storage
    o.jio = JIO.newJio({"type": "dummyallok"});

    // post empty document, some storage can create there own id (like couchdb
    // generates uuid). In this case, the dummy storage write an undefined id.
    o.spy(o, "value", {"ok": true, "id": undefined},
          "Post document with empty id");
    o.jio.post({}, o.f);
    o.tick(o);

    // post non empty document
    o.spy(o, "value", {"ok": true, "id": "file"}, "Post non empty document");
    o.jio.post({"_id": "file", "title": "myFile"}, o.f);
    o.tick(o);

    // put without id
    // error 20 -> document id required
    o.spy(o, "status", 20, "Put document with empty id");
    o.jio.put({}, o.f);
    o.tick(o);

    // put non empty document
    o.spy(o, "value", {"ok": true, "id": "file"}, "Put non empty document");
    o.jio.put({"_id": "file", "title": "myFile"}, o.f);
    o.tick(o);

    // put an attachment without attachment id
    // error 22 -> attachment id required
    o.spy(o, "status", 22,
          "Put attachment without id");
    o.jio.putAttachment({
        "id": "file",
        "data": "0123456789",
        "mimetype": "text/plain"
    }, o.f);
    o.tick(o);

    // put an attachment
    o.spy(o, "value", {"ok": true, "id": "file/attmt"},
          "Put attachment");
    o.jio.putAttachment({
        "id": "file/attmt",
        "data": "0123456789",
        "mimetype": "text/plain"
    }, o.f);
    o.tick(o);

    // get document
    o.spy(o, "value", {"_id": "file", "title": "get_title"}, "Get document");
    o.jio.get("file", o.f);
    o.tick(o);

    // get attachment
    o.spy(o, "value", "0123456789", "Get attachment");
    o.jio.get("file/attmt", o.f);
    o.tick(o);

    // remove document
    o.spy(o, "value", {"ok": true, "id": "file"}, "Remove document");
    o.jio.remove({"_id": "file"}, o.f);
    o.tick(o);

    // remove attachment
    o.spy(o, "value", {"ok": true, "id": "file/attmt"}, "Remove attachment");
    o.jio.remove({"_id": "file/attmt"}, o.f);
    o.tick(o);

    // alldocs
    // error 405 -> Method not allowed
    o.spy(o, "status", 405, "AllDocs fail");
    o.jio.allDocs(o.f);
    o.tick(o);

    o.jio.stop();
});

test ("All requests fail", function () {
    // Tests the request methods and the err object with dummy storages

    var o = generateTools(this);

    // All Ok Dummy Storage
    o.jio = JIO.newJio({"type": "dummyallfail"});

    // post empty document
    // error 0 -> unknown
    o.spy(o, "status", 0, "Post document with empty id");
    o.jio.post({}, o.f);
    o.tick(o);

    // test if the job still exists
    if (getLastJob(o.jio.getId()) !== undefined) {
        ok(false, "The job is not removed from the job queue");
    }

    // post non empty document
    o.spy(o, "status", 0, "Post non empty document");
    o.jio.post({"_id": "file", "title": "myFile"}, o.f);
    o.tick(o);

    // put without id
    // error 20 -> document id required
    o.spy(o, "status", 20, "Put document with empty id");
    o.jio.put({}, o.f);
    o.tick(o);

    // put non empty document
    o.spy(o, "status", 0, "Put non empty document");
    o.jio.put({"_id": "file", "title": "myFile"}, o.f);
    o.tick(o);

    // put an attachment without attachment id
    // error 22 -> attachment id required
    o.spy(o, "status", 22,
          "Put attachment without id");
    o.jio.putAttachment({
        "id": "file",
        "data": "0123456789",
        "mimetype": "text/plain"
    }, o.f);
    o.tick(o);

    // put an attachment
    o.spy(o, "status", 0,
          "Put attachment");
    o.jio.putAttachment({
        "id": "file/attmt",
        "data": "0123456789",
        "mimetype": "text/plain"
    }, o.f);
    o.tick(o);

    // get document
    o.spy(o, "status", 0, "Get document");
    o.jio.get("file", o.f);
    o.tick(o);

    // get attachment
    o.spy(o, "status", 0, "Get attachment");
    o.jio.get("file/attmt", o.f);
    o.tick(o);

    // remove document
    o.spy(o, "status", 0, "Remove document");
    o.jio.remove({"_id": "file"}, o.f);
    o.tick(o);

    // remove attachment
    o.spy(o, "status", 0, "Remove attachment");
    o.jio.remove({"_id": "file/attmt"}, o.f);
    o.tick(o);

    // alldocs
    // error 405 -> Method not allowed
    o.spy(o, "status", 405, "AllDocs fail");
    o.jio.allDocs(o.f);
    o.tick(o);

    o.jio.stop();
});

test ("All document not found", function () {
    // Tests the request methods without document

    var o = generateTools(this);

    // All Ok Dummy Storage
    o.jio = JIO.newJio({"type": "dummyallnotfound"});

    // post document
    o.spy(o, "value", {"ok": true, "id": "file"}, "Post document");
    o.jio.post({"_id": "file", "title": "myFile"}, o.f);
    o.tick(o);

    // put document
    o.spy(o, "value", {"ok": true, "id": "file"}, "Put document");
    o.jio.put({"_id": "file", "title": "myFile"}, o.f);
    o.tick(o);

    // put an attachment without attachment id
    // error 22 -> attachment id required
    o.spy(o, "status", 22,
          "Put attachment without id");
    o.jio.putAttachment({
        "id": "file",
        "data": "0123456789",
        "mimetype": "text/plain"
    }, o.f);
    o.tick(o);

    // put an attachment
    o.spy(o, "value", {"ok": true, "id": "file/attmt"},
          "Put attachment");
    o.jio.putAttachment({
        "id": "file/attmt",
        "data": "0123456789",
        "mimetype": "text/plain"
    }, o.f);
    o.tick(o);

    // get document
    o.spy(o, "status", 404, "Get document");
    o.jio.get("file", o.f);
    o.tick(o);

    // get attachment
    o.spy(o, "status", 404, "Get attachment");
    o.jio.get("file/attmt", o.f);
    o.tick(o);

    // remove document
    o.spy(o, "status", 404, "Remove document");
    o.jio.remove({"_id": "file"}, o.f);
    o.tick(o);

    // remove attachment
    o.spy(o, "status", 404, "Remove attachment");
    o.jio.remove({"_id": "file/attmt"}, o.f);
    o.tick(o);

    o.jio.stop();
});

test ("All document found", function () {
    // Tests the request methods with document

    var o = generateTools(this);

    // All Ok Dummy Storage
    o.jio = JIO.newJio({"type": "dummyallfound"});

    // post non empty document
    o.spy(o, "status", 409, "Post document");
    o.jio.post({"_id": "file", "title": "myFile"}, o.f);
    o.tick(o);

    // put non empty document
    o.spy(o, "value", {"ok": true, "id": "file"}, "Put non empty document");
    o.jio.put({"_id": "file", "title": "myFile"}, o.f);
    o.tick(o);

    // put an attachment without attachment id
    // error 22 -> attachment id required
    o.spy(o, "status", 22,
          "Put attachment without id");
    o.jio.putAttachment({
        "id": "file",
        "data": "0123456789",
        "mimetype": "text/plain"
    }, o.f);
    o.tick(o);

    // put an attachment
    o.spy(o, "value", {"ok": true, "id": "file/attmt"},
          "Put attachment");
    o.jio.putAttachment({
        "id": "file/attmt",
        "data": "0123456789",
        "mimetype": "text/plain"
    }, o.f);
    o.tick(o);

    // get document
    o.spy(o, "value", {"_id": "file", "title": "get_title"}, "Get document");
    o.jio.get("file", o.f);
    o.tick(o);

    // get attachment
    o.spy(o, "value", "0123456789", "Get attachment");
    o.jio.get("file/attmt", o.f);
    o.tick(o);

    // remove document
    o.spy(o, "value", {"ok": true, "id": "file"}, "Remove document");
    o.jio.remove({"_id": "file"}, o.f);
    o.tick(o);

    // remove attachment
    o.spy(o, "value", {"ok": true, "id": "file/attmt"}, "Remove attachment");
    o.jio.remove({"_id": "file/attmt"}, o.f);
    o.tick(o);

    o.jio.stop();
});

module ( "Jio Job Managing" );

test ("Several Jobs at the same time", function () {

    var o = generateTools(this);

    o.jio = JIO.newJio({"type":"dummyallok"});
    o.spy(o, "value", {"ok": true, "id": "file"}, "job1", "f");
    o.spy(o, "value", {"ok": true, "id": "file2"}, "job2", "f2");
    o.spy(o, "value", {"ok": true, "id": "file3"}, "job3", "f3");
    o.jio.put({"_id": "file",  "content": "content"}, o.f);
    o.jio.put({"_id": "file2", "content": "content2"}, o.f2);
    o.jio.put({"_id": "file3", "content": "content3"}, o.f3);
    o.tick(o, 1000, "f");
    o.tick(o, "f2");
    o.tick(o, "f3");
    o.jio.stop();

});

test ("Similar Jobs at the same time (Replace)", function () {

    var o = generateTools(this);

    o.jio = JIO.newJio({"type":"dummyallok"});
    o.spy(o, "status", 12, "job1 replaced", "f");
    o.spy(o, "status", 12, "job2 replaced", "f2");
    o.spy(o, "value", {"ok": true, "id": "file"}, "job3 ok", "f3");
    o.jio.put({"_id": "file", "content": "content"}, o.f);
    o.jio.put({"_id": "file", "content": "content"}, o.f2);
    o.jio.put({"_id": "file", "content": "content"}, o.f3);
    o.tick(o, 1000, "f");
    o.tick(o, "f2");
    o.tick(o, "f3");
    o.jio.stop();

});

test ("One document aim jobs at the same time (Wait for job(s))" , function () {

    var o = generateTools(this);

    o.jio = JIO.newJio({"type":"dummyallok"});
    o.spy(o, "value", {"ok": true, "id": "file"}, "job1", "f");
    o.spy(o, "value", {"ok": true, "id": "file"}, "job2", "f2");
    o.spy(o, "value", {"_id": "file", "title": "get_title"}, "job3", "f3");

    o.jio.post({"_id": "file", "content": "content"}, o.f);
    o.testLastJobWaitForJob(undefined, "job1 is not waiting for someone");

    o.jio.put({"_id": "file", "content": "content"}, o.f2);
    o.testLastJobWaitForJob([1], "job2 is waiting");

    o.jio.get("file", o.f3);
    o.testLastJobWaitForJob([1, 2], "job3 is waiting");

    o.tick(o, 1000, "f");
    o.tick(o, "f2");
    o.tick(o, "f3");
    o.jio.stop();

});

test ("One document aim jobs at the same time (Elimination)" , function () {

    var o = generateTools(this);

    o.jio = JIO.newJio({"type":"dummyallok"});
    o.spy(o, "status", 10, "job1 stopped", "f");
    o.spy(o, "value", {"ok": true, "id": "file"}, "job2", "f2");

    o.jio.post({"_id": "file", "content": "content"}, o.f);
    o.testLastJobLabel("post", "job1 exists");

    o.jio.remove({"_id": "file"}, o.f2);
    o.testLastJobLabel("remove", "job1 does not exist anymore");

    o.tick(o, 1000, "f");
    o.tick(o, "f2");
    o.jio.stop();

});

test ("One document aim jobs at the same time (Not Acceptable)" , function () {

    var o = generateTools(this);

    o.jio = JIO.newJio({"type":"dummyallok"});
    o.spy(o, "value", {"_id": "file", "title": "get_title"}, "job1", "f");
    o.spy(o, "status", 11, "job2 is not acceptable", "f2");

    o.jio.get("file", o.f);
    o.testLastJobId(1, "job1 added to queue");
    o.waitUntilLastJobIs("on going");

    o.jio.get("file", o.f2);
    o.testLastJobId(1, "job2 not added");

    o.tick(o, 1000, "f");
    o.tick(o, "f2");
    o.jio.stop();

});

test ("Server will be available soon (Wait for time)" , function () {

    var o = generateTools(this);
    o.max_retry = 3;

    o.jio = JIO.newJio({"type":"dummyall3tries"});
    o.spy(o, "value", {"ok": true, "id": "file"}, "job1", "f");

    o.jio.put({"_id": "file", "content": "content"},
              {"max_retry": o.max_retry}, o.f);
    for (o.i = 0; o.i < o.max_retry - 1; o.i += 1) {
        o.waitUntilLastJobIs("on going");
        o.waitUntilLastJobIs("wait");
        o.testLastJobWaitForTime("job1 is waiting for time");
    }

    o.tick(o, 1000, "f");
    o.jio.stop();

});

module ( "Jio Restore");

test ("Restore old Jio", function() {

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "dummyall3tries",
        "application_name": "jiotests"
    });

    o.jio_id = o.jio.getId();

    o.jio.put({"_id": "file", "title": "myFile"}, {"max_retry":3}, o.f);
    o.waitUntilLastJobIs("initial"); // "on going" or "wait" should work
    // xxx also test with o.waitUntilLastJobIs("on going") ?
    o.jio.close();

    o.jio = JIO.newJio({
        "type": "dummyallok",
        "application_name": "jiotests"
    });
    o.waitUntilAJobExists(30000); // timeout 30 sec
    o.testLastJobLabel("put", "Job restored");
    o.clock.tick(1000);
    ok(getLastJob(o.jio.getId()) === undefined,
       "Job executed");

    o.jio.stop();

});

module ( "Jio LocalStorage" );

test ("Post", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "local",
        "username": "upost",
        "application_name": "apost"
    });

    // post without id
    o.spy (o, "status", 405, "Post without id");
    o.jio.post({}, o.f);
    o.tick(o);

    // post non empty document
    o.spy (o, "value", {"ok": true, "id": "post1"}, "Post");
    o.jio.post({"_id": "post1", "title": "myPost1"}, o.f);
    o.tick(o);

    deepEqual(
        localstorage.getItem("jio/localstorage/upost/apost/post1"),
        {
            "_id": "post1",
            "title": "myPost1"
        },
        "Check document"
    );

    // post but document already exists
    o.spy (o, "status", 409, "Post but document already exists");
    o.jio.post({"_id": "post1", "title": "myPost2"}, o.f);
    o.tick(o);

    o.jio.stop();
});


test ("Put", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "local",
        "username": "uput",
        "application_name": "aput"
    });

    // put without id
    // error 20 -> document id required
    o.spy (o, "status", 20, "Put without id");
    o.jio.put({}, o.f);
    o.tick(o);

    // put non empty document
    o.spy (o, "value", {"ok": true, "id": "put1"}, "Creates a document");
    o.jio.put({"_id": "put1", "title": "myPut1"}, o.f);
    o.tick(o);

    // check document
    deepEqual(
        localstorage.getItem("jio/localstorage/uput/aput/put1"),
        {
            "_id": "put1",
            "title": "myPut1"
        },
        "Check document"
    );

    // put but document already exists
    o.spy (o, "value", {"ok": true, "id": "put1"}, "Update the document");
    o.jio.put({"_id": "put1", "title": "myPut2"}, o.f);
    o.tick(o);

    // check document
    deepEqual(
        localstorage.getItem("jio/localstorage/uput/aput/put1"),
        {
            "_id": "put1",
            "title": "myPut2"
        },
        "Check document"
    );

    o.jio.stop();

});

test ("PutAttachment", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "local",
        "username": "uputattmt",
        "application_name": "aputattmt"
    });

    // putAttachment without doc id
    // error 20 -> document id required
    o.spy(o, "status", 20, "PutAttachment without doc id");
    o.jio.putAttachment({}, o.f);
    o.tick(o);

    // putAttachment without attachment id
    // error 22 -> attachment id required
    o.spy(o, "status", 22, "PutAttachment without attachment id");
    o.jio.putAttachment({"id": "putattmt1"}, o.f);
    o.tick(o);

    // putAttachment without document
    // error 404 -> not found
    o.spy(o, "status", 404, "PutAttachment without document");
    o.jio.putAttachment({"id": "putattmt1/putattmt2"}, o.f);
    o.tick(o);

    // adding a document
    localstorage.setItem("jio/localstorage/uputattmt/aputattmt/putattmt1", {
        "_id": "putattmt1",
        "title": "myPutAttmt1"
    });

    // putAttachment with document
    o.spy(o, "value", {"ok": true, "id": "putattmt1/putattmt2"},
          "PutAttachment with document, without data");
    o.jio.putAttachment({"id": "putattmt1/putattmt2"}, o.f);
    o.tick(o);

    // check document
    deepEqual(
        localstorage.getItem("jio/localstorage/uputattmt/aputattmt/putattmt1"),
        {
            "_id": "putattmt1",
            "title": "myPutAttmt1",
            "_attachments": {
                "putattmt2": {
                    "length": 0,
                    // md5("")
                    "digest": "md5-d41d8cd98f00b204e9800998ecf8427e"
                }
            }
        },
        "Check document"
    );

    // check attachment
    deepEqual(
        localstorage.getItem(
            "jio/localstorage/uputattmt/aputattmt/putattmt1/putattmt2"),
        "", "Check attachment"
    );

    // update attachment
    o.spy(o, "value", {"ok": true, "id": "putattmt1/putattmt2"},
          "Update Attachment, with data");
    o.jio.putAttachment({"id": "putattmt1/putattmt2", "data": "abc"}, o.f);
    o.tick(o);

    // check document
    deepEqual(
        localstorage.getItem("jio/localstorage/uputattmt/aputattmt/putattmt1"),
        {
            "_id": "putattmt1",
            "title": "myPutAttmt1",
            "_attachments": {
                "putattmt2": {
                    "length": 3,
                    // md5("abc")
                    "digest": "md5-900150983cd24fb0d6963f7d28e17f72"
                }
            }
        },
        "Check document"
    );

    // check attachment
    deepEqual(
        localstorage.getItem(
            "jio/localstorage/uputattmt/aputattmt/putattmt1/putattmt2"),
        "abc", "Check attachment"
    );

    o.jio.stop();
});

test ("Get", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "local",
        "username": "uget",
        "application_name": "aget"
    });

    // get inexistent document
    o.spy(o, "status", 404, "Get inexistent document");
    o.jio.get("get1", o.f);
    o.tick(o);

    // get inexistent attachment
    o.spy(o, "status", 404, "Get inexistent attachment");
    o.jio.get("get1/get2", o.f);
    o.tick(o);

    // adding a document
    o.doc_get1 = {
        "_id": "get1",
        "title": "myGet1"
    };
    localstorage.setItem("jio/localstorage/uget/aget/get1", o.doc_get1);

    // get document
    o.spy(o, "value", o.doc_get1, "Get document");
    o.jio.get("get1", o.f);
    o.tick(o);

    // get inexistent attachment (document exists)
    o.spy(o, "status", 404, "Get inexistent attachment (document exists)");
    o.jio.get("get1/get2", o.f);
    o.tick(o);

    // adding an attachment
    o.doc_get1["_attachments"] = {
        "get2": {
            "length": 2,
            // md5("de")
            "digest": "md5-5f02f0889301fd7be1ac972c11bf3e7d"
        }
    };
    localstorage.setItem("jio/localstorage/uget/aget/get1", o.doc_get1);
    localstorage.setItem("jio/localstorage/uget/aget/get1/get2", "de");

    // get attachment
    o.spy(o, "value", "de", "Get attachment");
    o.jio.get("get1/get2", o.f);
    o.tick(o);

    o.jio.stop();

});

test ("Remove", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "local",
        "username": "uremove",
        "application_name": "aremove"
    });

    // remove inexistent document
    o.spy(o, "status", 404, "Remove inexistent document");
    o.jio.remove({"_id": "remove1"}, o.f);
    o.tick(o);

    // remove inexistent document/attachment
    o.spy(o, "status", 404, "Remove inexistent document/attachment");
    o.jio.remove({"_id": "remove1/remove2"}, o.f);
    o.tick(o);

    // adding a document
    localstorage.setItem("jio/localstorage/uremove/aremove/remove1", {
        "_id": "remove1",
        "title": "myRemove1"
    });

    // remove document
    o.spy(o, "value", {"ok": true, "id": "remove1"}, "Remove document");
    o.jio.remove({"_id": "remove1"}, o.f);
    o.tick(o);

    // check document
    ok(localstorage.getItem("jio/localstorage/uremove/aremove/remove1")===null,
       "Check document is removed");

    // adding a document + attmt
    localstorage.setItem("jio/localstorage/uremove/aremove/remove1", {
        "_id": "remove1",
        "title": "myRemove1",
        "_attachments": {
            "remove2": {
                "length": 4,
                "digest": "md5-blahblah"
            }
        }
    });
    localstorage.setItem(
        "jio/localstorage/uremove/aremove/remove1/remove2", "fghi");

    // remove attachment
    o.spy(o, "value", {"ok": true, "id": "remove1"}, "Remove document and attachment");
    o.jio.remove({"_id": "remove1"}, o.f);
    o.tick(o);
    ok(localstorage.getItem("jio/localstorage/uremove/aremove/remove1"
       )===null, "Check document is removed");
    ok(localstorage.getItem("jio/localstorage/uremove/aremove/remove1/remove2"
      )===null, "Check attachment is removed");

    o.jio.stop();

});


test ("AllDocs", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "local",
        "username": "ualldocs",
        "application_name": "aalldocs"
    });

    // alldocs
    // error 405 -> method not allowed
    o.spy(o, "status", 405, "Method not allowed");
    o.jio.allDocs(o.f);
    o.tick(o);

    o.jio.stop();

});

module ( "Jio Revision Storage + Local Storage" );

test ("Post", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "revision",
        "sub_storage": {
            "type": "local",
            "username": "urevpost",
            "application_name": "arevpost"
        }
    });
    o.localpath = "jio/localstorage/urevpost/arevpost";

    // post without id
    o.revisions = {"start": 0, "ids": []};
    o.spy (o, "status", undefined, "Post without id");
    o.jio.post({}, function (err, response) {
        o.f.apply(arguments);
        o.uuid = (err || response).id;
        ok(isUuid(o.uuid), "Uuid should look like " +
           "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx : " + o.uuid);
    });
    o.tick(o);
    o.rev = "1-"+generateRevisionHash({"_id": o.uuid}, o.revisions);

    // check document
    deepEqual(
        localstorage.getItem(o.localpath + "/" + o.uuid + "." + o.rev),
        {"_id": o.uuid + "." + o.rev},
        "Check document"
    );

    // check document tree
    o.doc_tree = {
        "_id": o.uuid + ".revision_tree.json",
        "children": [{
            "rev": o.rev, "status": "available", "children": []
        }]
    };
    deepEqual(
        localstorage.getItem(
            o.localpath + "/" + o.uuid + ".revision_tree.json"
        ),
        o.doc_tree,
        "Check document tree"
    );

    // post non empty document
    o.doc = {"_id": "post1", "title": "myPost1"};
    o.rev = "1-"+generateRevisionHash(o.doc, o.revisions);
    o.spy (o, "value", {"ok": true, "id": "post1", "rev": o.rev}, "Post");
    o.jio.post(o.doc, o.f);
    o.tick(o);

    // check document
    o.doc["_id"] = "post1."+o.rev;
    deepEqual(
        localstorage.getItem(o.localpath + "/post1." + o.rev),
        o.doc,
        "Check document"
    );

    // check document tree
    o.doc_tree._id = "post1.revision_tree.json";
    o.doc_tree.children[0] = {
        "rev": o.rev, "status": "available", "children": []
    };
    deepEqual(
        localstorage.getItem(
            o.localpath + "/post1.revision_tree.json"
        ),
        o.doc_tree,
        "Check document tree"
    );

    // post and document already exists
    o.doc = {"_id": "post1", "title": "myPost2"};
    o.rev = "1-"+generateRevisionHash(o.doc, o.revisions);
    o.spy (o, "value", {
        "ok": true, "id": "post1", "rev": o.rev
    }, "Post and document already exists");
    o.jio.post(o.doc, o.f);
    o.tick(o);

    // check document
    o.doc["_id"] = "post1."+o.rev;
    deepEqual(
        localstorage.getItem(o.localpath + "/post1." + o.rev),
        o.doc,
        "Check document"
    );

    // check document tree
    o.doc_tree._id = "post1.revision_tree.json";
    o.doc_tree.children.unshift({
        "rev": o.rev, "status": "available", "children": []
    });
    deepEqual(
        localstorage.getItem(
            o.localpath + "/post1.revision_tree.json"
        ),
        o.doc_tree,
        "Check document tree"
    );

    // post + revision
    o.doc = {"_id": "post1", "_rev": o.rev, "title": "myPost2"};
    o.revisions = {"start": 1, "ids": [o.rev.split('-')[1]]};
    o.rev = "2-"+generateRevisionHash(o.doc, o.revisions);
    o.spy (o, "status", undefined, "Post + revision");
    o.jio.post(o.doc, o.f);
    o.tick(o);

    // // keep_revision_history
    // ok (false, "keep_revision_history Option Not Implemented");

    // check document
    o.doc["_id"] = "post1."+o.rev;
    delete o.doc._rev;
    deepEqual(
        localstorage.getItem(o.localpath + "/post1." + o.rev),
        o.doc,
        "Check document"
    );

    // check document tree
    o.doc_tree._id = "post1.revision_tree.json";
    o.doc_tree.children[0].children.unshift({
        "rev": o.rev, "status": "available", "children": []
    });
    deepEqual(
        localstorage.getItem(
            o.localpath + "/post1.revision_tree.json"
        ),
        o.doc_tree,
        "Check document tree"
    );

    o.jio.stop();

});

test ("Put", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "revision",
        "sub_storage": {
            "type": "local",
            "username": "urevput",
            "application_name": "arevput"
        }
    });
    o.localpath = "jio/localstorage/urevput/arevput";

    // put without id
    // error 20 -> document id required
    o.spy (o, "status", 20, "Put without id");
    o.jio.put({}, o.f);
    o.tick(o);

    // put non empty document
    o.doc = {"_id": "put1", "title": "myPut1"};
    o.revisions = {"start": 0, "ids": []};
    o.rev = "1-"+generateRevisionHash(o.doc, o.revisions);
    o.spy (o, "value", {"ok": true, "id": "put1", "rev": o.rev},
           "Creates a document");
    o.jio.put(o.doc, o.f);
    o.tick(o);

    // check document
    o.doc._id = "put1." + o.rev;
    deepEqual(
        localstorage.getItem(o.localpath + "/put1." + o.rev),
        o.doc,
        "Check document"
    );

    // check document tree
    o.doc_tree = {
        "_id": "put1.revision_tree.json",
        "children": [{
            "rev": o.rev, "status": "available", "children": []
        }]
    };
    deepEqual(
        localstorage.getItem(
            o.localpath + "/put1.revision_tree.json"
        ),
        o.doc_tree,
        "Check document tree"
    );

    // put and document already exists
    o.spy (o, "status", 409, "Update the document");
    o.jio.put({"_id": "put1", "title": "myPut2"}, o.f);
    o.tick(o);


    // put + revision
    o.doc = {"_id": "put1", "_rev": o.rev, "title": "myPut2"};
    o.revisions = {"start": 1, "ids": [o.rev.split('-')[1]]};
    o.rev = "2-"+generateRevisionHash(o.doc, o.revisions);
    o.spy (o, "status", undefined, "Put + revision");
    o.jio.put(o.doc, o.f);
    o.tick(o);

    // check document
    o.doc._id = "put1." + o.rev;
    delete o.doc._rev;
    deepEqual(
        localstorage.getItem(o.localpath + "/put1." + o.rev),
        o.doc,
        "Check document"
    );

    // check document tree
    o.doc_tree.children[0].children.unshift({
        "rev": o.rev, "status": "available", "children": []
    });
    deepEqual(
        localstorage.getItem(
            o.localpath + "/put1.revision_tree.json"
        ),
        o.doc_tree,
        "Check document tree"
    );

    o.jio.stop();

});

test ("Get", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "revision",
        "sub_storage": {
            "type": "local",
            "username": "urevget",
            "application_name": "arevget"
        }
    });
    o.localpath = "jio/localstorage/urevget/arevget";

    // get inexistent document
    o.spy(o, "status", 404, "Get inexistent document (winner)");
    o.jio.get("get1", o.f);
    o.tick(o);

    // get inexistent attachment
    o.spy(o, "status", 404, "Get inexistent attachment (winner)");
    o.jio.get("get1/get2", o.f);
    o.tick(o);

    // adding a document
    o.doctree = {"children":[{
        "rev": "1-rev1", "status": "available", "children": []
    }]};
    o.doc_myget1 = {"_id": "get1", "title": "myGet1"};
    localstorage.setItem(o.localpath+"/get1.revision_tree.json", o.doctree);
    localstorage.setItem(o.localpath+"/get1.1-rev1", o.doc_myget1);

    // get document
    o.doc_myget1_cloned = clone(o.doc_myget1);
    o.doc_myget1_cloned["_rev"] = "1-rev1";
    o.doc_myget1_cloned["_revisions"] = {"start": 1, "ids": ["rev1"]};
    o.doc_myget1_cloned["_revs_info"] = [{
        "rev": "1-rev1", "status": "available"
    }];
    o.spy(o, "value", o.doc_myget1_cloned, "Get document (winner)");
    o.jio.get("get1", {"revs_info": true, "revs": true, "conflicts": true},
              o.f);
    o.tick(o);

    // adding two documents
    o.doctree = {"children":[{
        "rev": "1-rev1", "status": "available", "children": []
    },{
        "rev": "1-rev2", "status": "available", "children": [{
            "rev": "2-rev3", "status": "available", "children": []
        }]
    }]};
    o.doc_myget2 = {"_id": "get1", "title": "myGet2"};
    o.doc_myget3 = {"_id": "get1", "title": "myGet3"};
    localstorage.setItem(o.localpath+"/get1.revision_tree.json", o.doctree);
    localstorage.setItem(o.localpath+"/get1.1-rev2", o.doc_myget2);
    localstorage.setItem(o.localpath+"/get1.2-rev3", o.doc_myget3);

    // get document
    o.doc_myget3_cloned = clone(o.doc_myget3);
    o.doc_myget3_cloned["_rev"] = "2-rev3";
    o.doc_myget3_cloned["_revisions"] = {"start": 2, "ids": ["rev3","rev2"]};
    o.doc_myget3_cloned["_revs_info"] = [{
        "rev": "2-rev3", "status": "available"
    },{
        "rev": "1-rev2", "status": "available"
    }];
    o.doc_myget3_cloned["_conflicts"] = ["1-rev1"];
    o.spy(o, "value", o.doc_myget3_cloned,
          "Get document (winner, after posting another one)");
    o.jio.get("get1", {"revs_info": true, "revs": true, "conflicts": true},
              o.f);
    o.tick(o);

    // get inexistent specific document
    o.spy(o, "status", 404, "Get document (inexistent specific revision)");
    o.jio.get("get1", {
        "revs_info": true, "revs": true, "conflicts": true,
        "rev": "1-rev0"
    }, o.f);
    o.tick(o);

    // get specific document
    o.doc_myget2_cloned = clone(o.doc_myget2);
    o.doc_myget2_cloned["_rev"] = "1-rev2";
    o.doc_myget2_cloned["_revisions"] = {"start": 1, "ids": ["rev2"]};
    o.doc_myget2_cloned["_revs_info"] = [{
        "rev": "1-rev2", "status": "available"
    }];
    o.doc_myget2_cloned["_conflicts"] = ["1-rev1"];
    o.spy(o, "value", o.doc_myget2_cloned, "Get document (specific revision)");
    o.jio.get("get1", {
        "revs_info": true, "revs": true, "conflicts": true,
        "rev": "1-rev2"
    }, o.f);
    o.tick(o);

    // adding an attachment
    o.attmt_myget2 = {
        "get2": {
            "length": 3,
            "digest": "md5-dontcare",
            "revpos": 1
        }
    };
    o.doc_myget2["_attachments"] = o.attmt_myget2;
    o.doc_myget3["_attachments"] = o.attmt_myget2;
    localstorage.setItem(o.localpath+"/get1.1-rev2", o.doc_myget2);
    localstorage.setItem(o.localpath+"/get1.2-rev3", o.doc_myget3);
    localstorage.setItem(o.localpath+"/get1.1-rev2/get2", "abc");

    // get attachment winner
    o.spy(o, "value", "abc", "Get attachment (winner)");
    o.jio.get("get1/get2", o.f);
    o.tick(o);

    // get inexistent attachment specific rev
    o.spy(o, "status", 404, "Get inexistent attachment (specific revision)");
    o.jio.get("get1/get2", {
        "revs_info": true, "revs": true, "conflicts": true,
        "rev": "1-rev1"
    }, o.f);
    o.tick(o);

    // get attachment specific rev
    o.spy(o, "value", "abc", "Get attachment (specific revision)");
    o.jio.get("get1/get2", {
        "revs_info": true, "revs": true, "conflicts": true,
        "rev": "1-rev2"
    }, o.f);
    o.tick(o);

    // get document with attachment (specific revision)
    o.doc_myget2_cloned["_attachments"] = o.attmt_myget2;
    o.spy(o, "value", o.doc_myget2_cloned,
          "Get document which have an attachment (specific revision)");
    o.jio.get("get1", {
        "revs_info": true, "revs": true, "conflicts": true,
        "rev": "1-rev2"
    }, o.f);
    o.tick(o);

    // get document with attachment (winner)
    o.doc_myget3_cloned["_attachments"] = o.attmt_myget2;
    o.spy(o, "value", o.doc_myget3_cloned,
          "Get document which have an attachment (winner)");
    o.jio.get("get1", {"revs_info": true, "revs": true, "conflicts": true},
              o.f);
    o.tick(o);

    o.jio.stop();

});

test ("Remove", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "revision",
        "sub_storage": {
            "type": "local",
            "username": "urevrem",
            "application_name": "arevrem"
        }
    });
    o.localpath = "jio/localstorage/urevrem/arevrem";

    // 1. remove document without revision
    o.spy (o, "status", 404,
           "Remove document (no doctree, no revision)");
    o.jio.remove({"_id":"remove1"}, o.f);
    o.tick(o);

    // 2. remove attachment without revision
    o.spy (o, "status", 404,
           "Remove attachment (no doctree, no revision)");
    o.jio.remove({"_id":"remove1/remove2"}, o.f);
    o.tick(o);

    // adding two documents
    o.doc_myremove1 = {"_id": "remove1", "title": "myRemove1"};
    o.doc_myremove2 = {"_id": "remove1", "title": "myRemove2"};

    o.very_old_rev = "1-veryoldrev";

    localstorage.setItem(o.localpath+"/remove1."+o.very_old_rev,
                         o.doc_myremove1);
    localstorage.setItem(o.localpath+"/remove1.1-rev2", o.doc_myremove1);

    // add attachment
    o.attmt_myremove1 = {
        "remove2": {
            "length": 3,
            "digest": "md5-dontcare",
            "revpos":1
        },
    };
    o.doc_myremove1 = {"_id": "remove1", "title": "myRemove1",
                       "_attachments":o.attmt_myremove1};
    o.revisions = {"start":1,"ids":[o.very_old_rev.split('-'),[1]]}
    o.old_rev = "2-"+generateRevisionHash(o.doc_myremove1, o.revisions);

    localstorage.setItem(o.localpath+"/remove1."+o.old_rev, o.doc_myremove1);
    localstorage.setItem(o.localpath+"/remove1."+o.old_rev+"/remove2", "xyz");

    o.doctree = {"children":[{
        "rev": o.very_old_rev, "status": "available", "children": [{
            "rev": o.old_rev, "status": "available", "children": []
        }]
    },{
        "rev": "1-rev2", "status": "available", "children": []
    }]};
    localstorage.setItem(o.localpath+"/remove1.revision_tree.json", o.doctree);

    // 3. remove non existing attachment with revision
    o.spy(o, "status", 404,
          "Remove NON-existing attachment (revision)");
    o.jio.remove({"_id":"remove1.1-rev2/remove0","_rev":o.old_rev}, o.f);
    o.tick(o);

    o.revisions = {"start": 2, "ids":[
        o.old_rev.split('-')[1], o.very_old_rev.split('-')[1]
    ]};
    o.doc_myremove1 = {"_id":"remove1/remove2","_rev":o.old_rev};
    o.rev = "3-"+generateRevisionHash(o.doc_myremove1, o.revisions);

    // 4. remove existing attachment with revision
    o.spy (o, "value", {"ok": true, "id": "remove1."+o.rev, "rev": o.rev},
           "Remove existing attachment (revision)");
    o.jio.remove({"_id":"remove1/remove2","_rev":o.old_rev}, o.f);
    o.tick(o);

    o.testtree = {"children":[{
        "rev": o.very_old_rev, "status": "available", "children": [{
            "rev": o.old_rev, "status": "available", "children": [{
                "rev": o.rev, "status": "available", "children": []
            }]
        }]
    },{
        "rev": "1-rev2", "status": "available", "children": []
    }]};

    // 5. check if document tree has been updated correctly
    deepEqual(localstorage.getItem(
        "jio/localstorage/urevrem/arevrem/remove1.revision_tree.json"
    ),o.testtree, "Check document tree");

    // 6. check if attachment has been removed
    deepEqual(localstorage.getItem(
        "jio/localstorage/urevrem/arevrem/remove1."+o.rev+"/remove2"
    ), null, "Check attachment");

    // 7. check if document is updated
    deepEqual(localstorage.getItem(
        "jio/localstorage/urevrem/arevrem/remove1."+o.rev
    ), {"_id": "remove1."+o.rev, "title":"myRemove1"}, "Check document");

    // add another attachment
    o.attmt_myremove2 = {
        "remove3": {
            "length": 3,
            "digest": "md5-hello123"
        },
        "revpos":1
    };
    o.doc_myremove2 = {"_id": "remove1", "title": "myRemove2",
                       "_attachments":o.attmt_myremove2};
    o.revisions = {"start":1,"ids":["rev2"] };
    o.second_old_rev = "2-"+generateRevisionHash(o.doc_myremove2, o.revisions);

    localstorage.setItem(o.localpath+"/remove1."+o.second_old_rev,
                         o.doc_myremove2);
    localstorage.setItem(o.localpath+"/remove1."+o.second_old_rev+"/remove3",
                         "stu");

    o.doctree = {"children":[{
        "rev": o.very_old_rev, "status": "available", "children": [{
            "rev": o.old_rev, "status": "available", "children": [{
                "rev": o.rev, "status": "available", "children":[]
            }]
        }]
    },{
        "rev": "1-rev2", "status": "available", "children": [{
            "rev": o.second_old_rev, "status": "available", "children":[]
        }]
    }]};
    localstorage.setItem(o.localpath+"/remove1.revision_tree.json", o.doctree);

    // 8. remove non existing attachment without revision
    o.spy (o,"status", 409,
           "409 - Removing non-existing-attachment (no revision)");
    o.jio.remove({"_id":"remove1/remove0"}, o.f);
    o.tick(o);

    o.revisions = {"start":2,"ids":[o.second_old_rev.split('-')[1],"rev2"]};
    o.doc_myremove3 = {"_id":"remove1/remove3","_rev":o.second_old_rev};
    o.second_rev = "3-"+generateRevisionHash(o.doc_myremove3, o.revisions);

    // 9. remove existing attachment without revision
    o.spy (o,"status", 409, "409 - Removing existing attachment (no revision)");
    o.jio.remove({"_id":"remove1/remove3"}, o.f);
    o.tick(o);

    // 10. remove wrong revision
    o.spy (o,"status", 409, "409 - Removing document (false revision)");
    o.jio.remove({"_id":"remove1","_rev":"1-rev2"}, o.f);
    o.tick(o);

    o.revisions = {"start": 3, "ids":[
        o.rev.split('-')[1],
        o.old_rev.split('-')[1],o.very_old_rev.split('-')[1]
    ]};
    o.doc_myremove4 = {"_id":"remove1","_rev":o.rev};
    o.second_new_rev = "4-"+
        generateRevisionHash(o.doc_myremove4, o.revisions, true);

    // 11. remove document version with revision
    o.spy (o, "value", {"ok": true, "id": "remove1", "rev":
        o.second_new_rev},
           "Remove document (with revision)");
    o.jio.remove({"_id":"remove1", "_rev":o.rev}, o.f);
    o.tick(o);

    o.testtree["children"][0]["children"][0]["children"][0]["children"].push({
        "rev": o.second_new_rev,
        "status": "deleted",
        "children": []
    });
    o.testtree["children"][1]["children"].push({
        "rev":o.second_old_rev,
        "status":"available",
        "children":[]
    });

    deepEqual(localstorage.getItem(
        "jio/localstorage/urevrem/arevrem/remove1.revision_tree.json"
    ), o.testtree, "Check document tree");

    deepEqual(localstorage.getItem(
        "jio/localstorage/urevrem/arevrem/remove1."+o.second_new_rev+"/remove2"
    ), null, "Check attachment");

    deepEqual(localstorage.getItem(
        "jio/localstorage/urevrem/arevrem/remove1."+o.second_new_rev
    ), null, "Check document");

    // remove document without revision
    o.spy (o,"status", 409, "409 - Removing document (no revision)");
    o.jio.remove({"_id":"remove1"}, o.f);
    o.tick(o);

    o.jio.stop();
});

module ( "Jio Revision Storage + Local Storage" );

test ("Scenario", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "revision",
        "sub_storage": {
            "type": "local",
            "username": "usam1",
            "application_name": "asam1"
        }
    });
    o.localpath = "jio/localstorage/usam1/asam1";

    // new application
    ok ( o.jio, "I open my application with revision and localstorage");

    // put non empty document A-1
    o.doc = {"_id": "sample1", "title": "mySample1"};
    o.revisions = {"start": 0, "ids": []};
    o.hex = generateRevisionHash(o.doc, o.revisions);
    o.rev = "1-"+o.hex;

    o.spy (o, "value", {"ok": true, "id": "sample1", "rev": o.rev},
           "Then, I create a new document (no attachment), my application "+
           "keep the revision in memory");
    o.jio.put(o.doc, o.f);
    o.tick(o);

    // open new tab (JIO)
    o.jio2 = JIO.newJio({
        "type": "revision",
        "sub_storage": {
            "type": "local",
            "username": "usam1",
            "application_name": "asam1"
        }
    });
    o.localpath = "jio/localstorage/usam1/asam1";

    // Create a new JIO in a new tab
    ok (o.jio2, "Now, I am opening a new tab, with the same application"+
        " and the same storage tree");

    // Get the document from the first storage
    o.doc._rev = o.rev;
    o.doc._revisions = {"ids":[o.hex], "start":1 };
    o.doc._revs_info = [{"rev": o.rev, "status": "available"}];
    o.spy(o, "value", o.doc, "And, on this new tab, I load the document,"+
        "and my application keep the revision in memory");
    o.jio2.get("sample1", {
        "revs_info": true, "revs": true, "conflicts": true,
        "rev": o.rev }, o.f);
    o.tick(o);

    // MODFIY the 2nd version
    o.doc_2 = {"_id": "sample1", "_rev": o.rev,
        "title":"mySample2_modified"};
    o.revisions_2 = {"start":1 , "ids":[o.hex]};
    o.hex_2 = generateRevisionHash(o.doc_2, o.revisions_2)
    o.rev_2 = "2-"+o.hex_2;
    o.spy (o, "value", {"id":"sample1", "ok":true, "rev": o.rev_2},
           "So, I can modify and update it");
    o.jio2.put(o.doc_2, o.f);
    o.tick(o);

    // MODFIY first version
    o.doc_1 = {
        "_id": "sample1", "_rev": o.rev, "title": "mySample1_modified"
    };
    o.revisions_1 = {"start": 1, "ids":[o.rev.split('-')[1]
    ]};
    o.hex_1 = generateRevisionHash(o.doc_1, o.revisions_1);
    o.rev_1 = "2-"+o.hex_1;
    o.spy (o, "value", {"id":"sample1", "ok":true, "rev": o.rev_1},
           "Back to the first tab, I update the document.");
    o.jio.put(o.doc_1, o.f);
    o.tick(o);

    // Close 1st tab
    o.jio.close();

    // Close 2nd tab
    o.jio2.close();
    ok ( o.jio2, "I close tab both tabs");

    // Reopen JIO
    o.jio = JIO.newJio({
        "type": "revision",
        "sub_storage": {
            "type": "local",
            "username": "usam1",
            "application_name": "asam1"
        }
    });
    o.localpath = "jio/localstorage/usam1/asam1";
    ok ( o.jio, "Later, I open my application again");

    // GET document without revision = winner & conflict!
    o.mydocSample3 = {"_id": "sample1", "title": "mySample1_modified",
                      "_rev": o.rev_1};
    o.mydocSample3._conflicts = [o.rev_2]
    o.mydocSample3._revs_info = [{"rev": o.rev_1, "status": "available"},{
        "rev":o.rev,"status":"available"
        }];
    o.mydocSample3._revisions = {"ids":[o.hex_1, o.hex], "start":2 };
    o.spy(o, "value", o.mydocSample3,
          "I load the same document as before, and a popup shows that "+
          "there is a conflict");
    o.jio.get("sample1", {"revs_info": true, "revs": true, "conflicts": true,
        }, o.f);
    o.tick(o);

    // REMOVE one of the two conflicting versions
    o.revisions = {"start": 2, "ids":[
        o.rev_1.split('-')[1],o.rev.split('-')[1]
    ]};
    o.doc_myremove3 = {"_id": "sample1", "_rev": o.rev_1};
    o.rev_3 = "3-"+generateRevisionHash(o.doc_myremove3, o.revisions,true);

    o.spy (o, "value", {"ok": true, "id": "sample1", "rev": o.rev_3},
           "I choose one of the document and close the application.");
    o.jio.remove({"_id":"sample1", "_rev":o.rev_1}, o.f);
    o.tick(o);

    // check to see if conflict still exists
    o.mydocSample4 = {"_id": "sample1", "title": "mySample2_modified",
                      "_rev": o.rev_2};
    o.mydocSample4._revs_info = [{"rev": o.rev_2, "status": "available"},{
        "rev":o.rev,"status":"available"
        }];
    o.mydocSample4._revisions = {"ids":[o.hex_2, o.hex], "start":2 };

    o.spy(o, "value", o.mydocSample4, "Test if conflict still exists");
    o.jio.get("sample1", {"revs_info": true, "revs": true,
              "conflicts": true,}, o.f);
    o.tick(o);

    // END
    o.jio.stop();

});

module ("JIO Replicate Revision Storage");

  var testReplicateRevisionStorageGenerator = function (
    sinon, jio_description, document_name_have_revision
  ) {

    var o = generateTools(sinon), leavesAction, generateLocalPath;

    o.jio = JIO.newJio(jio_description);

    generateLocalPath = function (storage_description) {
      return "jio/localstorage/" + storage_description.username + "/" +
        storage_description.application_name;
    };

    leavesAction = function (action, storage_description, param) {
      var i;
      if (param === undefined) {
        param = {};
      } else {
        param = clone(param);
      }
      if (storage_description.storage_list !== undefined) {
        // it is the replicate revision storage tree
        for (i = 0; i < storage_description.storage_list.length; i += 1) {
          leavesAction(action, storage_description.storage_list[i], param);
        }
      } else if (storage_description.sub_storage !== undefined) {
        // it is the revision storage tree
        param.revision = true;
        leavesAction(action, storage_description.sub_storage, param);
      } else {
        // it is the storage tree leaf
        param[storage_description.type] = true;
        action(storage_description, param);
      }
    };
    o.leavesAction = function (action) {
      leavesAction(action, jio_description);
    };

    // post a new document without id
    o.doc = {"title": "post document without id"};
    o.revision = {"start": 0, "ids": []};
    o.spy(o, "status", undefined, "Post document (without id)");
    o.jio.post(o.doc, function (err, response) {
      o.f.apply(arguments);
      o.response_rev = (err || response).rev;
      if (isUuid((err || response).id)) {
        ok(true, "Uuid format");
        o.uuid = (err || response).id;
      } else {
        deepEqual((err || response).id,
                  "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "Uuid format");
      }
    });
    o.tick(o);

    // check document
    o.doc._id = o.uuid;
    o.rev = "1";
    o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision);
    o.leavesAction(function (storage_description, param) {
      var suffix = "", doc = clone(o.doc);
      if (param.revision) {
        deepEqual(o.response_rev, o.rev, "Check revision");
        doc._id += "." + o.local_rev;
        suffix = "." + o.local_rev;
      }
      deepEqual(
        localstorage.getItem(generateLocalPath(storage_description) +
                             "/" + o.uuid + suffix),
        doc, "Check document"
      );
    });

    // post a new document with id
    o.doc = {"_id": "post1", "title": "post new doc with id"};
    o.rev = "1"
    o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev},
          "Post document (with id)");
    o.jio.post(o.doc, o.f);
    o.tick(o);

    // check document
    o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision);
    o.leavesAction(function (storage_description, param) {
      var suffix = "", doc = clone(o.doc);
      if (param.revision) {
        doc._id += "." + o.local_rev;
        suffix = "." + o.local_rev;
      }
      deepEqual(
        localstorage.getItem(generateLocalPath(storage_description) +
                             "/post1" + suffix),
        doc, "Check document"
      );
    });

    // post same document without revision
    o.doc = {"_id": "post1", "title": "post same document without revision"};
    o.rev = "2";
    o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev},
          "Post same document (without revision)");
    o.jio.post(o.doc, o.f);
    o.tick(o);

    // check document
    o.local_rev = "1-" + generateRevisionHash(o.doc, o.revision);
    o.leavesAction(function (storage_description, param) {
      var suffix = "", doc = clone(o.doc);
      if (param.revision) {
        doc._id += "." + o.local_rev;
        suffix = "." + o.local_rev;
      }
      deepEqual(
        localstorage.getItem(generateLocalPath(storage_description) +
                             "/post1" + suffix),
        doc, "Check document"
      );
    });

    // post a new revision
    o.doc = {"_id": "post1", "title": "post new revision", "_rev": o.rev};
    o.rev = "3";
    o.spy(o, "value", {"ok": true, "id": "post1", "rev": o.rev},
          "Post document (with revision)");
    o.jio.post(o.doc, o.f);
    o.tick(o);

    // check document
    o.revision.start += 1;
    o.revision.ids.unshift(o.local_rev.split("-").slice(1).join("-"));
    o.doc._rev = o.local_rev;
    o.local_rev = "2-" + generateRevisionHash(o.doc, o.revision);
    o.leavesAction(function (storage_description, param) {
      var suffix = "", doc = clone(o.doc);
      delete doc._rev;
      if (param.revision) {
        doc._id += "." + o.local_rev;
        suffix = "." + o.local_rev;
      }
      deepEqual(
        localstorage.getItem(generateLocalPath(storage_description) +
                             "/post1" + suffix),
        doc, "Check document"
      );
    });

    o.jio.stop();

  };

  test ("[Local Storage] Scenario", function () {
    testReplicateRevisionStorageGenerator(this, {
      "type": "replicaterevision",
      "storage_list": [{
        "type": "local",
        "username": "ureploc",
        "application_name": "areploc"
      }]
    });
  });
  test ("[Revision + Local Storage] Scenario", function () {
    testReplicateRevisionStorageGenerator(this, {
      "type": "replicaterevision",
      "storage_list": [{
        "type": "revision",
        "sub_storage": {
          "type": "local",
          "username": "ureprevloc",
          "application_name": "areprevloc"
        }
      }]
    });
  });
  test ("[Revision + Local Storage, Local Storage] Scenario", function () {
    testReplicateRevisionStorageGenerator(this, {
      "type": "replicaterevision",
      "storage_list": [{
        "type": "revision",
        "sub_storage": {
          "type": "local",
          "username": "ureprevlocloc",
          "application_name": "areprevlocloc"
        }
      },{
        "type": "local",
        "username": "ureprevlocloc2",
        "application_name": "areprevlocloc2"
      }]
    });
  });

module ("Jio DAVStorage");

test ("Post", function () {

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "dav",
        "username": "davpost",
        "password": "checkpwd",
        "url": "https://ca-davstorage:8080"
    });

    // post without id
    o.spy (o, "status", 405, "Post without id");
    o.jio.post({}, o.f);
    o.clock.tick(5000);

    // post non empty document
    o.addFakeServerResponse("PUT", "myFile", 201, "HTML RESPONSE");
    o.spy(o, "value", {"id": "myFile", "ok": true},
          "Create = POST non empty document");
    o.jio.post({"_id": "myFile", "title": "hello there"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    // post but document already exists (post = error!, put = ok)
    o.answer = JSON.stringify({"_id": "myFile", "title": "hello there"});
    o.addFakeServerResponse("GET", "myFile", 200, o.answer);
    o.spy (o, "status", 409, "Post but document already exists");
    o.jio.post({"_id": "myFile", "title": "hello again"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    // as all custom headers trigger preflight requests, the test also
    // need to simulate CORS (cross domain ajax with preflight)
    // custom header may be authentication for example
    o.jio.stop();

    // do the same tests live webDav-Server simulating CORS!
    /* also check for equality

    deepEqual(
        localstorage.getItem("jio/localstorage/uput/aput/put1"),
        {
            "_id": "put1",
            "title": "myPut1"
        },
        "Check document"
    );
    */
});

test ("Put", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "dav",
        "username": "davput",
        "password": "checkpwd",
        "url": "https://ca-davstorage:8080"
    });

    // put without id => id required
    o.spy (o, "status", 20, "Put without id");
    o.jio.put({}, o.f);
    o.clock.tick(5000);

    // put non empty document
    o.addFakeServerResponse("PUT", "put1", 201, "HTML RESPONSE");
    o.spy (o, "value", {"ok": true, "id": "put1"},
           "Create = PUT non empty document");
    o.jio.put({"_id": "put1", "title": "myPut1"}, o.f);
    o.clock.tick(5000);
    o.server.respond();
    //console.log( o.server );
    //console.log( o.server.requests[0].requestHeaders );
    //console.log( o.server.requests[0].responseHeaders );

    // put but document already exists = update
    o.answer = JSON.stringify({"_id": "put1", "title": "myPut1"});
    o.addFakeServerResponse("GET", "put1", 200, o.answer);
    o.addFakeServerResponse("PUT", "put1", 201, "HTML RESPONSE");
    o.spy (o, "value", {"ok": true, "id": "put1"}, "Updated the document");
    o.jio.put({"_id": "put1", "title": "myPut2abcdedg"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    o.jio.stop();

    // do the same tests live webDav-Server/simulate CORS
    // check for credentials in sinon

});

test ("PutAttachment", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "dav",
        "username": "davputattm",
        "password": "checkpwd",
        "url": "https://ca-davstorage:8080"
    });

    // putAttachment without doc id => id required
    o.spy(o, "status", 20, "PutAttachment without doc id");
    o.jio.putAttachment({}, o.f);
    o.clock.tick(5000);

    // putAttachment without attachment id => attachment id required
    o.spy(o, "status", 22, "PutAttachment without attachment id");
    o.jio.putAttachment({"id": "putattmt1"}, o.f);
    o.clock.tick(5000);

    // putAttachment without underlying document => not found
    o.addFakeServerResponse("GET", "putattmtx", 404, "HTML RESPONSE");
    o.spy(o, "status", 404, "PutAttachment without document");
    o.jio.putAttachment({"id": "putattmtx/putattmt2"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    // putAttachment with document without data
    o.answer = JSON.stringify({"_id": "putattmt1", "title": "myPutAttm1"});
    o.addFakeServerResponse("GET", "putattmt1", 200, o.answer);
    o.addFakeServerResponse("PUT", "putattmt1", 201, "HTML RESPONSE");
    o.addFakeServerResponse("PUT", "putattmt1/putattmt2", 201,"HTML RESPONSE");
    o.spy(o, "value", {"ok": true, "id": "putattmt1/putattmt2"},
          "PutAttachment with document, without data");
    o.jio.putAttachment({"id": "putattmt1/putattmt2"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    // check document
    // check attachment

    // update attachment
    o.answer = JSON.stringify({"_id": "putattmt1", "title": "myPutAttm1"});
    o.addFakeServerResponse("GET", "putattmt1", 200, o.answer);
    o.addFakeServerResponse("PUT", "putattmt1", 201, "HTML RESPONSE");
    o.addFakeServerResponse("PUT", "putattmt1/putattmt2", 201,"HTML RESPONSE");
    o.spy(o, "value", {"ok": true, "id": "putattmt1/putattmt2"},
          "Update Attachment, with data");
    o.jio.putAttachment({"id": "putattmt1/putattmt2", "data": "abc"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    // check document
    // check attachment

    o.jio.stop();

    // do the same tests live webDav-Server/simulate CORS
    // check for credentials in sinon
});

test ("Get", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "dav",
        "username": "davget",
        "password": "checkpwd",
        "url": "https://ca-davstorage:8080"
    });

    // get inexistent document
    o.addFakeServerResponse("GET", "get1", 404, "HTML RESPONSE");
    o.spy(o, "status", 404, "Get non existing document");
    o.jio.get("get1", o.f);
    o.clock.tick(5000);
    o.server.respond();

    // get inexistent attachment
    o.addFakeServerResponse("GET", "get1/get2", 404, "HTML RESPONSE");
    o.spy(o, "status", 404, "Get non existing attachment");
    o.jio.get("get1/get2", o.f);
    o.clock.tick(5000);
    o.server.respond();

    // get document
    o.answer = JSON.stringify({"_id": "get3", "title": "some title"});
    o.addFakeServerResponse("GET", "get3", 200, o.answer);
    o.spy(o, "value", {"_id": "get3", "title": "some title"}, "Get document");
    o.jio.get("get3", o.f);
    o.clock.tick(5000);
    o.server.respond();

    // get inexistent attachment (document exists)
    o.addFakeServerResponse("GET", "get3/getx", 404, "HTML RESPONSE");
    o.spy(o, "status", 404, "Get non existing attachment (doc exists)");
    o.jio.get("get3/getx", o.f);
    o.clock.tick(5000);
    o.server.respond();

    // get attachment
    o.answer = JSON.stringify({"_id": "get4", "title": "some attachment"});
    o.addFakeServerResponse("GET", "get3/get4", 200, o.answer);
    o.spy(o, "value", {"_id": "get4", "title": "some attachment"},
      "Get attachment");
    o.jio.get("get3/get4", o.f);
    o.clock.tick(5000);
    o.server.respond();

    o.jio.stop();

    // do the same tests live webDav-Server/simulate CORS
    // check for credentials in sinon
});

test ("Remove", function(){

    var o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "dav",
        "username": "davremove",
        "password": "checkpwd",
        "url": "https://ca-davstorage:8080"
    });

    // remove inexistent document
    o.addFakeServerResponse("GET", "remove1", 404, "HTML RESPONSE");
    o.spy(o, "status", 404, "Remove non existening document");
    o.jio.remove({"_id": "remove1"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    // remove inexistent document/attachment
    o.addFakeServerResponse("GET", "remove1/remove2", 404, "HTML RESPONSE");
    o.spy(o, "status", 404, "Remove inexistent document/attachment");
    o.jio.remove({"_id": "remove1/remove2"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    // remove document
    o.answer = JSON.stringify({"_id": "remove3", "title": "some doc"});
    o.addFakeServerResponse("GET", "remove3", 200, o.answer);
    o.addFakeServerResponse("REMOVE", "remove3", 200, "HTML RESPONSE");
    o.spy(o, "value", {"ok": true, "id": "remove3"}, "Remove document");
    o.jio.remove({"_id": "remove3"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    o.answer = JSON.stringify({
      "_id": "remove4",
      "title": "some doc",
      "_attachments": {
            "remove5": {
                "length": 4,
                "digest": "md5-d41d8cd98f00b204e9800998ecf8427e"
            }
      }
    });
    // remove attachment
    o.addFakeServerResponse("GET", "remove4", 200, o.answer);
    o.addFakeServerResponse("PUT", "remove4", 201, "HTML RESPONSE");
    o.addFakeServerResponse("REMOVE", "remove4/remove5", 200, "HTML RESPONSE");
    o.spy(o, "value", {"ok": true, "id": "remove4/remove5"},
          "Remove attachment");
    o.jio.remove({"_id": "remove4/remove5"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    o.answer = JSON.stringify({
      "_id": "remove6",
      "title": "some other doc",
      "_attachments": {
            "remove7": {
                "length": 4,
                "digest": "md5-d41d8cd98f00b204e9800998ecf8427e"
            },
            "remove8": {
                "length": 4,
                "digest": "md5-e41d8cd98f00b204e9800998ecf8427e"
            },
            "remove9": {
                "length": 4,
                "digest": "md5-f41d8cd98f00b204e9800998ecf8427e"
            }
      }
    });
    // remove document with multiple attachments
    o.addFakeServerResponse("GET", "remove6", 200, o.answer);
    o.addFakeServerResponse("REMOVE", "remove6/remove7", 200, "HTML RESPONSE");
    o.addFakeServerResponse("REMOVE", "remove6/remove8", 200, "HTML RESPONSE");
    o.addFakeServerResponse("REMOVE", "remove6/remove9", 200, "HTML RESPONSE");
    o.addFakeServerResponse("REMOVE", "remove6", 200, "HTML RESPONSE");
    o.spy(o, "value", {"ok": true, "id": "remove6"},
          "Remove document with multiple attachments");
    o.jio.remove({"_id": "remove6"}, o.f);
    o.clock.tick(5000);
    o.server.respond();

    o.jio.stop();

    // do the same tests live webDav-Server/simulate CORS
    // check for credentials in sinon
});

test ("AllDocs", function () {

  // need to make server requests before activating fakeServer
  var davlist = getXML('responsexml/davlist'),
    o = generateTools(this);

    o.jio = JIO.newJio({
        "type": "dav",
        "username": "davall",
        "password": "checkpwd",
        "url": "https://ca-davstorage:8080"
    });

  // get allDocs, no content
  o.addFakeServerResponse("PROPFIND", "", 200, davlist);
  o.thisShouldBeTheAnswer = {
      "rows": [
        {"id": "alldocs1", "key": "alldocs1", "value": {}},
        {"id": "alldocs2", "key": "alldocs2", "value": {}}
      ],
      "total_rows": 2
  }
  o.spy(o, "value", o.thisShouldBeTheAnswer, "allDocs (no content)");
  o.jio.allDocs(o.f);
  o.clock.tick(5000);
  o.server.respond();

  // allDocs with option include
  o.all1 = JSON.stringify({"_id": "allDocs1", "title": "a doc title"});
  o.all2 = JSON.stringify({"_id": "allDocs2", "title": "another doc title"});
  o.addFakeServerResponse("GET", "alldocs1", 200, o.all1);
  o.addFakeServerResponse("GET", "alldocs2", 200, o.all2);
  o.spy(o, "value", o.thisShouldBeTheAnswer, "allDocs (include_docs)");
  o.jio.allDocs({"include_docs":true}, o.f);
  o.clock.tick(5000);
  o.server.respond();

  // do the same tests live webDav-Server/simulate CORS
  // check for credentials in sinon

  o.jio.stop();
});

/*
module ('Jio ReplicateStorage');

test ('Document load', function () {
    // Test if ReplicateStorage can load several documents.

    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick(base_tick);
    o.mytest = function (message,doc,doc2) {
        o.f = function (err,val) {
            var gooddoc = doc;
            if (val) {
                if (doc2 && val.content === doc2.content) {
                    gooddoc = doc2;
                }
            }
            deepEqual (err || val,gooddoc,message);
        };
        o.t.spy(o,'f');
        o.jio.get('file',{max_retry:3},o.f);
        o.clock.tick(10000);
        if (!o.f.calledOnce) {
            if (o.f.called) {
                ok(false, 'too much results');
            } else {
                ok(false, 'no response');
            }
        }
    };
    o.jio = JIO.newJio({type:'replicate',storagelist:[
        {type:'dummyallok',username:'1'},
        {type:'dummyallok',username:'2'}]});
    o.mytest('DummyStorageAllOK,OK: load same file',{
        _id:'file',content:'content',
        _last_modified:15000,
        _creation_date:10000
    });
    o.jio.stop();

    o.jio = JIO.newJio({type:'replicate',storagelist:[
        {type:'dummyall3tries'},
        {type:'dummyallok'}]});
    o.mytest('DummyStorageAllOK,3tries: load 2 different files',
             {
                 _id:'file',content:'content',
                 _last_modified:15000,_creation_date:10000
             },{
                 _id:'file',content:'content file',
                 _last_modified:17000,_creation_date:11000
             });
    o.jio.stop();
});

test ('Document save', function () {
    // Test if ReplicateStorage can save several documents.

    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick(base_tick);
    o.mytest = function (message,value) {
        o.f = function (err,val) {
            if (err) {
                err = err.status;
            }
            deepEqual (err || val,value,message);
        };
        o.t.spy(o,'f');
        o.jio.put({_id:'file',content:'content'},{max_retry:3},o.f);
        o.clock.tick(500);
        if (!o.f.calledOnce) {
            if (o.f.called) {
                ok(false, 'too much results');
            } else {
                ok(false, 'no response');
            }
        }
    };
    o.jio = JIO.newJio({type:'replicate',storagelist:[
        {type:'dummyallok',username:'1'},
        {type:'dummyallok',username:'2'}]});
    o.mytest('DummyStorageAllOK,OK: save a file.',{ok:true,id:'file'});
    o.jio.stop();

    o.jio = JIO.newJio({type:'replicate',storagelist:[
        {type:'dummyall3tries',username:'1'},
        {type:'dummyallok',username:'2'}]});
    o.mytest('DummyStorageAll3Tries,OK: save a file.',{ok:true,id:'file'});
    o.jio.stop();
});

test ('Get Document List', function () {
    // Test if ReplicateStorage can get several list.

    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick(base_tick);
    o.mytest = function (message,value) {
        o.f = function (err,val) {
            deepEqual (err || objectifyDocumentArray(val.rows),
                       objectifyDocumentArray(value),message);
        };
        o.t.spy(o,'f');
        o.jio.allDocs({max_retry:3},o.f);
        o.clock.tick(10000);
        if (!o.f.calledOnce) {
            if (o.f.called) {
                ok(false, 'too much results');
            } else {
                ok(false, 'no response');
            }
        }
    };
    o.jio = JIO.newJio({type:'replicate',storagelist:[
        {type:'dummyall3tries',username:'1'},
        {type:'dummyallok',username:'2'}]});
    o.doc1 = {id:'file',key:'file',value:{
              _last_modified:15000,_creation_date:10000}};
    o.doc2 = {id:'memo',key:'memo',value:{
              _last_modified:25000,_creation_date:20000}};
    o.mytest('DummyStorageAllOK,3tries: get document list.',
             [o.doc1,o.doc2]);
    o.jio.stop();

    o.jio = JIO.newJio({type:'replicate',storagelist:[
        {type:'dummyall3tries',username:'3'},
        {type:'dummyall3tries',username:'4'}]});
    o.mytest('DummyStorageAll3tries,3tries: get document list.',
             [o.doc1,o.doc2]);
    o.jio.stop();
});

test ('Remove document', function () {
    // Test if ReplicateStorage can remove several documents.

    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick(base_tick);
    o.mytest = function (message,value) {
        o.f = function (err,val) {
            if (err) {
                err = err.status;
            }
            deepEqual (err || val,value,message);
        };
        o.t.spy(o,'f');
        o.jio.remove({_id:'file'},{max_retry:3},o.f);
        o.clock.tick(10000);
        if (!o.f.calledOnce) {
            if (o.f.called) {
                ok(false, 'too much results');
            } else {
                ok(false, 'no response');
            }
        }
    };
    o.jio = JIO.newJio({type:'replicate',storagelist:[
        {type:'dummyallok',username:'1'},
        {type:'dummyall3tries',username:'2'}]});
    o.mytest('DummyStorageAllOK,3tries: remove document.',{ok:true,id:'file'});
    o.jio.stop();
});

module ('Jio IndexedStorage');

test ('Document load', function () {
    var o = {}; o.clock = this.sandbox.useFakeTimers();
    o.clock.tick(base_tick);
    o.jio = JIO.newJio({type:'indexed',storage:{type:'dummyall3tries'}});
    // loading must take long time with dummyall3tries
    o.f = this.spy();
    o.jio.get('memo',{max_retry:3,metadata_only:true},o.f);
    o.clock.tick(1000);
    ok(!o.f.called,'Callback must not be called');
    // wait long time too retreive list
    o.clock.tick(1000);

    // now we can test if the document metadata are loaded faster.
    o.doc = {_id:'memo',_last_modified:25000,_creation_date:20000};
    o.f2 = function (err,val) {
        deepEqual (err||val,o.doc,'Document metadata retrieved');
    };
    this.spy(o,'f2');
    o.jio.get('memo',{max_retry:3,metadata_only:true},o.f2);
    o.clock.tick(1000);
    if (!o.f2.calledOnce) {
        if (o.f2.called) {
            ok (false, 'too much results');
        } else {
            ok (false, 'no response');
        }
    }

    // test a simple document loading
    o.doc2 = {_id:'file',_last_modified:17000,
              _creation_date:11000,content:'content file'};
    o.f3 = function (err,val) {
        deepEqual (err||val,o.doc2,'Simple document loading');
    };
    this.spy(o,'f3');
    o.jio.get('file',{max_retry:3},o.f3);
    o.clock.tick(2000);
    if (!o.f3.calledOnce) {
        ok (false, 'no response / too much results');
    }
    o.jio.stop();
});

test ('Document save', function () {
    var o = {}; o.clock = this.sandbox.useFakeTimers();
    o.clock.tick(base_tick);
    o.jio = JIO.newJio({type:'indexed',
                        storage:{type:'dummyall3tries',
                                 username:'indexsave'}});
    o.f = function (err,val) {
        if (err) {
            err = err.status;
        }
        deepEqual (err || val,{ok:true,id:'file'},'document save');
    };
    this.spy(o,'f');
    o.jio.put({_id:'file',content:'content'},{max_retry:3},o.f);
    o.clock.tick(2000);
    if (!o.f.calledOnce){
        ok (false, 'no response / too much results');
    }
    o.jio.stop();
});

test ('Get document list', function () {
    var o = {}; o.clock = this.sandbox.useFakeTimers();
    o.clock.tick(base_tick);
    o.jio = JIO.newJio({type:'indexed',
                        storage:{type:'dummyall3tries',
                                 username:'indexgetlist'}});
    o.doc1 = {id:'file',key:'file',value:{
        _last_modified:15000,_creation_date:10000}};
    o.doc2 = {id:'memo',key:'memo',value:{
        _last_modified:25000,_creation_date:20000}};
    // getting list must take long time with dummyall3tries
    o.f = this.spy();
    o.jio.allDocs({max_retry:3},o.f);
    o.clock.tick(1000);
    ok(!o.f.called,'Callback must not be called');
    // wail long time too retreive list
    o.clock.tick(1000);
    // now we can test if the document list is loaded faster
    o.f2 = function (err,val) {
        deepEqual (err || objectifyDocumentArray(val.rows),
                   objectifyDocumentArray([o.doc1,o.doc2]),'get document list');
    };
    this.spy(o,'f2');
    o.jio.allDocs({max_retry:3},o.f2);
    o.clock.tick(1000)
    if (!o.f2.calledOnce) {
        ok (false, 'no response / too much results');
    }
});

test ('Remove document', function () {
    var o = {}; o.clock = this.sandbox.useFakeTimers();
    o.clock.tick(base_tick);
    o.sub_storage = {type:'dummyall3tries',username:'indexremove'}
    o.storage_file_object_name = 'jio/indexed_file_object/'+
        JSON.stringify (o.sub_storage);

    o.jio = JIO.newJio({type:'indexed',storage:o.sub_storage});
    o.f = function (err,val) {
        if (err) {
            err = err.status;
        }
        deepEqual (err || val,{ok:true,id:'file'},'document remove');
    };
    this.spy(o,'f');
    o.jio.remove({_id:'file'},{max_retry:3},o.f);
    o.clock.tick(2000);
    if (!o.f.calledOnce){
        ok (false, 'no response / too much results');
    }

    o.tmp = LocalOrCookieStorage.getItem(o.storage_file_object_name) || {};
    ok (!o.tmp.file,'File does not exists anymore');

    o.jio.stop();
});

module ('Jio CryptedStorage');

test ('Document save' , function () {
    var o = {}, clock = this.sandbox.useFakeTimers();
    clock.tick(base_tick);
    o.jio=JIO.newJio({type:'crypt',
                      username:'cryptsave',
                      password:'mypwd',
                      storage:{type:'local',
                               username:'cryptsavelocal',
                               application_name:'jiotests'}});
    o.f = function (err,val) {
        if (err) {
            err = err.status;
        }
        deepEqual (err || val,{ok:true,id:'testsave'},'save ok');
    };
    this.spy(o,'f');
    o.jio.put({_id:'testsave',content:'contentoftest'},o.f);
    clock.tick(1000);
    if (!o.f.calledOnce) {
        ok (false, 'no response / too much results');
    }
    // encrypt 'testsave' with 'cryptsave:mypwd' password
    o.tmp = LocalOrCookieStorage.getItem( // '/' = '%2F'
        'jio/local/cryptsavelocal/jiotests/rZx5PJxttlf9QpZER%2F5x354bfX54QFa1');
    if (o.tmp) {
        delete o.tmp._last_modified;
        delete o.tmp._creation_date;
    }
    deepEqual (o.tmp,
               {_id:'rZx5PJxttlf9QpZER/5x354bfX54QFa1',
                content:'upZkPIpitF3QMT/DU5jM3gP0SEbwo1n81rMOfLE'},
               'Check if the document is realy encrypted');
    o.jio.stop();
});

test ('Document load' , function () {
    var o = {}, clock = this.sandbox.useFakeTimers();
    clock.tick(base_tick);
    o.jio=JIO.newJio({type:'crypt',
                      username:'cryptload',
                      password:'mypwd',
                      storage:{type:'local',
                               username:'cryptloadlocal',
                               application_name:'jiotests'}});
    o.f = function (err,val) {
        deepEqual (err || val,{
            _id:'testload',content:'contentoftest',
            _last_modified:500,_creation_date:500},'load ok');
    };
    this.spy(o,'f');
    // encrypt 'testload' with 'cryptload:mypwd' password
    // and 'contentoftest' with 'cryptload:mypwd'
    o.doc = {
        _id:'hiG4H80pwkXCCrlLl1X0BD0BfWLZwDUX',
        content:'kSulH8Qo105dSKHcY2hEBXWXC9b+3PCEFSm1k7k',
        _last_modified:500,_creation_date:500};
    addFileToLocalStorage('cryptloadlocal','jiotests',o.doc);
    o.jio.get('testload',o.f);
    clock.tick(1000);
    if (!o.f.calledOnce) {
        ok (false, 'no response / too much results');
    }
    o.jio.stop();
});

test ('Get Document List', function () {
    var o = {}, clock = this.sandbox.useFakeTimers();
    clock.tick(base_tick);
    o.jio=JIO.newJio({type:'crypt',
                      username:'cryptgetlist',
                      password:'mypwd',
                      storage:{type:'local',
                               username:'cryptgetlistlocal',
                               application_name:'jiotests'}});
    o.f = function (err,val) {
        deepEqual (err || objectifyDocumentArray(val.rows),
                   objectifyDocumentArray(o.doc_list),'Getting list');
    };
    o.tick = function (tick) {
        clock.tick (tick || 1000);
        if (!o.f.calledOnce) {
            if (o.f.called) {
                ok (false, 'too much results');
            } else {
                ok (false, 'no response');
            }
        }
    };
    this.spy(o,'f');
    o.doc_list = [{
        id:'testgetlist1',key:'testgetlist1',value:{
            _last_modified:500,_creation_date:200}
    },{
        id:'testgetlist2',key:'testgetlist2',value:{
            _last_modified:300,_creation_date:300}
    }];
    o.doc_encrypt_list = [
        {_id:'541eX0WTMDw7rqIP7Ofxd1nXlPOtejxGnwOzMw',
         content:'/4dBPUdmLolLfUaDxPPrhjRPdA',
         _last_modified:500,_creation_date:200},
        {_id:'541eX0WTMDw7rqIMyJ5tx4YHWSyxJ5UjYvmtqw',
         content:'/4FBALhweuyjxxD53eFQDSm4VA',
         _last_modified:300,_creation_date:300}
    ];
    // encrypt with 'cryptgetlist:mypwd' as password
    LocalOrCookieStorage.setItem(
        'jio/local_file_name_array/cryptgetlistlocal/jiotests',
        [o.doc_encrypt_list[0]._id,o.doc_encrypt_list[1]._id]);
    LocalOrCookieStorage.setItem(
        'jio/local/cryptgetlistlocal/jiotests/'+o.doc_encrypt_list[0]._id,
        o.doc_encrypt_list[0]);
    LocalOrCookieStorage.setItem(
        'jio/local/cryptgetlistlocal/jiotests/'+o.doc_encrypt_list[1]._id,
        o.doc_encrypt_list[1]);
    o.jio.allDocs(o.f);
    o.tick(10000);

    o.jio.stop();
});

test ('Remove document', function () {
    var o = {}, clock = this.sandbox.useFakeTimers();
    clock.tick(base_tick);
    o.jio=JIO.newJio({type:'crypt',
                      username:'cryptremove',
                      password:'mypwd',
                      storage:{type:'local',
                               username:'cryptremovelocal',
                               application_name:'jiotests'}});
    o.f = function (err,val) {
        deepEqual (err || val,{ok:true,id:'file'},'Document remove');
    };
    this.spy(o,'f');
    // encrypt with 'cryptremove:mypwd' as password
    o.doc = {_id:'JqCLTjyxQqO9jwfxD/lyfGIX+qA',
             content:'LKaLZopWgML6IxERqoJ2mUyyO',
             _last_modified:500,_creation_date:500};
    o.jio.remove({_id:'file'},o.f);
    clock.tick(1000);
    if (!o.f.calledOnce){
        ok (false, 'no response / too much results');
    }
    o.jio.stop();
});


module ('Jio ConflictManagerStorage');

test ('Simple methods', function () {
    // Try all the simple methods like saving, loading, removing a document and
    // getting a list of document without testing conflicts

    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick(base_tick);
    o.spy = function(value,message) {
        o.f = function(err,val) {
            deepEqual (err || val,value,message);
        };
        o.t.spy(o,'f');
    };
    o.tick = function (tick) {
        o.clock.tick(tick || 1000);
        if (!o.f.calledOnce) {
            if (o.f.called) {
                ok(false, 'too much results');
            } else {
                ok(false, 'no response');
            }
        }
    };
    o.jio = JIO.newJio({type:'conflictmanager',
                        username:'methods',
                        storage:{type:'local',
                                 username:'conflictmethods',
                                 application_name:'jiotests'}});
    // PUT
    o.spy({ok:true,id:'file.doc',rev:'1'},'saving "file.doc".');
    o.jio.put({_id:'file.doc',content:'content1'},function (err,val) {
        if (val) {
            o.rev1 = val.rev;
            val.rev = val.rev.split('-')[0];
        }
        o.f (err,val);
    });
    o.tick();
    // PUT with options
    o.spy({ok:true,id:'file2.doc',rev:'1',
           conflicts:{total_rows:0,rows:[]},
           revisions:{start:1,ids:['1']},
           revs_info:[{rev:'1',status:'available'}]},
          'saving "file2.doc".');
    o.jio.put({_id:'file2.doc',content:'yes'},
              {revs:true,revs_info:true,conflicts:true},
              function (err,val) {
                  if (val) {
                      o.rev2 = val.rev;
                      val.rev = val.rev.split('-')[0];
                      if (val.revs_info) {
                          if (val.revisions) {
                              makeRevsAccordingToRevsInfo(
                                  val.revisions,val.revs_info);
                          }
                          val.revs_info[0].rev =
                              val.revs_info[0].rev.split('-')[0];
                      }
                 }
                  o.f (err,val);
              });
    o.tick();

    // GET
    o.get_callback = function (err,val) {
        if (val) {
            val._rev = (val._rev?val._rev.split('-')[0]:'/');
            val._creation_date = (val._creation_date?true:undefined);
            val._last_modified = (val._last_modified?true:undefined);
        }
        o.f(err,val);
    };
    o.spy({_id:'file.doc',content:'content1',_rev:'1',
           _creation_date:true,_last_modified:true},'loading "file.doc".');
    o.jio.get('file.doc',o.get_callback);
    o.tick();
    // GET with options
    o.get_callback = function (err,val) {
        if (val) {
            val._rev = (val._rev?val._rev.split('-')[0]:'/');
            val._creation_date = (val._creation_date?true:undefined);
            val._last_modified = (val._last_modified?true:undefined);
            if (val._revs_info) {
                if (val._revisions) {
                    makeRevsAccordingToRevsInfo(
                        val._revisions,val._revs_info);
                }
                val._revs_info[0].rev =
                    val._revs_info[0].rev.split('-')[0];
            }
        }
        o.f(err,val);
    };
    o.spy({_id:'file2.doc',content:'yes',_rev:'1',
           _creation_date:true,_last_modified:true,
           _conflicts:{total_rows:0,rows:[]},
           _revisions:{start:1,ids:['1']},
           _revs_info:[{rev:'1',status:'available'}]},
          'loading "file2.doc".');
    o.jio.get('file2.doc',{revs:true,revs_info:true,conflicts:true},
              o.get_callback);
    o.tick();

    // allDocs
    o.spy({total_rows:2,rows:[{
        id:'file.doc',key:'file.doc',
        value:{_rev:'1',_creation_date:true,_last_modified:true}
    },{
        id:'file2.doc',key:'file2.doc',
        value:{_rev:'1',_creation_date:true,_last_modified:true}
    }]},'getting list.');
    o.jio.allDocs(function (err,val) {
        if (val) {
            var i;
            for (i = 0; i < val.total_rows; i+= 1) {
                val.rows[i].value._creation_date =
                    val.rows[i].value._creation_date?
                    true:undefined;
                val.rows[i].value._last_modified =
                    val.rows[i].value._last_modified?
                    true:undefined;
                val.rows[i].value._rev = val.rows[i].value._rev.split('-')[0];
            }
            // because the result can be disordered
            if (val.total_rows === 2 && val.rows[0].id === 'file2.doc') {
                var tmp = val.rows[0];
                val.rows[0] = val.rows[1];
                val.rows[1] = tmp;
            }
        }
        o.f(err,val);
    });
    o.tick();

    // remove
    o.spy({ok:true,id:'file.doc',rev:'2'},
          'removing "file.doc"');
    o.jio.remove({_id:'file.doc'},{rev:o.rev1},function (err,val) {
        if (val) {
            val.rev = val.rev?val.rev.split('-')[0]:undefined;
        }
        o.f(err,val);
    });
    o.tick();
    // remove with options
    o.spy({
        ok:true,id:'file2.doc',rev:'2',
        conflicts:{total_rows:0,rows:[]},
        revisions:{start:2,ids:['2',getHashFromRev(o.rev2)]},
        revs_info:[{rev:'2',status:'deleted'}]
    },'removing "file2.doc"');
    o.jio.remove(
        {_id:'file2.doc'},
        {rev:o.rev2,conflicts:true,revs:true,revs_info:true},
        function (err,val) {
            if (val) {
                val.rev = val.rev?val.rev.split('-')[0]:undefined;
                if (val.revs_info) {
                    if (val.revisions) {
                        makeRevsAccordingToRevsInfo(
                            val.revisions,val.revs_info);
                    }
                    val.revs_info[0].rev =
                        val.revs_info[0].rev.split('-')[0];
                }
            }
            o.f(err,val);
        });
    o.tick();

    o.spy(404,'loading document fail.');
    o.jio.get('file.doc',function (err,val) {
        if (err) {
            err = err.status;
        }
        o.f(err,val);
    });
    o.tick();

    o.jio.stop();
});

test ('Revision Conflict', function() {
    // Try to tests all revision conflict possibility

    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick (base_tick);
    o.spy = basic_spy_function;
    o.tick = basic_tick_function;

    o.localNamespace = 'jio/local/revisionconflict/jiotests/';
    o.rev={};
    o.checkContent = function (string,message) {
        ok (LocalOrCookieStorage.getItem(o.localNamespace + string),
            message || '"' + string + '" is saved.');
    };
    o.checkNoContent = function (string,message) {
        ok (!LocalOrCookieStorage.getItem(o.localNamespace + string),
            message || '"' + string + '" does not exists.');
    };
    o.sub_storage_spec = {type:'local',
                            username:'revisionconflict',
                            application_name:'jiotests'}
    //////////////////////////////////////////////////////////////////////
    o.jio = JIO.newJio({type:'conflictmanager',
                        storage:o.sub_storage_spec});
    // create a new file
    o.spy(o,'value',
          {ok:true,id:'file.doc',rev:'1',conflicts:{total_rows:0,rows:[]},
           revs_info:[{rev:'1',status:'available'}],
           revisions:{start:1,ids:['1']}},
          'new file "file.doc".');
    o.jio.put(
        {_id:'file.doc',content:'content1'},
        {revs:true,revs_info:true,conflicts:true},
        function (err,val) {
            if (val) {
                o.rev.first = val.rev;
                val.rev = val.rev?val.rev.split('-')[0]:undefined;
                if (val.revs_info) {
                    if (val.revisions) {
                        makeRevsAccordingToRevsInfo(
                            val.revisions,val.revs_info);
                    }
                    val.revs_info[0].rev =
                        val.revs_info[0].rev.split('-')[0];
                }
            }
            o.f(err,val);
        }
    );
    o.tick(o);
    o.checkContent('file.doc.'+o.rev.first);
    // modify the file
    o.spy(o,'value',
          {ok:true,id:'file.doc',rev:'2',
           conflicts:{total_rows:0,rows:[]},
           revisions:{start:2,ids:['2',getHashFromRev(o.rev.first)]},
           revs_info:[{rev:'2',status:'available'}]},
          'modify "file.doc", revision: "'+
          o.rev.first+'".');
    o.jio.put(
        {_id:'file.doc',content:'content2',_rev:o.rev.first},
        {revs:true,revs_info:true,conflicts:true},
        function (err,val) {
            if (val) {
                o.rev.second = val.rev;
                val.rev = val.rev?val.rev.split('-')[0]:undefined;
                if (val.revs_info) {
                    if (val.revisions) {
                        makeRevsAccordingToRevsInfo(
                            val.revisions,val.revs_info);
                    }
                    val.revs_info[0].rev =
                        val.revs_info[0].rev.split('-')[0];
                }
            }
            o.f(err,val);
        }
    );
    o.tick(o);
    o.checkContent('file.doc.'+o.rev.second);
    o.checkNoContent('file.doc.'+o.rev.first);
    // modify the file from the second revision instead of the third
    o.test_message = 'modify "file.doc", revision: "'+
        o.rev.first+'" -> conflict!';
    o.f = o.t.spy();
    o.jio.put(
        {_id:'file.doc',content:'content3',_rev:o.rev.first},
        {revs:true,revs_info:true,conflicts:true},function (err,val) {
            o.f();
            var k;
            if (err) {
                o.rev.third = err.rev;
                err.rev = checkRev(err.rev);
                if (err.conflicts && err.conflicts.rows) {
                    o.tmp = err.conflicts;
                    o.solveConflict = checkConflictRow (err.conflicts.rows[0]);
                }
                for (k in {'error':0,'message':0,'reason':0,'statusText':0}) {
                    if (err[k]) {
                        delete err[k];
                    } else {
                        err[k] = 'ERROR: ' + k + ' is missing !';
                    }
                }
            }
            deepEqual(err||val,{
                rev:o.rev.third,
                conflicts:{total_rows:1,rows:[
                    {id:'file.doc',key:[o.rev.second,o.rev.third],
                     value:{_solveConflict:'function'}}]},
                status:409,
                // just one revision in the history, it does not keep older
                // revisions because it is not a revision manager storage.
                revisions:{start:1,ids:[getHashFromRev(o.rev.third)]},
                revs_info:[{rev:o.rev.second,status:'available'},
                           {rev:o.rev.third,status:'available'}]
            },o.test_message);
            ok (!revs_infoContains(err.revs_info,o.rev.first),
                'check if the first revision is not include to '+
                'the conflict list.');
            ok (revs_infoContains(err.revs_info,err.rev),
                'check if the new revision is include to '+
                'the conflict list.');
        });
    o.tick(o);
    o.checkContent ('file.doc.'+o.rev.third);
    // loading test
    o.spy(o,'value',{_id:'file.doc',_rev:o.rev.third,content:'content3',
                     _conflicts:o.tmp},
          'loading "file.doc" -> conflict!');
    o.jio.get('file.doc',{conflicts:true},function (err,val) {
        var k;
        if (val) {
            if (val._conflicts && val._conflicts.rows) {
                checkConflictRow (val._conflicts.rows[0]);
            }
            for (k in {'_creation_date':0,'_last_modified':0}) {
                if (val[k]) {
                    delete val[k];
                } else {
                    val[k] = 'ERROR: ' + k + ' is missing !';
                }
            }
        }
        o.f(err,val);
    });
    o.tick(o);
    if (!o.solveConflict) { return ok(false,'Cannot to continue the tests'); }
    // solving conflict
    o.spy(o,'value',{ok:true,id:'file.doc',rev:'3'},
          'solve conflict "file.doc".');
    o.solveConflict(
        'content4',function (err,val) {
            if (val) {
                o.rev.forth = val.rev;
                val.rev = val.rev?val.rev.split('-')[0]:undefined;
            }
            o.f(err,val);
        });
    o.tick(o);
    o.checkContent('file.doc.'+o.rev.forth);
    o.checkNoContent('file.doc.'+o.rev.second);
    o.checkNoContent('file.doc.'+o.rev.third);
    o.jio.stop();
});

test ('Conflict in a conflict solving', function () {
    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick (base_tick);
    o.spy = basic_spy_function;
    o.tick = basic_tick_function;

    o.localNamespace = 'jio/local/conflictconflict/jiotests/';
    o.rev={};
    o.checkContent = function (string,message) {
        ok (LocalOrCookieStorage.getItem(o.localNamespace + string),
            message || '"' + string + '" is saved.');
    };
    o.checkNoContent = function (string,message) {
        ok (!LocalOrCookieStorage.getItem(o.localNamespace + string),
            message || '"' + string + '" does not exists.');
    };
    o.sub_storage_spec = {type:'local',
                            username:'conflictconflict',
                            application_name:'jiotests'}
    //////////////////////////////////////////////////////////////////////
    o.jio = JIO.newJio({type:'conflictmanager',
                        storage:o.sub_storage_spec});
    // create a new file
    o.test_message = 'new file "file.doc", revision: "0".'
    o.f = o.t.spy();
    o.jio.put(
        {_id:'file.doc',content:'content1'},
        {conflicts:true,revs:true,revs_info:true},
        function(err,val) {
            o.f();
            if (val) {
                o.rev.first = val.rev;
                val.rev = checkRev(val.rev);
            }
            deepEqual(err||val,{
                ok:true,id:'file.doc',rev:o.rev.first,
                conflicts:{total_rows:0,rows:[]},
                revisions:{start:1,ids:[getHashFromRev(o.rev.first)]},
                revs_info:[{rev:o.rev.first,status:'available'}]
            },o.test_message);
        });
    o.tick(o);
    o.checkContent ('file.doc.'+o.rev.first);
    // modify the file from the second revision instead of the third
    o.test_message = 'modify "file.doc", revision: "0" -> conflict!';
    o.f = o.t.spy();
    o.jio.put(
        {_id:'file.doc',content:'content2'},
        {conflicts:true,revs:true,revs_info:true},
        function (err,val) {
        o.f();
        var k;
        if (err) {
            o.rev.second = err.rev;
            err.rev = checkRev(err.rev);
            if (err.conflicts && err.conflicts.rows) {
                o.solveConflict = checkConflictRow (err.conflicts.rows[0]);
            }
            for (k in {'error':0,'message':0,'reason':0,'statusText':0}) {
                if (err[k]) {
                    delete err[k];
                } else {
                    err[k] = 'ERROR: ' + k + ' is missing !';
                }
            }
        }
        deepEqual(err||val,{
            rev:o.rev.second,
            conflicts:{total_rows:1,rows:[
                {id:'file.doc',key:[o.rev.first,o.rev.second],
                 value:{_solveConflict:'function'}}]},
            status:409,
            // just one revision in the history, it does not keep older
            // revisions because it is not a revision manager storage.
            revisions:{start:1,ids:[getHashFromRev(o.rev.second)]},
            revs_info:[{rev:o.rev.first,status:'available'},
                       {rev:o.rev.second,status:'available'}]
        },o.test_message);
    });
    o.tick(o);
    o.checkContent ('file.doc.'+o.rev.second);
    if (!o.solveConflict) { return ok(false,'Cannot to continue the tests'); }
    // saving another time
    o.test_message = 'modify "file.doc" when solving, revision: "'+
        o.rev.first+'" -> conflict!';
    o.f = o.t.spy();
    o.jio.put(
        {_id:'file.doc',content:'content3',_rev:o.rev.first},
        {conflicts:true,revs:true,revs_info:true},
        function(err,val){
            o.f();
            if (err) {
                o.rev.third = err.rev;
                err.rev = checkRev(err.rev);
                if (err.conflicts && err.conflicts.rows) {
                    checkConflictRow (err.conflicts.rows[0]);
                }
                for (k in {'error':0,'message':0,'reason':0,'statusText':0}) {
                    if (err[k]) {
                        delete err[k];
                    } else {
                        err[k] = 'ERROR: ' + k + ' is missing !';
                    }
                }
            }
            deepEqual(err||val,{
                rev:o.rev.third,
                conflicts:{total_rows:1,rows:[
                    {id:'file.doc',key:[o.rev.second,o.rev.third],
                     value:{_solveConflict:'function'}}]},
                status:409,
                // just one revision in the history, it does not keep older
                // revisions because it is not a revision manager storage.
                revisions:{start:2,ids:[getHashFromRev(o.rev.third),
                                        getHashFromRev(o.rev.first)]},
                revs_info:[{rev:o.rev.second,status:'available'},
                           {rev:o.rev.third,status:'available'}]
            },o.test_message);
        });
    o.tick(o);
    o.checkContent ('file.doc.'+o.rev.third);
    o.checkNoContent ('file.doc.'+o.rev.first);
    // solving first conflict
    o.test_message = 'solving conflict "file.doc" -> conflict!';
    o.f = o.t.spy();
    o.solveConflict(
        'content4',{conflicts:true,revs:true,revs_info:true},
        function (err,val) {
            o.f();
            if (err) {
                o.rev.forth = err.rev;
                err.rev = checkRev(err.rev);
                if (err.conflicts && err.conflicts.rows) {
                    o.solveConflict = checkConflictRow (err.conflicts.rows[0]);
                }
                for (k in {'error':0,'message':0,'reason':0,'statusText':0}) {
                    if (err[k]) {
                        delete err[k];
                    } else {
                        err[k] = 'ERROR: ' + k + ' is missing !';
                    }
                }
            }
            deepEqual(err||val,{
                rev:o.rev.forth,
                conflicts:{total_rows:1,rows:[
                    {id:'file.doc',key:[o.rev.third,o.rev.forth],
                     value:{_solveConflict:'function'}}]},
                status:409,
                // just one revision in the history, it does not keep older
                // revisions because it is not a revision manager storage.
                revisions:{start:2,ids:[getHashFromRev(o.rev.forth),
                                        getHashFromRev(o.rev.second)]},
                revs_info:[{rev:o.rev.third,status:'available'},
                           {rev:o.rev.forth,status:'available'}]
            },o.test_message);
        });
    o.tick(o);
    o.checkContent ('file.doc.'+o.rev.forth);
    o.checkNoContent ('file.doc.'+o.rev.second);
    if (!o.solveConflict) { return ok(false,'Cannot to continue the tests'); }
    // solving last conflict
    o.test_message = 'solving last conflict "file.doc".';
    o.f = o.t.spy();
    o.solveConflict(
        'content5',{conflicts:true,revs:true,revs_info:true},
        function (err,val) {
            if (val) {
                o.rev.fifth = val.rev;
                val.rev = checkRev(val.rev);
            }
            deepEqual(err||val,{
                ok:true,id:'file.doc',rev:o.rev.fifth,
                conflicts:{total_rows:0,rows:[]},
                revisions:{start:3,ids:[getHashFromRev(o.rev.fifth),
                                        getHashFromRev(o.rev.forth),
                                        getHashFromRev(o.rev.second)]},
                revs_info:[{rev:o.rev.fifth,status:'available'}]
            },o.test_message);
            o.f();
        });
    o.tick(o);
    o.checkContent ('file.doc.'+o.rev.fifth);

    o.jio.stop();
});

test ('Remove revision conflict', function () {
    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick (base_tick);
    o.spy = basic_spy_function;
    o.tick = basic_tick_function;

    o.localNamespace = 'jio/local/removeconflict/jiotests/';
    o.rev={};
    o.checkContent = function (string,message) {
        ok (LocalOrCookieStorage.getItem(o.localNamespace + string),
            message || '"' + string + '" is saved.');
    };
    o.checkNoContent = function (string,message) {
        ok (!LocalOrCookieStorage.getItem(o.localNamespace + string),
            message || '"' + string + '" does not exists.');
    };
    o.sub_storage_spec = {type:'local',
                            username:'removeconflict',
                            application_name:'jiotests'}
    //////////////////////////////////////////////////////////////////////
    o.jio = JIO.newJio({type:'conflictmanager',
                        storage:o.sub_storage_spec});

    o.test_message = 'new file "file.doc", revision: "0".';
    o.f = o.t.spy();
    o.jio.put(
        {_id:'file.doc',content:'content1'},
        {conflicts:true,revs:true,revs_info:true},
        function(err,val) {
            o.f();
            if (val) {
                o.rev.first = val.rev;
                val.rev = checkRev(val.rev);
            }
            deepEqual(err||val,{
                ok:true,id:'file.doc',rev:o.rev.first,
                conflicts:{total_rows:0,rows:[]},
                revisions:{start:1,ids:[getHashFromRev(o.rev.first)]},
                revs_info:[{rev:o.rev.first,status:'available'}]
            },o.test_message);
        });
    o.tick(o);
    o.checkContent ('file.doc.'+o.rev.first);

    o.test_message = 'remove "file.doc", revision: "wrong" -> conflict!';
    o.f = o.t.spy();
    o.jio.remove(
        {_id:'file.doc'},
        {conflicts:true,revs:true,revs_info:true,rev:'wrong'},
        function (err,val) {
            o.f();
            if (err) {
                o.rev.second = err.rev;
                err.rev = checkRev(err.rev);
                if (err.conflicts && err.conflicts.rows) {
                    o.solveConflict = checkConflictRow (err.conflicts.rows[0]);
                }
                for (k in {'error':0,'message':0,'reason':0,'statusText':0}) {
                    if (err[k]) {
                        delete err[k];
                    } else {
                        err[k] = 'ERROR: ' + k + ' is missing !';
                    }
                }
            }
            deepEqual(err||val,{
                rev:o.rev.second,
                conflicts:{total_rows:1,rows:[
                    {id:'file.doc',key:[o.rev.first,o.rev.second],
                     value:{_solveConflict:'function'}}]},
                status:409,
                // just one revision in the history, it does not keep older
                // revisions because it is not a revision manager storage.
                revisions:{start:1,ids:[getHashFromRev(o.rev.second)]},
                revs_info:[{rev:o.rev.first,status:'available'},
                           {rev:o.rev.second,status:'deleted'}]
            },o.test_message);
        });
    o.tick(o);

    o.test_message = 'new file again "file.doc".';
    o.f = o.t.spy();
    o.jio.put(
        {_id:'file.doc',content:'content2'},
        {conflicts:true,revs:true,revs_info:true},
        function (err,val) {
            o.f();
            if (err) {
                o.rev.third = err.rev;
                err.rev = checkRev(err.rev);
                if (err.conflicts && err.conflicts.rows) {
                    o.solveConflict = checkConflictRow (err.conflicts.rows[0]);
                }
                for (k in {'error':0,'message':0,'reason':0,'statusText':0}) {
                    if (err[k]) {
                        delete err[k];
                    } else {
                        err[k] = 'ERROR: ' + k + ' is missing !';
                    }
                }
            }
            deepEqual(err||val,{
                rev:o.rev.third,
                conflicts:{total_rows:1,rows:[
                    {id:'file.doc',key:[o.rev.first,o.rev.second,o.rev.third],
                     value:{_solveConflict:'function'}}]},
                status:409,
                // just one revision in the history, it does not keep older
                // revisions because it is not a revision manager storage.
                revisions:{start:1,ids:[getHashFromRev(o.rev.third)]},
                revs_info:[{rev:o.rev.first,status:'available'},
                           {rev:o.rev.second,status:'deleted'},
                           {rev:o.rev.third,status:'available'}]
            },o.test_message);
        });
    o.tick(o);
    o.checkContent ('file.doc.'+o.rev.third);

    o.test_message = 'remove "file.doc", revision: "'+o.rev.first+
        '" -> conflict!'
    o.f = o.t.spy();
    o.jio.remove(
        {_id:'file.doc'},
        {conflicts:true,revs:true,revs_info:true,rev:o.rev.first},
        function (err,val) {
            o.f();
            if (err) {
                o.rev.forth = err.rev;
                err.rev = checkRev(err.rev);
                if (err.conflicts && err.conflicts.rows) {
                    o.solveConflict = checkConflictRow (err.conflicts.rows[0]);
                }
                for (k in {'error':0,'message':0,'reason':0,'statusText':0}) {
                    if (err[k]) {
                        delete err[k];
                    } else {
                        err[k] = 'ERROR: ' + k + ' is missing !';
                    }
                }
            }
            deepEqual(err||val,{
                rev:o.rev.forth,
                conflicts:{total_rows:1,rows:[
                    {id:'file.doc',key:[o.rev.second,o.rev.third,o.rev.forth],
                     value:{_solveConflict:'function'}}]},
                status:409,
                // just one revision in the history, it does not keep older
                // revisions because it is not a revision manager storage.
                revisions:{start:2,ids:[getHashFromRev(o.rev.forth),
                                        getHashFromRev(o.rev.first)]},
                revs_info:[{rev:o.rev.second,status:'deleted'},
                           {rev:o.rev.third,status:'available'},
                           {rev:o.rev.forth,status:'deleted'}]
            },o.test_message);
        });
    o.tick(o);
    o.checkNoContent ('file.doc.'+o.rev.first);
    o.checkNoContent ('file.doc.'+o.rev.forth);

    if (!o.solveConflict) { return ok(false, 'Cannot continue the tests'); }
    o.test_message = 'solve "file.doc"';
    o.f = o.t.spy();
    o.solveConflict({conflicts:true,revs:true,revs_info:true},function(err,val){
        o.f();
        if (val) {
            o.rev.fifth = val.rev;
            val.rev = checkRev(val.rev);
        }
        deepEqual(err||val,{
            ok:true,id:'file.doc',rev:o.rev.fifth,
            conflicts:{total_rows:0,rows:[]},
            revisions:{start:3,ids:[getHashFromRev(o.rev.fifth),
                                    getHashFromRev(o.rev.forth),
                                    getHashFromRev(o.rev.first)]},
            revs_info:[{rev:o.rev.fifth,status:'deleted'}]
        },o.test_message);
    });
    o.tick(o);
    o.checkNoContent ('file.doc.'+o.rev.second);
    o.checkNoContent ('file.doc.'+o.rev.forth);
    o.checkNoContent ('file.doc.'+o.rev.fifth);

    o.test_message = 'save "file3.doc"';
    o.f = o.t.spy();
    o.jio.put(
        {_id:'file3.doc',content:'content3'},
        function(err,val) {
            o.f();
            if (val) {
                o.rev.sixth = val.rev;
                val.rev = checkRev(val.rev);
            }
            deepEqual(err||val,{
                ok:true,id:'file3.doc',rev:o.rev.sixth
            },o.test_message);
        });
    o.tick(o);
    o.test_message = 'save "file3.doc", rev "'+o.rev.sixth+'"';
    o.f = o.t.spy();
    o.jio.put(
        {_id:'file3.doc',content:'content3',_rev:o.rev.sixth},
        function(err,val) {
            o.f();
            if (val) {
                o.rev.seventh = val.rev;
                val.rev = checkRev(val.rev);
            }
            deepEqual(err||val,{
                ok:true,id:'file3.doc',rev:o.rev.seventh
            },o.test_message);
        });
    o.tick(o);

    o.test_message = 'remove last "file3.doc"';
    o.f = o.t.spy();
    o.jio.remove(
        {_id:'file3.doc'},
        {conflicts:true,revs:true,revs_info:true,rev:'last'},
        function (err,val) {
            o.f();
            if (val) {
                o.rev.eighth = val.rev;
                val.rev = checkRev(val.rev);
            }
            deepEqual(err||val,{
                ok:true,id:'file3.doc',
                rev:o.rev.eighth,
                conflicts:{total_rows:0,rows:[]},
                // just one revision in the history, it does not keep older
                // revisions because it is not a revision manager storage.
                revisions:{start:3,ids:[getHashFromRev(o.rev.eighth),
                                        getHashFromRev(o.rev.seventh),
                                        getHashFromRev(o.rev.sixth)]},
                revs_info:[{rev:o.rev.eighth,status:'deleted'}]
            },o.test_message);
        });
    o.tick(o);

    o.jio.stop();
});

test ('Load Revisions', function () {
    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick (base_tick);
    o.spy = basic_spy_function;
    o.tick = basic_tick_function;
    o.sub_storage_spec = {type:'local',
                            username:'loadrevisions',
                            application_name:'jiotests'}
    //////////////////////////////////////////////////////////////////////
    o.jio = JIO.newJio({type:'conflictmanager',
                        storage:o.sub_storage_spec});
    o.spy(o,'status',404,'load file rev:1,','f'); // 12 === Replaced
    o.spy(o,'status',404,'load file rev:2','g');
    o.spy(o,'status',404,'and load file rev:3 at the same time','h');
    o.jio.get('file',{rev:'1'},o.f);
    o.jio.get('file',{rev:'2'},o.g);
    o.jio.get('file',{rev:'3'},o.h);
    o.tick(o,1000,'f'); o.tick(o,0,'g'); o.tick(o,0,'h');
    o.jio.stop();
});

test ('Get revision List', function () {
    var o = {}; o.clock = this.sandbox.useFakeTimers(); o.t = this;
    o.clock.tick (base_tick);
    o.spy = basic_spy_function;
    o.tick = basic_tick_function;
    o.sub_storage_spec = {type:'local',
                            username:'getrevisionlist',
                            application_name:'jiotests'}
    o.rev = {};
    //////////////////////////////////////////////////////////////////////
    o.jio = JIO.newJio({type:'conflictmanager',
                        storage:o.sub_storage_spec});
    o.spy(o,'value',{total_rows:0,rows:[]},'Get revision list');
    o.jio.allDocs(o.f);
    o.tick(o);

    o.spy(o,'value',{total_rows:0,rows:[],conflicts:{total_rows:0,rows:[]}},
          'Get revision list with informations');
    o.jio.allDocs({conflicts:true,revs:true,info_revs:true},o.f);
    o.tick(o);

    o.spy(o,'jobstatus','done','saving file');
    o.jio.put({_id:'file',content:'content file'},function (err,val) {
        o.rev.file1 = val?val.rev:undefined;
        o.f(err,val);
    });
    o.tick(o);
    o.spy(o,'jobstatus','done','saving memo');
    o.jio.put({_id:'memo',content:'content memo'},function (err,val) {
        o.rev.memo1 = val?val.rev:undefined;
        o.f(err,val);
    });
    o.tick(o);
    o.spy(o,'status',409,'saving memo conflict');
    o.jio.put({_id:'memo',content:'content memo'},function (err,val) {
        o.rev.memo2 = err?err.rev:undefined;
        o.f(err,val);
    });
    o.tick(o);

    o.f = o.t.spy();
    o.jio.allDocs(function (err,val) {
        var i;
        if (val) {
            for (i = 0; i < val.total_rows; i+= 1) {
                val.rows[i].value._creation_date =
                    val.rows[i].value._creation_date?true:undefined;
                val.rows[i].value._last_modified =
                    val.rows[i].value._last_modified?true:undefined;
                o.rev[i] = checkRev (val.rows[i].value._rev);
            }
        }
        deepEqual(err||val,{total_rows:2,rows:[{
            id:'file',key:'file',value:{
                _creation_date:true,_last_modified:true,_rev:o.rev[0]
            }
        },{
            id:'memo',key:'memo',value:{
                _creation_date:true,_last_modified:true,_rev:o.rev[1]
            }
        }]},'Get revision list after adding 2 files');
        o.f();
    });
    o.tick(o);

    o.f = o.t.spy();
    o.jio.allDocs(
        {conflicts:true,revs:true,revs_info:true},
        function (err,val) {
            var i;
            if (val) {
                for (i = 0; i < val.total_rows; i+= 1) {
                    val.rows[i].value._creation_date =
                        val.rows[i].value._creation_date?true:undefined;
                    val.rows[i].value._last_modified =
                        val.rows[i].value._last_modified?true:undefined;
                    if (val.conflicts && val.conflicts.rows) {
                        o.solveConflict =
                            checkConflictRow (val.conflicts.rows[0]);
                    }
                }
            }
            deepEqual(err||val,{
                total_rows:2,rows:[{
                    id:'file',key:'file',value:{
                        _creation_date:true,_last_modified:true,
                        _revisions:{start:1,ids:[getHashFromRev(o.rev.file1)]},
                        _rev:o.rev.file1,_revs_info:[{
                            rev:o.rev.file1,status:'available'
                        }]
                    }
                },{
                    id:'memo',key:'memo',value:{
                        _creation_date:true,_last_modified:true,
                        _revisions:{start:1,ids:[getHashFromRev(o.rev.memo2)]},
                        _rev:o.rev.memo2,_revs_info:[{
                            rev:o.rev.memo1,status:'available'
                        },{
                            rev:o.rev.memo2,status:'available'
                        }]
                    }
                }],
                conflicts:{total_rows:1,rows:[{
                    id:'memo',key:[o.rev.memo1,o.rev.memo2],
                    value:{_solveConflict:'function'}
                }]}
            },'Get revision list with informations after adding 2 files');
            o.f();
        });
    o.tick(o);

    o.jio.stop();
});
*/
};                              // end thisfun

if (window.requirejs) {
    require.config ({
        paths: {
            jiotestsloader: './jiotests.loader',

            jQueryAPI: '../lib/jquery/jquery',
            jQuery: '../js/jquery.requirejs_module',
            JIO: '../src/jio',
            Base64API: '../lib/base64/base64',
            Base64: '../js/base64.requirejs_module',
            JIODummyStorages: '../src/jio.dummystorages',
            JIOStorages: '../src/jio.storage',
            SJCLAPI:'../lib/sjcl/sjcl.min',
            SJCL:'../js/sjcl.requirejs_module'
        }
    });
    require(['jiotestsloader'],thisfun);
} else {
    thisfun ({JIO:jIO});
}

}());