import std/options
import std/strutils
import std/unittest
import pkg/codex/blocktype
import pkg/codex/conf
import pkg/codex/contracts/requests
import pkg/codex/logutils
import pkg/codex/purchasing/purchaseid
import pkg/codex/units
import pkg/codex/utils/json
import pkg/libp2p/cid
import pkg/libp2p/multiaddress
import pkg/questionable
import pkg/questionable/results
import pkg/stew/byteutils
import pkg/stint
import ../checktest

export logutils

logStream testlines[textlines[nocolors,notimestamps,dynamic]]
logStream testjson[json[nocolors,notimestamps,dynamic]]

type
  ObjectType = object
    a: string
  DistinctType {.borrow: `.`.} = distinct ObjectType
  RefType = ref object
    a: string
  AnotherType = object
    a: int

# must be defined at the top-level
proc `$`*(t: ObjectType): string = "used `$`"
func `%`*(t: RefType): JsonNode = % t.a
logutils.formatIt(LogFormat.textLines, ObjectType): "formatted_" & it.a
logutils.formatIt(LogFormat.textLines, RefType): "formatted_" & it.a
logutils.formatIt(LogFormat.textLines, DistinctType): "formatted_" & it.a
logutils.formatIt(LogFormat.json, ObjectType): "formatted_" & it.a
logutils.formatIt(LogFormat.json, RefType): %it
logutils.formatIt(LogFormat.json, DistinctType): "formatted_" & it.a
logutils.formatIt(AnotherType): it.a

