'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);
        });

        it('should work with non-JSON documents', function() {
          var testBytes = Buffer.from([1, 2, 3, 4, 5, 0, 9, 8, 7, 6]);
          var key = H.key();

          // Set a passthru transcoder to insert with 0 flags
          H.b.setTranscoder(function(val) {
            return { value: val, flags: 0 };
          }, null);

          // Upsert the test bytes
          H.b.upsert(
            key,
            testBytes,
            H.okCallback(function() {
              // Reset the transcoder
              H.b.setTranscoder(null, null);

              H.b.get(key, function(err, res) {
                assert(!err);
                assert.deepEqual(res.value, testBytes);
              });
            })
          );
        });
      });
      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() {
              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) {
            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() {
                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) {
          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() {
          H.b.replace(
            key,
            'foo',
            {
              expiry: 0,
            },
            H.okCallback(function() {
              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() {
                  done();
                })
              );
            }
          );
        })
      );
    });

    it('null values should work', function(done) {
      this.timeout(4000);
      var key = H.key();
      H.b.insert(
        key,
        null,
        H.okCallback(function() {
          H.b.get(
            key,
            H.okCallback(function(getRes) {
              assert.deepEqual(getRes.value, null);
              done();
            })
          );
        })
      );
    });

    it('boolean values should work', function(done) {
      this.timeout(4000);
      var key = H.key();
      H.b.insert(
        key,
        false,
        H.okCallback(function() {
          H.b.get(
            key,
            H.okCallback(function(getRes) {
              assert.deepEqual(getRes.value, false);
              done();
            })
          );
        })
      );
    });

    it('integer values should work', function(done) {
      this.timeout(4000);
      var key = H.key();
      H.b.insert(
        key,
        142525,
        H.okCallback(function() {
          H.b.get(
            key,
            H.okCallback(function(getRes) {
              assert.deepEqual(getRes.value, 142525);
              done();
            })
          );
        })
      );
    });

    it('string values should work', function(done) {
      this.timeout(4000);
      var key = H.key();
      H.b.insert(
        key,
        'hello world',
        H.okCallback(function() {
          H.b.get(
            key,
            H.okCallback(function(getRes) {
              assert.deepEqual(getRes.value, 'hello world');
              done();
            })
          );
        })
      );
    });

    it('object values should work', function(done) {
      this.timeout(4000);
      var key = H.key();
      H.b.insert(
        key,
        { foo: 'bar' },
        H.okCallback(function() {
          H.b.get(
            key,
            H.okCallback(function(getRes) {
              assert.deepEqual(getRes.value, { foo: 'bar' });
              done();
            })
          );
        })
      );
    });
  }

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