'use strict';

var assert = require('assert');
var harness = require('./harness.js');

describe('#crud', function () {
  function allTests(H) {
    function testBadBasic(fn) {
      it('should fail with an invalid key type', function () {
        assert.throws(function () {
          fn({}, undefined, H.noCallback());
        }, TypeError);
      });
      it('should fail with an invalid hashkey option', function () {
        assert.throws(function () {
          fn(H.key(), {hashkey: {}}, H.noCallback());
        }, TypeError);
      });
      it('should fail with non object options', function () {
        assert.throws(function () {
          fn(H.key(), 'opts', H.noCallback());
        }, TypeError);
      });
      it('should fail if no callback is passed', function () {
        assert.throws(function () {
          fn(H.key(), {});
        }, TypeError);
      });
    }
    function testBadDura(fn) {
      it('should fail for negative persist_to values', function() {
        assert.throws(function() {
          fn(H.key(), {persist_to:-1}, H.noCallback());
        });
      });
      it('should fail for negative replicate_to values', function() {
        assert.throws(function() {
          fn(H.key(), {replicate_to:-1}, H.noCallback());
        });
      });
      it('should fail for very-high persist_to values', function() {
        assert.throws(function() {
          fn(H.key(), {persist_to:10}, H.noCallback());
        });
      });
      it('should fail for very-high replicate_to values', function() {
        assert.throws(function() {
          fn(H.key(), {replicate_to:10}, H.noCallback());
        });
      });
    }
    function testBadExpiry(fn) {
      it('should fail with negative expiry', function () {
        assert.throws(function () {
          fn(H.key(), -1, H.noCallback());
        }, TypeError);
      });
      it('should fail with null expiry', function () {
        assert.throws(function () {
          fn(H.key(), null, H.noCallback());
        }, TypeError);
      });
      it('should fail with non-numeric expiry', function () {
        assert.throws(function () {
          fn(H.key(), '1', H.noCallback());
        }, TypeError);
      });
    }

    function testBadCas(fn) {
      it('should fail with an invalid cas option', function () {
        assert.throws(function () {
          fn(H.key(), 1111, H.noCallback());
        }, Error);
      });
    }

    function testBadValue(fn) {
      it('should fail with an undefined value', function () {
        assert.throws(function () {
          fn(H.key(), undefined, H.noCallback());
        }, TypeError);
      });
    }

    describe('#store', function () {
      describe('upsert', function () {
        testBadBasic(function (key, options, callback) {
          H.b.upsert(key, 'bar', options, callback);
        });
        testBadExpiry(function (key, expiry, callback) {
          H.b.upsert(key, 'bar', {expiry: expiry}, callback);
        });
        testBadCas(function(key, cas, callback) {
          H.b.upsert(key, 'bar', {cas: cas}, callback);
        });
        testBadValue(function(key, value, callback) {
          H.b.upsert(key, value, {}, callback);
        });
        testBadDura(function(key, options, callback) {
          H.b.upsert(key, 'bar', options, callback);
        });
      });

      describe('insert', function () {
        testBadBasic(function (key, options, callback) {
          H.b.insert(key, 'bar', options, callback);
        });
        testBadExpiry(function (key, expiry, callback) {
          H.b.insert(key, 'bar', {expiry: expiry}, callback);
        });
        testBadCas(function(key, cas, callback) {
          H.b.insert(key, 'bar', {cas: cas}, callback);
        });
        testBadValue(function(key, value, callback) {
          H.b.insert(key, value, {}, callback);
        });
        testBadDura(function(key, options, callback) {
          H.b.insert(key, 'bar', options, callback);
        });
      });


    });

    describe('#retrieve', function () {
      describe('get', function () {
        testBadBasic(function (key, options, callback) {
          H.b.get(key, options, callback);
        });
      });
      describe('getMulti', function() {
        it('should fail with a non-array keys', function() {
          assert.throws(function() {
            H.b.getMulti('key', H.noCallback());
          }, TypeError);
        });
        it('should fail for undefined keys', function() {
          assert.throws(function() {
            H.b.getMulti(undefined, H.noCallback());
          }, TypeError);
        });
        it('should fail with an empty array of keys', function() {
          assert.throws(function() {
            H.b.getMulti([], H.noCallback());
          }, TypeError);
        });
        it('should fail with a missing callback', function() {
          assert.throws(function() {
            H.b.getMulti(['test1', 'test2']);
          }, TypeError);
        });
        it('should work normally', function(done) {
          var key1 = H.key();
          var key2 = H.key();
          H.b.insert(key1, 'foo', H.okCallback(function() {
            H.b.insert(key2, 'bar', H.okCallback(function() {
              H.b.getMulti([key1, key2], function(err, res) {
                assert(!err);
                assert(res);
                assert(res[key1]);
                assert(res[key1].cas);
                assert(res[key1].value);
                assert(!res[key1].error);
                assert(res[key2]);
                assert(res[key2].cas);
                assert(res[key2].value);
                assert(!res[key2].error);
                done();
              });
            }));
          }));
        });
        it('should work with partial failures', function(done) {
          var key1 = H.key();
          var key2 = H.key();
          H.b.insert(key1, 'foo', H.okCallback(function() {
            H.b.getMulti([key1, key2], function(err, res) {
              assert(err === 1);
              assert(res);
              assert(res[key1]);
              assert(res[key1].cas);
              assert(res[key1].value);
              assert(!res[key1].error);
              assert(res[key2]);
              assert(!res[key2].cas);
              assert(!res[key2].value);
              assert(res[key2].error);
              done();
            });
          }));
        });
      });
      describe('getAndLock', function () {
        testBadBasic(function (key, options, callback) {
          H.b.getAndLock(key, options, callback);
        });
        it('should fail with an invalid lockTime', function () {
          assert.throws(function () {
            H.b.getAndLock(H.key(), {lockTime: 0}, H.noCallback());
          }, TypeError);
        });
        it('should fail with a negative lockTime', function () {
          assert.throws(function () {
            H.b.getAndLock(H.key(), {lockTime: -1}, H.noCallback());
          }, TypeError);
        });
        it('should fail with an non-numeric lockTime', function () {
          assert.throws(function () {
            H.b.getAndLock(H.key(), {lockTime: '1'}, H.noCallback());
          }, TypeError);
        });
      });
      describe('getAndTouch', function () {
        testBadBasic(function (key, options, callback) {
          H.b.getAndTouch(key, 1, options, callback);
        });
        testBadExpiry(function (key, expiry, callback) {
          H.b.getAndTouch(key, expiry, {}, callback);
        });
        testBadDura(function(key, options, callback) {
          H.b.getAndTouch(key, 1, options, callback);
        });
      });
      describe('getReplica', function () {
        testBadBasic(function (key, options, callback) {
          H.b.getReplica(key, options, callback);
        });

        // This test just tries to invoke the function so that it appears to
        //  work in some way.  In reality, we can't consistently test this since
        //  we can't predetermine the cluster size.
        it('should not crash', function(done) {
          H.b.getReplica(H.key(), function () {
            done();
          });
        });
      });
    });

    describe('#misc', function () {
      describe('remove', function () {
        testBadBasic(function (key, options, callback) {
          H.b.remove(key, options, callback);
        });
        testBadCas(function(key, cas, callback) {
          H.b.remove(key, {cas: cas}, callback);
        });
        testBadDura(function(key, options, callback) {
          H.b.remove(key, options, callback);
        });

        it('should fail on missing key', function(done) {
          H.b.remove(H.key(), function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        });
        it('should fail on a locked key', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(){
            H.b.getAndLock(key, H.okCallback(function() {
              H.b.remove(key, function(err, res) {
                assert(err);
                assert(!res);
                done();
              });
            }));
          }));
        });
      });

      describe('unlock', function () {
        testBadBasic(function (key, options, callback) {
          H.b.unlock(key, {}, options, callback);
        });
        testBadCas(function(key, cas, callback) {
          H.b.unlock(key, cas, callback);
        });
        it('should fail on missing key', function(done) {
          H.b.unlock(H.key(), {}, function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        });
        it('should fail on an never been locked key', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(insertRes) {
            H.b.unlock(key, insertRes.cas, function(err, res) {
              assert(err);
              assert(!res);
              done();
            });
          }));
        });
        it('should fail on a timed out lock (slow)', function(done) {
          this.timeout(3000);
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(insertRes) {
            H.b.getAndLock(key, {lockTime:1}, H.okCallback(function(lockRes) {
              H.timeTravel(function() {
                H.b.unlock(key, lockRes.cas, function(err, res) {
                  assert(err);
                  assert(!res);
                  done();
                });
              }, 2000);
            }));
          }));
        });
        it('should fail for an incorrect cas', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(insertRes) {
            H.b.getAndLock(key, H.okCallback(function() {
              H.b.unlock(key, insertRes.cas, function(err, res) {
                assert(err);
                assert(!res);
                done();
              });
            }));
          }));
        });
        it('should actually unlock the key', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function() {
            H.b.getAndLock(key, H.okCallback(function(lockRes) {
              H.b.unlock(key, lockRes.cas, function() {
                H.b.replace(key, 'bar', H.okCallback(function() {
                  done();
                }));
              });
            }));
          }));
        });
      });

      describe('touch', function () {
        testBadBasic(function (key, options, callback) {
          H.b.touch(key, 1, options, callback);
        });
        testBadExpiry(function (key, expiry, callback) {
          H.b.touch(key, expiry, {}, callback);
        });
        testBadDura(function(key, options, callback) {
          H.b.touch(key, options, callback);
        });
      });

      describe('counter', function() {
        testBadBasic(function (key, options, callback) {
          H.b.counter(key, 1, options, callback);
        });
        testBadDura(function(key, options, callback) {
          H.b.counter(key, 1, options, callback);
        });

        it('should fail when passed a 0 delta', function () {
          assert.throws(function () {
            H.b.counter(H.key(), 0, H.noCallback());
          }, TypeError);
        });
        it('should fail when passed a non-numeric delta', function () {
          assert.throws(function () {
            H.b.counter(H.key(), {}, H.noCallback());
          }, TypeError);
        });
        it('should fail when passed a negative initial', function () {
          assert.throws(function () {
            H.b.counter(H.key(), 1, {initial:-1}, H.noCallback());
          }, TypeError);
        });
        it('should fail when passed a non-numeric initial', function () {
          assert.throws(function () {
            H.b.counter(H.key(), 1, {initial:{}}, H.noCallback());
          }, TypeError);
        });

        it('should increment properly', function(done) {
          var key = H.key();
          H.b.insert(key, '6', H.okCallback(function() {
            H.b.counter(key, 1, H.okCallback(function(res) {
              assert(res.value, 7);
              H.b.get(key, H.okCallback(function(getRes) {
                assert(getRes.value, res.value);
                done();
              }));
            }));
          }));
        });

        it('should add properly', function(done) {
          var key = H.key();
          H.b.insert(key, '6', H.okCallback(function() {
            H.b.counter(key, 3, H.okCallback(function(res) {
              assert(res.value, 9);
              H.b.get(key, H.okCallback(function(getRes) {
                assert(getRes.value, res.value);
                done();
              }));
            }));
          }));
        });

        it('should decrement properly', function(done) {
          var key = H.key();
          H.b.insert(key, '6', H.okCallback(function() {
            H.b.counter(key, -1, H.okCallback(function(res) {
              assert(res.value, 5);
              H.b.get(key, H.okCallback(function(getRes) {
                assert(getRes.value, res.value);
                done();
              }));
            }));
          }));
        });

        it('should subtract properly', function(done) {
          var key = H.key();
          H.b.insert(key, '6', H.okCallback(function() {
            H.b.counter(key, -3, H.okCallback(function(res) {
              assert.equal(res.value, 3);
              H.b.get(key, H.okCallback(function(getRes) {
                assert.equal(getRes.value, res.value);
                done();
              }));
            }));
          }));
        });

        it('should fail on missing key', function(done) {
          H.b.counter(H.key(), 1, function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        });
        it('should fail on a locked key', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(){
            H.b.getAndLock(key, H.okCallback(function() {
              H.b.counter(key, 1, function(err, res) {
                assert(err);
                assert(!res);
                done();
              });
            }));
          }));
        });

        it('should create with initial set', function(done) {
          var key = H.key();
          H.b.counter(key, 1, {initial:7}, H.okCallback(function(res) {
            assert.equal(res.value, 7);
            H.b.get(key, H.okCallback(function(getRes) {
              assert.equal(getRes.value, '7');
              done();
            }));
          }));
        });
      });

      describe('append', function() {
        testBadBasic(function (key, options, callback) {
          H.b.append(key, 'foo', options, callback);
        });
        testBadDura(function(key, options, callback) {
          H.b.append(key, 'foo', options, callback);
        });

        it('should append to a key', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(insertRes) {
            H.b.append(key, 'bar', H.okCallback(function(appendRes) {
              assert.notDeepEqual(insertRes.cas, appendRes.cas);
              H.b.get(key, H.okCallback(function(getRes) {
                assert.deepEqual(getRes.cas, appendRes.cas);
                assert.equal(getRes.value, 'foobar');
                done();
              }));
            }));
          }));
        });
        it('should fail on missing key', function(done) {
          H.b.append(H.key(), 'foo', function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        });
        it('should fail on a locked key', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(){
            H.b.getAndLock(key, H.okCallback(function() {
              H.b.append(key, 'bar', function(err, res) {
                assert(err);
                assert(!res);
                done();
              });
            }));
          }));
        });
      });

      describe('prepend', function() {
        testBadBasic(function (key, options, callback) {
          H.b.prepend(key, 'foo', options, callback);
        });
        testBadDura(function(key, options, callback) {
          H.b.prepend(key, 'foo', options, callback);
        });

        it('should prepend to a key', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(insertRes) {
            H.b.prepend(key, 'bar', H.okCallback(function(appendRes) {
              assert.notDeepEqual(insertRes.cas, appendRes.cas);
              H.b.get(key, H.okCallback(function(getRes) {
                assert.deepEqual(getRes.cas, appendRes.cas);
                assert.equal(getRes.value, 'barfoo');
                done();
              }));
            }));
          }));
        });
        it('should fail on missing key', function(done) {
          H.b.prepend(H.key(), 'foo', function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        });
        it('should fail on a locked key', function(done) {
          var key = H.key();
          H.b.insert(key, 'foo', H.okCallback(function(){
            H.b.getAndLock(key, H.okCallback(function() {
              H.b.prepend(key, 'bar', function(err, res) {
                assert(err);
                assert(!res);
                done();
              });
            }));
          }));
        });
      });
    });

    it('should successfully round-trip a document', function (done) {
      var key = H.key();
      var doc = {x: 'hello', y: 'world'};
      H.b.insert(key, doc, H.okCallback(function () {
        H.b.get(key, H.okCallback(function (res) {
          assert.deepEqual(res.value, doc);
          done();
        }));
      }));
    });

    it('should fail to insert an already existing document', function(done) {
      var key = H.key();
      H.b.insert(key, 'bar', H.okCallback(function() {
        H.b.insert(key, 'foo', function(err, res) {
          assert(err);
          done();
        });
      }));
    });

    it('should successfully upsert a document', function(done) {
      var key = H.key();
      H.b.upsert(key, 'bar', H.okCallback(function(upsertRes){
        H.b.get(key, H.okCallback(function(getRes) {
          assert.deepEqual(getRes.cas, upsertRes.cas);
          done();
        }))
      }));
    });

    it('should fail upsert on a locked key', function(done) {
      var key = H.key();
      H.b.insert(key, 'foo', H.okCallback(function(){
        H.b.getAndLock(key, H.okCallback(function() {
          H.b.upsert(key, 'bar', function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        }));
      }));
    });

    it('should fail replace on a locked key', function(done) {
      var key = H.key();
      H.b.insert(key, 'foo', H.okCallback(function(){
        H.b.getAndLock(key, H.okCallback(function() {
          H.b.replace(key, 'bar', function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        }));
      }));
    });


    it('should fail upsert with cas mismatch', function(done) {
      var key = H.key();
      H.b.insert(key, 'foo', H.okCallback(function(insertRes){
        H.b.upsert(key, 'bar', H.okCallback(function(){
          H.b.upsert(key, 'wat', {cas:insertRes.cas}, function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        }));
      }));
    });

    it('should successfully expire a document (slow)', function (done) {
      this.timeout(3000);
      var key = H.key();
      H.b.insert(key, 'foo', {expiry: 1}, H.okCallback(function () {
        H.b.get(key, H.okCallback(function () {
          H.timeTravel(function () {
            H.b.get(key, function (err) {
              assert(err);
              done();
            });
          }, 2000);
        }));
      }));
    });

    function touchTests(touchFn) {
      it('should succesfully update expiry (slow)', function(done) {
        this.timeout(5000);
        var key = H.key();
        H.b.insert(key, 'foo', {expiry: 1}, H.okCallback(function () {
          touchFn(key, 2, H.okCallback(function (res) {
            H.timeTravel(function () {
              H.b.get(key, H.okCallback(function() {
                H.timeTravel(function() {
                  H.b.get(key, function (err, res) {
                    assert(err);
                    assert(!res);
                    done();
                  });
                }, 3000);
              }));
            }, 100);
          }));
        }));
      });
      it('should fail on missing key', function(done) {
        touchFn(H.key(), 1, function(err, res) {
          assert(err);
          assert(!res);
          done();
        });
      });
      it('should fail on a locked key', function(done) {
        var key = H.key();
        H.b.insert(key, 'foo', H.okCallback(function(){
          H.b.getAndLock(key, H.okCallback(function() {
            touchFn(key, 1, function(err, res) {
              assert(err);
              assert(!res);
              done();
            });
          }));
        }));
      });
    }
    describe('getAndTouch', function() {
      touchTests(function(key, expiry, callback) {
        H.b.getAndTouch(key, expiry, callback);
      });
    });
    describe('touch', function() {
      touchTests(function(key, expiry, callback) {
        H.b.touch(key, expiry, callback);
      });
    });

    it('should require an accurate cas', function (done) {
      var key = H.key();
      H.b.insert(key, 'bar', H.okCallback(function (insertRes) {
        H.b.replace(key, 'foo', H.okCallback(function () {
          H.b.replace(key, 'dog', {cas: insertRes.cas}, function (err) {
            assert(err);
            done();
          });
        }));
      }));
    });

    it('should work with a valid cas', function (done) {
      var key = H.key();
      H.b.insert(key, 'bar', H.okCallback(function (insertRes) {
        H.b.replace(key, 'dog', {cas: insertRes.cas}, H.okCallback(function () {
          done();
        }));
      }));
    });

    it('should be able to remove a document', function(done) {
      var key = H.key();
      H.b.insert(key, 'bar', H.okCallback(function(insertRes) {
        H.b.remove(key, H.okCallback(function(removeRes) {
          assert.notDeepEqual(insertRes.cas, removeRes.cas);
          H.b.get(key, function(getErr, getRes) {
            assert(getErr);
            assert(!getRes);
            done();
          });
        }));
      }));
    });

    it('getAndLock should get the value', function(done) {
      var key = H.key();
      H.b.insert(key, 'bar', H.okCallback(function(insertRes) {
        H.b.getAndLock(key, H.okCallback(function(lockRes){
          assert.notDeepEqual(insertRes.cas, lockRes.cas);
          assert.equal(lockRes.value, 'bar');
          done();
        }));
      }));
    });

    it('get should work on a locked key', function(done) {
      var key = H.key();
      H.b.insert(key, 'bar', H.okCallback(function() {
        H.b.getAndLock(key, H.okCallback(function(){
          H.b.get(key, H.okCallback(function(res) {
            assert(res.value, 'bar');
            done();
          }));
        }));
      }));
    });

    it('should fail getAndLock for missing key', function(done) {
      H.b.getAndLock(H.key(), function(err, res) {
        assert(err);
        assert(!res);
        done();
      });
    });

    it('should fail getAndLock for locked key', function(done) {
      var key = H.key();
      H.b.insert(key, 'bar', H.okCallback(function(insertRes) {
        H.b.getAndLock(key, {lockTime: 1}, H.okCallback(function (lockRes) {
          assert.notDeepEqual(insertRes.cas, lockRes.cas);
          H.b.getAndLock(key, function(err, res) {
            assert(err);
            assert(!res);
            done();
          });
        }));
      }));
    });

    it('should set the correct lockTime (slow)', function(done) {
      // This test takes at least 2000ms to execute due to Server lock time
      //   rounding issue (locktime 1000ms takes up to 1999ms to expire).
      this.timeout(3000);
      var key = H.key();
      H.b.insert(key, 'bar', H.okCallback(function(insertRes) {
        H.b.getAndLock(key, {lockTime:1}, H.okCallback(function(lockRes){
          assert.notDeepEqual(insertRes.cas, lockRes.cas);
          H.b.replace(key, 'foo', function(getErr, getRes) {
            assert(getErr);
            assert(!getRes);
            H.timeTravel(function() {
              H.b.replace(key, 'rew', H.okCallback(function(replaceRes) {
                assert.notDeepEqual(lockRes.cas, replaceRes.cas);
                done();
              }));
            }, 2000);
          });
        }));
      }));
    });

    it('should fail to get a missing key', function(done) {
      H.b.get(H.key(), function(err, res) {
        assert(err);
        assert(!res);
        done();
      });
    });

    it('durability should not crash', function (done) {
      this.timeout(4000);
      H.b.insert(H.key(), 'foo', {persist_to: 1, replicate_to: 0}, function() {
        done();
      });
    });

    it('durability should propagate errors', function (done) {
      H.b.replace(H.key(), 'bar', {persist_to: 1, replicate_to: 0},
        function (err, res) {
          assert(err);
          done();
        });
    });

    it('should return from replace with 0 expiry (slow)', function(done) {
      this.timeout(3000);
      var key = H.key();
      H.b.insert(key, 'bar', {expiry:1}, H.okCallback(function(insertRes) {
        H.b.replace(key, 'foo', {expiry:0}, H.okCallback(function (replaceRes) {
          H.timeTravel(function () {
            H.b.get(key, H.okCallback(function (getRes) {
              assert(getRes);
              done();
            }));
          }, 2000);
        }));
      }));
    });

    it('should work with string cas values', function(done) {
      var key = H.key();
      H.b.upsert(key, 'test1', H.okCallback(function(res) {
        var strCas = res.cas.toString();
        var jsonCas = JSON.stringify(res.cas);
        assert(jsonCas);
        assert(strCas);
        assert(jsonCas === '"'+strCas+'"');
        H.b.upsert(key, 'test2', {cas:'18446744073709551614'}, function(err) {
          assert(err);
          H.b.upsert(key, 'test3', {cas:strCas}, H.okCallback(function(res) {
            done();
          }));
        });
      }));
    });
  }

  describe('#RealBucket', allTests.bind(this, harness));
  describe('#MockBucket', allTests.bind(this, harness.mock));
});
