var Sinon = require(“sinon”) var stringify = require(“..”) function jsonify(obj) { return JSON.stringify(obj, null, 2) }

describe(“Stringify”, function() {

it("must stringify circular objects", function() {
  var obj = {name: "Alice"}
  obj.self = obj
  var json = stringify(obj, null, 2)
  json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"}))
})

it("must stringify circular objects with intermediaries", function() {
  var obj = {name: "Alice"}
  obj.identity = {self: obj}
  var json = stringify(obj, null, 2)
  json.must.eql(jsonify({name: "Alice", identity: {self: "[Circular ~]"}}))
})

it("must stringify circular objects deeper", function() {
  var obj = {name: "Alice", child: {name: "Bob"}}
  obj.child.self = obj.child

  stringify(obj, null, 2).must.eql(jsonify({
    name: "Alice",
    child: {name: "Bob", self: "[Circular ~.child]"}
  }))
})

it("must stringify circular objects deeper with intermediaries", function() {
  var obj = {name: "Alice", child: {name: "Bob"}}
  obj.child.identity = {self: obj.child}

  stringify(obj, null, 2).must.eql(jsonify({
    name: "Alice",
    child: {name: "Bob", identity: {self: "[Circular ~.child]"}}
  }))
})

it("must stringify circular objects in an array", function() {
  var obj = {name: "Alice"}
  obj.self = [obj, obj]

  stringify(obj, null, 2).must.eql(jsonify({
    name: "Alice", self: ["[Circular ~]", "[Circular ~]"]
  }))
})

it("must stringify circular objects deeper in an array", function() {
  var obj = {name: "Alice", children: [{name: "Bob"}, {name: "Eve"}]}
  obj.children[0].self = obj.children[0]
  obj.children[1].self = obj.children[1]

  stringify(obj, null, 2).must.eql(jsonify({
    name: "Alice",
    children: [
      {name: "Bob", self: "[Circular ~.children.0]"},
      {name: "Eve", self: "[Circular ~.children.1]"}
    ]
  }))
})

it("must stringify circular arrays", function() {
  var obj = []
  obj.push(obj)
  obj.push(obj)
  var json = stringify(obj, null, 2)
  json.must.eql(jsonify(["[Circular ~]", "[Circular ~]"]))
})

it("must stringify circular arrays with intermediaries", function() {
  var obj = []
  obj.push({name: "Alice", self: obj})
  obj.push({name: "Bob", self: obj})

  stringify(obj, null, 2).must.eql(jsonify([
    {name: "Alice", self: "[Circular ~]"},
    {name: "Bob", self: "[Circular ~]"}
  ]))
})

it("must stringify repeated objects in objects", function() {
  var obj = {}
  var alice = {name: "Alice"}
  obj.alice1 = alice
  obj.alice2 = alice

  stringify(obj, null, 2).must.eql(jsonify({
    alice1: {name: "Alice"},
    alice2: {name: "Alice"}
  }))
})

it("must stringify repeated objects in arrays", function() {
  var alice = {name: "Alice"}
  var obj = [alice, alice]
  var json = stringify(obj, null, 2)
  json.must.eql(jsonify([{name: "Alice"}, {name: "Alice"}]))
})

it("must call given decycler and use its output", function() {
  var obj = {}
  obj.a = obj
  obj.b = obj

  var decycle = Sinon.spy(function() { return decycle.callCount })
  var json = stringify(obj, null, 2, decycle)
  json.must.eql(jsonify({a: 1, b: 2}, null, 2))

  decycle.callCount.must.equal(2)
  decycle.thisValues[0].must.equal(obj)
  decycle.args[0][0].must.equal("a")
  decycle.args[0][1].must.equal(obj)
  decycle.thisValues[1].must.equal(obj)
  decycle.args[1][0].must.equal("b")
  decycle.args[1][1].must.equal(obj)
})

it("must call replacer and use its output", function() {
  var obj = {name: "Alice", child: {name: "Bob"}}

  var replacer = Sinon.spy(bangString)
  var json = stringify(obj, replacer, 2)
  json.must.eql(jsonify({name: "Alice!", child: {name: "Bob!"}}))

  replacer.callCount.must.equal(4)
  replacer.args[0][0].must.equal("")
  replacer.args[0][1].must.equal(obj)
  replacer.thisValues[1].must.equal(obj)
  replacer.args[1][0].must.equal("name")
  replacer.args[1][1].must.equal("Alice")
  replacer.thisValues[2].must.equal(obj)
  replacer.args[2][0].must.equal("child")
  replacer.args[2][1].must.equal(obj.child)
  replacer.thisValues[3].must.equal(obj.child)
  replacer.args[3][0].must.equal("name")
  replacer.args[3][1].must.equal("Bob")
})

it("must call replacer after describing circular references", function() {
  var obj = {name: "Alice"}
  obj.self = obj

  var replacer = Sinon.spy(bangString)
  var json = stringify(obj, replacer, 2)
  json.must.eql(jsonify({name: "Alice!", self: "[Circular ~]!"}))

  replacer.callCount.must.equal(3)
  replacer.args[0][0].must.equal("")
  replacer.args[0][1].must.equal(obj)
  replacer.thisValues[1].must.equal(obj)
  replacer.args[1][0].must.equal("name")
  replacer.args[1][1].must.equal("Alice")
  replacer.thisValues[2].must.equal(obj)
  replacer.args[2][0].must.equal("self")
  replacer.args[2][1].must.equal("[Circular ~]")
})

it("must call given decycler and use its output for nested objects",
  function() {
  var obj = {}
  obj.a = obj
  obj.b = {self: obj}

  var decycle = Sinon.spy(function() { return decycle.callCount })
  var json = stringify(obj, null, 2, decycle)
  json.must.eql(jsonify({a: 1, b: {self: 2}}))

  decycle.callCount.must.equal(2)
  decycle.args[0][0].must.equal("a")
  decycle.args[0][1].must.equal(obj)
  decycle.args[1][0].must.equal("self")
  decycle.args[1][1].must.equal(obj)
})

it("must use decycler's output when it returned null", function() {
  var obj = {a: "b"}
  obj.self = obj
  obj.selves = [obj, obj]

  function decycle() { return null }
  stringify(obj, null, 2, decycle).must.eql(jsonify({
    a: "b",
    self: null,
    selves: [null, null]
  }))
})

it("must use decycler's output when it returned undefined", function() {
  var obj = {a: "b"}
  obj.self = obj
  obj.selves = [obj, obj]

  function decycle() {}
  stringify(obj, null, 2, decycle).must.eql(jsonify({
    a: "b",
    selves: [null, null]
  }))
})

it("must throw given a decycler that returns a cycle", function() {
  var obj = {}
  obj.self = obj
  var err
  function identity(key, value) { return value }
  try { stringify(obj, null, 2, identity) } catch (ex) { err = ex }
  err.must.be.an.instanceof(TypeError)
})

describe(".getSerialize", function() {
  it("must stringify circular objects", function() {
    var obj = {a: "b"}
    obj.circularRef = obj
    obj.list = [obj, obj]

    var json = JSON.stringify(obj, stringify.getSerialize(), 2)
    json.must.eql(jsonify({
      "a": "b",
      "circularRef": "[Circular ~]",
      "list": ["[Circular ~]", "[Circular ~]"]
    }))
  })

  // This is the behavior as of Mar 3, 2015.
  // The serializer function keeps state inside the returned function and
  // so far I'm not sure how to not do that. JSON.stringify's replacer is not
  // called _after_ serialization.
  xit("must return a function that could be called twice", function() {
    var obj = {name: "Alice"}
    obj.self = obj

    var json
    var serializer = stringify.getSerialize()

    json = JSON.stringify(obj, serializer, 2)
    json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"}))

    json = JSON.stringify(obj, serializer, 2)
    json.must.eql(jsonify({name: "Alice", self: "[Circular ~]"}))
  })
})

})

function bangString(key, value) {

return typeof value == "string" ? value + "!" : value

}