discard """
  output: ""
  matrix: "--mm:refc; --mm:orc"
  targets: "c js"
"""

import json, strutils, options, tables
import std/assertions

# The definition of the `%` proc needs to be here, since the `% c` calls below
# can only find our custom `%` proc for `Pix` if defined in global scope.
type
  Pix = tuple[x, y: uint8, ch: uint16]
proc `%`(p: Pix): JsonNode =
  result = %* { "x" : % p.x,
                "y" : % p.y,
                "ch" : % p.ch }

proc testJson() =
  # Tests inspired by own use case (with some additional tests).
  # This should succeed.
  type
    Point[T] = object
      x, y: T

    ReplayEventKind = enum
      FoodAppeared, FoodEaten, DirectionChanged

    ReplayEvent = object
      time*: float
      case kind*: ReplayEventKind
      of FoodAppeared, FoodEaten:
        foodPos*: Point[float]
        case subKind*: bool
        of true:
          it: int
        of false:
          ot: float
      of DirectionChanged:
        playerPos*: float

    Replay = ref object
      events*: seq[ReplayEvent]
      test: int
      test2: string
      test3: bool
      testNil: string

  var x = Replay(
    events: @[
      ReplayEvent(
        time: 1.2345,
        kind: FoodEaten,
        foodPos: Point[float](x: 5.0, y: 1.0),
        subKind: true,
        it: 7
      )
    ],
    test: 18827361,
    test2: "hello world",
    test3: true,
    testNil: "nil"
  )

  let node = %x

  let y = to(node, Replay)
  doAssert y.events[0].time == 1.2345
  doAssert y.events[0].kind == FoodEaten
  doAssert y.events[0].foodPos.x == 5.0
  doAssert y.events[0].foodPos.y == 1.0
  doAssert y.test == 18827361
  doAssert y.test2 == "hello world"
  doAssert y.test3
  doAssert y.testNil == "nil"

  # Test for custom object variants (without an enum) and with an else branch.
  block:
    type
      TestVariant = object
        name: string
        case age: uint8
        of 2:
          preSchool: string
        of 8:
          primarySchool: string
        else:
          other: int

    var node = %{
      "name": %"Nim",
      "age": %8,
      "primarySchool": %"Sandtown"
    }

    var result = to(node, TestVariant)
    doAssert result.age == 8
    doAssert result.name == "Nim"
    doAssert result.primarySchool == "Sandtown"

    node = %{
      "name": %"⚔️Foo☢️",
      "age": %25,
      "other": %98
    }

    result = to(node, TestVariant)
    doAssert result.name == node["name"].getStr()
    doAssert result.age == node["age"].getInt().uint8
    doAssert result.other == node["other"].getBiggestInt()

  # TODO: Test object variant with set in of branch.
  # TODO: Should we support heterogeneous arrays?

  # Tests that verify the error messages for invalid data.
  block:
    type
      Person = object
        name: string
        age: int

    var node = %{
      "name": %"Dominik"
    }

    try:
      discard to(node, Person)
      doAssert false
    except KeyError as exc:
      doAssert("age" in exc.msg)
    except:
      doAssert false

    node["age"] = %false

    try:
      discard to(node, Person)
      doAssert false
    except JsonKindError as exc:
      doAssert("age" in exc.msg)
    except:
      doAssert false

    type
      PersonAge = enum
        Fifteen, Sixteen

      PersonCase = object
        name: string
        case age: PersonAge
        of Fifteen:
          discard
        of Sixteen:
          id: string

    try:
      discard to(node, PersonCase)
      doAssert false
    except JsonKindError as exc:
      doAssert("age" in exc.msg)
    except:
      doAssert false

  # Test the example in json module.
  block:
    let jsonNode = parseJson("""
      {
        "person": {
          "name": "Nimmer",
          "age": 21
        },
        "list": [1, 2, 3, 4]
      }
    """)

    type
      Person = object
        name: string
        age: int

      Data1 = object # TODO: Codegen bug when changed to ``Data``.
        person: Person
        list: seq[int]

    var data = to(jsonNode, Data1)
    doAssert data.person.name == "Nimmer"
    doAssert data.person.age == 21
    doAssert data.list == @[1, 2, 3, 4]

  # Test non-variant enum fields.
  block:
    type
      EnumType = enum
        Foo, Bar

      TestEnum = object
        field: EnumType

    var node = %{
      "field": %"Bar"
    }

    var result = to(node, TestEnum)
    doAssert result.field == Bar

  # Test ref type in field.
  block:
    var jsonNode = parseJson("""
      {
        "person": {
          "name": "Nimmer",
          "age": 21
        },
        "list": [1, 2, 3, 4]
      }
    """)

    type
      Person = ref object
        name: string
        age: int

      Data = object
        person: Person
        list: seq[int]

    var data = to(jsonNode, Data)
    doAssert data.person.name == "Nimmer"
    doAssert data.person.age == 21
    doAssert data.list == @[1, 2, 3, 4]

    jsonNode = parseJson("""
      {
        "person": null,
        "list": [1, 2, 3, 4]
      }
    """)
    data = to(jsonNode, Data)
    doAssert data.person.isNil

  block:
    type
      FooBar = object
        field: float

    let x = parseJson("""{ "field": 5}""")
    let data = to(x, FooBar)
    doAssert data.field == 5.0

  block:
    type
      BirdColor = object
        name: string
        rgb: array[3, float]

    type
      Bird = object
        age: int
        height: float
        name: string
        colors: array[2, BirdColor]

    var red = BirdColor(name: "red", rgb: [1.0, 0.0, 0.0])
    var blue = BirdColor(name: "blue", rgb: [0.0, 0.0, 1.0])
    var b = Bird(age: 3, height: 1.734, name: "bardo", colors: [red, blue])
    let jnode = %b
    let data = jnode.to(Bird)
    doAssert data == b

  block:
    type
      MsgBase = ref object of RootObj
        name*: string

      MsgChallenge = ref object of MsgBase
        challenge*: string

    let data = %*{"name": "foo", "challenge": "bar"}
    let msg = data.to(MsgChallenge)
    doAssert msg.name == "foo"
    doAssert msg.challenge == "bar"

  block:
    type
      Color = enum Red, Brown
      Thing = object
        animal: tuple[fur: bool, legs: int]
        color: Color

    var j = parseJson("""
      {"animal":{"fur":true,"legs":6},"color":"Red"}
    """)

    let parsed = to(j, Thing)
    doAssert parsed.animal.fur
    doAssert parsed.animal.legs == 6
    doAssert parsed.color == Red

  block:
    when not defined(js):
      # disable on js because of #12492
      type
        Car = object
          engine: tuple[name: string, capacity: float]
          model: string

      let j = """
        {"engine": {"name": "V8", "capacity": 5.5}, "model": "Skyline"}
      """

      var i = 0
      proc mulTest(): JsonNode =
        inc i
        return parseJson(j)

      let parsed = mulTest().to(Car)
      doAssert parsed.engine.name == "V8"

      doAssert i == 1

  block:
    # Option[T] support!
    type
      Car1 = object # TODO: Codegen bug when `Car`
        engine: tuple[name: string, capacity: Option[float]]
        model: string
        year: Option[int]

    let noYear = """
      {"engine": {"name": "V8", "capacity": 5.5}, "model": "Skyline"}
    """

    let noYearParsed = parseJson(noYear)
    let noYearDeser = to(noYearParsed, Car1)
    doAssert noYearDeser.engine.capacity == some(5.5)
    doAssert noYearDeser.year.isNone
    doAssert noYearDeser.engine.name == "V8"

    # Issue #7433
    type
      Obj2 = object
        n1: int
        n2: Option[string]
        n3: bool

    var j = %*[ { "n1": 4, "n2": "ABC", "n3": true },
                { "n1": 1, "n3": false },
                { "n1": 1, "n2": "XYZ", "n3": false } ]

    let jDeser = j.to(seq[Obj2])
    doAssert jDeser[0].n2.get() == "ABC"
    doAssert jDeser[1].n2.isNone()

    # Issue #6902
    type
      Obj = object
        n1: int
        n2: Option[int]
        n3: Option[string]
        n4: Option[bool]

    var j0 = parseJson("""{"n1": 1, "n2": null, "n3": null, "n4": null}""")
    let j0Deser = j0.to(Obj)
    doAssert j0Deser.n1 == 1
    doAssert j0Deser.n2.isNone()
    doAssert j0Deser.n3.isNone()
    doAssert j0Deser.n4.isNone()

  # Table[T, Y] support.
  block:
    type
      Friend = object
        name: string
        age: int

      Dynamic = object
        name: string
        friends: Table[string, Friend]

    let data = """
      {"friends": {
                    "John": {"name": "John", "age": 35},
                    "Elizabeth": {"name": "Elizabeth", "age": 23}
                  }, "name": "Dominik"}
    """

    let dataParsed = parseJson(data)
    let dataDeser = to(dataParsed, Dynamic)
    doAssert dataDeser.name == "Dominik"
    doAssert dataDeser.friends["John"].age == 35
    doAssert dataDeser.friends["Elizabeth"].age == 23

  # JsonNode support
  block:
    type
      Test = object
        name: string
        fallback: JsonNode

    let data = """
      {"name": "FooBar", "fallback": 56.42}
    """

    let dataParsed = parseJson(data)
    let dataDeser = to(dataParsed, Test)
    doAssert dataDeser.name == "FooBar"
    doAssert dataDeser.fallback.kind == JFloat
    doAssert dataDeser.fallback.getFloat() == 56.42

  # int64, float64 etc support.
  block:
    type
      Test1 = object
        a: int8
        b: int16
        c: int32
        d: int64
        e: uint8
        f: uint16
        g: uint32
        h: uint64
        i: float32
        j: float64

    let data = """
      {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6, "g": 7,
       "h": 8, "i": 9.9, "j": 10.10}
    """

    let dataParsed = parseJson(data)
    let dataDeser = to(dataParsed, Test1)
    doAssert dataDeser.a == 1
    doAssert dataDeser.f == 6
    doAssert dataDeser.i == 9.9'f32

  # deserialize directly into a table
  block:
    let s = """{"a": 1, "b": 2}"""
    let t = parseJson(s).to(Table[string, int])
    doAssert t["a"] == 1
    doAssert t["b"] == 2

  block:
    # bug #8037
    type
      Apple = distinct string
      String = distinct Apple
      Email = distinct string
      MyList = distinct seq[int]
      MyYear = distinct Option[int]
      MyTable = distinct Table[string, int]
      MyArr = distinct array[3, float]
      MyRef = ref object
        name: string
      MyObj = object
        color: int
      MyDistRef = distinct MyRef
      MyDistObj = distinct MyObj
      Toot = object
        name*: String
        email*: Email
        list: MyList
        year: MyYear
        dict: MyTable
        arr: MyArr
        person: MyDistRef
        distfruit: MyDistObj
        dog: MyRef
        fruit: MyObj
        emails: seq[String]

    var tJson = parseJson("""
      {
        "name":"Bongo",
        "email":"bongo@bingo.com",
        "list": [11,7,15],
        "year": 1975,
        "dict": {"a": 1, "b": 2},
        "arr": [1.0, 2.0, 7.0],
        "person": {"name": "boney"},
        "dog": {"name": "honey"},
        "fruit": {"color": 10},
        "distfruit": {"color": 11},
        "emails": ["abc", "123"]
      }
    """)

    var t = to(tJson, Toot)
    doAssert string(t.name) == "Bongo"
    doAssert string(t.email) == "bongo@bingo.com"
    doAssert seq[int](t.list) == @[11,7,15]
    doAssert Option[int](t.year).get() == 1975
    doAssert Table[string,int](t.dict)["a"] == 1
    doAssert Table[string,int](t.dict)["b"] == 2
    doAssert array[3, float](t.arr) == [1.0,2.0,7.0]

    doAssert MyRef(t.person).name == "boney"
    doAssert MyObj(t.distfruit).color == 11
    doAssert t.dog.name == "honey"
    doAssert t.fruit.color == 10
    doAssert seq[string](t.emails) == @["abc", "123"]

    block test_table:
      var y = parseJson("""{"a": 1, "b": 2, "c": 3}""")
      var u = y.to(MyTable)
      var v = y.to(Table[string, int])
      doAssert Table[string, int](u)["a"] == 1
      doAssert Table[string, int](u)["b"] == 2
      doAssert Table[string, int](u)["c"] == 3
      doAssert v["a"] == 1

    block primitive_string:
      const kApple = "apple"
      var u = newJString(kApple)
      var v = u.to(Email)
      var w = u.to(Apple)
      var x = u.to(String)
      doAssert string(v) == kApple
      doAssert string(w) == kApple
      doAssert string(x) == kApple

    block test_option:
      var u = newJInt(1137)
      var v = u.to(MyYear)
      var w = u.to(Option[int])
      doAssert Option[int](v).get() == 1137
      doAssert w.get() == 1137

    block test_object:
      var u = parseJson("""{"color": 987}""")
      var v = u.to(MyObj)
      var w = u.to(MyDistObj)
      doAssert v.color == 987
      doAssert MyObj(w).color == 987

    block test_ref_object:
      var u = parseJson("""{"name": "smith"}""")
      var v = u.to(MyRef)
      var w = u.to(MyDistRef)
      doAssert v.name == "smith"
      doAssert MyRef(w).name == "smith"

  block:
    # bug #12015
    type
      Cluster = object
        works: tuple[x, y: uint8, ch: uint16] # working
        fails: Pix # previously broken

    let data = (x: 123'u8, y: 53'u8, ch: 1231'u16)
    let c = Cluster(works: data, fails: data)
    let cFromJson = (% c).to(Cluster)
    doAssert c == cFromJson

  block:
    # bug related to #12015
    type
      PixInt = tuple[x, y, ch: int]
      SomePix = Pix | PixInt
      Cluster[T: SomePix] = seq[T]
      ClusterObject[T: SomePix] = object
        data: Cluster[T]
      RecoEvent[T: SomePix] = object
        cluster: seq[ClusterObject[T]]

    let data = @[(x: 123'u8, y: 53'u8, ch: 1231'u16)]
    var c = RecoEvent[Pix](cluster: @[ClusterObject[Pix](data: data)])
    let cFromJson = (% c).to(RecoEvent[Pix])
    doAssert c == cFromJson


  block:
    # ref objects with cycles.
    type
      Misdirection = object
        cycle: Cycle

      Cycle = ref object
        foo: string
        cycle: Misdirection

    let data = """
      {"cycle": null}
    """

    let dataParsed = parseJson(data)
    let dataDeser = to(dataParsed, Misdirection)

  block:
    # ref object from #12316
    type
      Foo = ref Bar
      Bar = object

    discard "null".parseJson.to Foo

  block:
    # named array #12289
    type Vec = array[2, int]
    let arr = "[1,2]".parseJson.to Vec
    doAssert arr == [1,2]

  block:
    # test error message in exception

    type
      MyType = object
        otherMember: string
        member: MySubType

      MySubType = object
        somethingElse: string
        list: seq[MyData]

      MyData = object
        value: int

    let jsonNode = parseJson("""
      {
        "otherMember": "otherValue",
        "member": {
          "somethingElse": "something",
          "list": [{"value": 1}, {"value": 2}, {}]
        }
      }
    """)

    try:
      let tmp = jsonNode.to(MyType)
      doAssert false, "this should be unreachable"
    except KeyError:
      doAssert getCurrentExceptionMsg().contains ".member.list[2].value"

  block:
    # Enum indexed array test
    type Test = enum
      one, two, three, four, five
    let a = [
      one: 300,
      two: 20,
      three: 10,
      four: 0,
      five: -10
    ]
    doAssert (%* a).to(a.typeof) == a


testJson()
static:
  testJson()