checksuite "Test logging output":
  var outputLines: string
  var outputJson: string

  proc writeToLines(logLevel: LogLevel, msg: LogOutputStr) =
    outputLines &= msg

  proc writeToJson(logLevel: LogLevel, msg: LogOutputStr) =
    outputJson &= msg

  setup:
    outputLines = ""
    outputJson = ""
    testlines.outputs[0].writer = writeToLines
    testjson.outputs[0].writer = writeToJson

  template logged(prop, expected): auto =
    let toFind = prop & "=" & expected
    outputLines.contains(toFind)

  template loggedJson(prop, expected): auto =
    let jsonVal = !JsonNode.parse(outputJson)
    $ jsonVal{prop} == expected

  template log(val) =
    testlines.trace "test", val
    testjson.trace "test", val

  test "logs objects":
    let t = ObjectType(a: "a")
    log t
    check logged("t", "formatted_a")
    check loggedJson("t", "\"formatted_a\"")

  test "logs sequences of objects":
    let t1 = ObjectType(a: "a")
    let t2 = ObjectType(a: "b")
    let t = @[t1, t2]
    log t
    check logged("t", "\"@[formatted_a, formatted_b]\"")
    check loggedJson("t", "[\"formatted_a\",\"formatted_b\"]")

  test "logs ref types":
    let t = RefType(a: "a")
    log t
    check logged("t", "formatted_a")
    check loggedJson("t", "\"a\"")

  test "logs sequences of ref types":
    let t1 = RefType(a: "a")
    let t2 = RefType(a: "b")
    let t = @[t1, t2]
    log t
    check logged("t", "\"@[formatted_a, formatted_b]\"")
    check loggedJson("t", "[\"a\",\"b\"]")

  test "logs distinct types":
    let t = DistinctType(ObjectType(a: "a"))
    log t
    check logged("t", "formatted_a")
    check loggedJson("t", "\"formatted_a\"")

  test "logs sequences of distinct types":
    let t1 = DistinctType(ObjectType(a: "a"))
    let t2 = DistinctType(ObjectType(a: "b"))
    let t = @[t1, t2]
    log t
    check logged("t", "\"@[formatted_a, formatted_b]\"")
    check loggedJson("t", "[\"formatted_a\",\"formatted_b\"]")

  test "formatIt can return non-string types":
    let t = AnotherType(a: 1)
    log t
    check logged("t", "1")
    check loggedJson("t", "1")

  test "logs Option types":
    let t = some ObjectType(a: "a")
    log t
    check logged("t", "some(formatted_a)")
    check loggedJson("t", "\"formatted_a\"")

  test "logs sequences of Option types":
    let t1 = some ObjectType(a: "a")
    let t2 = none ObjectType
    let t = @[t1, t2]
    log t
    check logged("t", "\"@[some(formatted_a), none(ObjectType)]\"")
    check loggedJson("t", """["formatted_a",null]""")

  test "logs Result types -- success with string property":
    let t: ?!ObjectType = success ObjectType(a: "a")
    log t
    check logged("t", "formatted_a")
    check loggedJson("t", "\"formatted_a\"")

  test "logs Result types -- success with int property":
    let t: ?!AnotherType = success AnotherType(a: 1)
    log t
    check logged("t", "1")
    check loggedJson("t", "1")

  test "logs Result types -- failure":
    let t: ?!ObjectType = ObjectType.failure newException(ValueError, "some error")
    log t
    check logged("t", "\"Error: some error\"")
    check loggedJson("t", """{"error":"some error"}""")

  test "can define `$` override for T":
    let o = ObjectType()
    check $o == "used `$`"

  test "logs NByte correctly":
    let nb = 12345.NBytes
    log nb
    check logged("nb", "12345\'NByte")
    check loggedJson("nb", "\"12345\'NByte\"")

  test "logs BlockAddress correctly":
    let cid = Cid.init("zb2rhgsDE16rLtbwTFeNKbdSobtKiWdjJPvKEuPgrQAfndjU1").tryGet
    let ba = BlockAddress.init(cid, 0)
    log ba
    check logged("ba", "\"treeCid: zb2*fndjU1, index: 0\"")
    check loggedJson("ba", """{"treeCid":"zb2rhgsDE16rLtbwTFeNKbdSobtKiWdjJPvKEuPgrQAfndjU1","index":0}""")

  test "logs Cid correctly":
    let cid = Cid.init("zb2rhmfWaXASbyi15iLqbz5yp3awnSyecpt9jcFnc2YA5TgiD").tryGet
    log cid
    check logged("cid", "zb2*A5TgiD")
    check loggedJson("cid", "\"zb2rhmfWaXASbyi15iLqbz5yp3awnSyecpt9jcFnc2YA5TgiD\"")

  test "logs StUint correctly":
    let stint = 12345678901234.u256
    log stint
    check logged("stint", "12345678901234")
    check loggedJson("stint", "\"12345678901234\"")

  test "logs int correctly":
    let int = 123
    log int
    check logged("int", "123")
    check loggedJson("int", "123")

  test "logs EthAddress correctly":
    let address = EthAddress.fromHex("0xf75e076f650cd51dbfa0fd9c465d5037f22e1b1b")
    log address
    check logged("address", "0xf75e..1b1b")
    check loggedJson("address", "\"0xf75e076f650cd51dbfa0fd9c465d5037f22e1b1b\"")

  test "logs PurchaseId correctly":
    let id = PurchaseId.fromHex("0x712003bdfc0db9abf21e7fbb7119cd52ff221c96714d21d39e782d7c744d3dea")
    log id
    check logged("id", "0x7120..3dea")

  test "logs RequestId correctly":
    let id = RequestId.fromHex("0x712003bdfc0db9abf21e7fbb7119cd52ff221c96714d21d39e782d7c744d3dea")
    log id
    check logged("id", "0x7120..3dea")
    check loggedJson("id", "\"0x712003bdfc0db9abf21e7fbb7119cd52ff221c96714d21d39e782d7c744d3dea\"")

  test "logs seq[RequestId] correctly":
    let id = RequestId.fromHex("0x712003bdfc0db9abf21e7fbb7119cd52ff221c96714d21d39e782d7c744d3dea")
    let id2 = RequestId.fromHex("0x9ab2c4d102a95d990facb022d67b3c9b39052597c006fddf122bed2cb594c282")
    let ids = @[id, id2]
    log ids
    check logged("ids", "\"@[0x7120..3dea, 0x9ab2..c282]\"")
    check loggedJson("ids", """["0x712003bdfc0db9abf21e7fbb7119cd52ff221c96714d21d39e782d7c744d3dea","0x9ab2c4d102a95d990facb022d67b3c9b39052597c006fddf122bed2cb594c282"]""")

  test "logs SlotId correctly":
    let id = SlotId.fromHex("0x9ab2c4d102a95d990facb022d67b3c9b39052597c006fddf122bed2cb594c282")
    log id
    check logged("id", "0x9ab2..c282")
    check loggedJson("id", "\"0x9ab2c4d102a95d990facb022d67b3c9b39052597c006fddf122bed2cb594c282\"")

  test "logs Nonce correctly":
    let n = Nonce.fromHex("ce88f368a7b776172ebd29a212456eb66acb60f169ee76eae91935e7fafad6ea")
    log n
    check logged("n", "0xce88..d6ea")
    check loggedJson("n", "\"0xce88f368a7b776172ebd29a212456eb66acb60f169ee76eae91935e7fafad6ea\"")

  test "logs MultiAddress correctly":
    let ma = MultiAddress.init("/ip4/127.0.0.1/tcp/0").tryGet
    log ma
    check logged("ma", "/ip4/127.0.0.1/tcp/0")
    check loggedJson("ma", "\"/ip4/127.0.0.1/tcp/0\"")

  test "logs seq[MultiAddress] correctly":
    let ma = @[MultiAddress.init("/ip4/127.0.0.1/tcp/0").tryGet,
               MultiAddress.init("/ip4/127.0.0.2/tcp/1").tryGet]
    log ma
    check logged("ma", "\"@[/ip4/127.0.0.1/tcp/0, /ip4/127.0.0.2/tcp/1]\"")
    check loggedJson("ma", "[\"/ip4/127.0.0.1/tcp/0\",\"/ip4/127.0.0.2/tcp/1\"]")