import unittest, json, strutils, strformat, sequtils, macros, times, os, math, unicode
import ormin
import ormin/db_utils

when defined(postgre):
  when defined(macosx):
    {.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
  import db_connector/db_postgres as db_postgres

  const backend = DbBackend.postgre
  importModel(backend, "model_postgre")
  const sqlFileName = "model_postgre.sql"
  let db {.global.} = db_postgres.open("localhost", "test", "test", "test_ormin")
else:
  from db_connector/db_sqlite import exec, getValue

  const backend = DbBackend.sqlite
  importModel(backend, "model_sqlite")
  const sqlFileName = "model_sqlite.sql"
  # let db {.global.} = open(":memory:", "", "", "")
  let db {.global.} = open("test.db", "", "", "")

let
  testDir = currentSourcePath.parentDir()
  sqlFile = Path(testDir / sqlFileName)

proc substr(s: string; start, length: int): string {.importSql.}

let
  min = -3
  max = 3
  serial_int_seqs = [(1, min), (2, 1), (3, 2), (4, max)]

suite &"Test common database types and functions of {backend}":
  discard

suite "serial_insert":
  setup:
    db.dropTable(sqlFile, "tb_serial")
    db.createTable(sqlFile, "tb_serial")

  test "insert":
    for i in serial_int_seqs:
      query:
        insert tb_serial(typinteger = ?i[1])
    check db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len

  test "json":
    for i in serial_int_seqs:
      let v = %*{"typinteger": i[1]}
      query:
        insert tb_serial(typinteger = %v["typinteger"])
    check db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len


suite "serial":
  db.dropTable(sqlFile, "tb_serial")
  db.createTable(sqlFile, "tb_serial")

  let insertSql = sql"insert into tb_serial(typinteger) values (?)"
  for (_, v) in serial_int_seqs:
    db.exec(insertSql, v) 
  doAssert db.getValue(sql"select count(*) from tb_serial") == $serial_int_seqs.len

  test "query":
    let res = query:
      select tb_serial(typserial, typinteger)
    check res == serial_int_seqs

  test "where":
    let
      id = 1
      res = query:
        select tb_serial(typserial, typinteger)
        where typserial == ?id
    check res == serial_int_seqs.filterIt(it[0] == id)

  test "json":
    let res = query:
      select tb_serial(typserial, typinteger)
      produce json
    check res == %*serial_int_seqs.mapIt(%*{"typserial": it[0], "typinteger": it[1]})

  test "count":
    let res = query:
      select tb_serial(count(_))
    check res[0] == serial_int_seqs.len

  test "sum":
    let res = query:
      select tb_serial(sum(typinteger))
    check res[0] == serial_int_seqs.mapIt(it[1]).sum()

  test "avg":
    let res = query:
      select tb_serial(avg(typinteger))
    check res[0] == serial_int_seqs.mapIt(it[1]).sum() / serial_int_seqs.len()

  test "min":
    let res = query:
      select tb_serial(min(typinteger))
    check res[0] == min

  test "max":
    let res = query:
      select tb_serial(max(typinteger))
    check res[0] == max

  test "abs":
    let res = query:
      select tb_serial(abs(typinteger))
    check res == serial_int_seqs.mapIt(abs(it[1]))


let seqs = toSeq(1..3)

suite "boolean_insert":
  setup:
    db.dropTable(sqlFile, "tb_boolean")
    db.createTable(sqlFile, "tb_boolean")

  test "insert":
    for i in seqs:
      let b = (i mod 2 == 0)
      query:
        insert tb_boolean(typboolean = ?b)
    check db.getValue(sql"select count(*) from tb_boolean") == $seqs.len

  test "json":
    for i in seqs:
      let b = %*{"typboolean": (i mod 2 == 0)}
      query:
        insert tb_boolean(typboolean = %b["typboolean"])
    check db.getValue(sql"select count(*) from tb_boolean") == $seqs.len


suite "boolean":
  db.dropTable(sqlFile, "tb_boolean")
  db.createTable(sqlFile, "tb_boolean")

  proc toBool(x: int): bool =
    if x mod 2 == 0: true else: false

  proc toDbValue(x: int): auto =
    when defined(postgre):
      if x mod 2 == 0: "t" else: "f"
    else:
      if x mod 2 == 0: 1 else: 0

  let insertSql = sql"insert into tb_boolean(typboolean) values (?)"
  for i in seqs:
    db.exec(insertSql, toDbValue(i))
  doAssert db.getValue(sql"select count(*) from tb_boolean") == $seqs.len

  test "query":
    let res = query:
      select tb_boolean(typboolean)
    check res == seqs.mapIt(it mod 2 == 0)

  test "where":
    let
      b = true
      res = query:
        select tb_boolean(typboolean)
        where typboolean == ?b
    check res == seqs.map(toBool).filterIt(it == b)

  test "json":
    let res = query:
      select tb_boolean(typboolean)
      produce json
    check res == %*seqs.mapIt(%*{"typboolean": toBool(it)})

  when defined(sqlite):
    test "importSql substr wrong type":
      ## TODO: should this be allowed?
      let res = query:
        select tb_boolean(substr(typboolean, 1, 2))
      echo "res: ", res

let fs = [-3.14, 2.56, 10.45]

suite "float_insert":
  setup:
    db.dropTable(sqlFile, "tb_float")
    db.createTable(sqlFile, "tb_float")

  test "insert":
    for f in fs:
      query:
        insert tb_float(typfloat = ?f)
    check db.getValue(sql"select count(*) from tb_float") == $fs.len

  test "json":
    for f in fs:
      let v = %*{"typfloat": f}
      query:
        insert tb_float(typfloat = %v["typfloat"])
    check db.getValue(sql"select count(*) from tb_float") == $fs.len


suite "float":
  db.dropTable(sqlFile, "tb_float")
  db.createTable(sqlFile, "tb_float")

  let insertSql = sql"insert into tb_float(typfloat) values (?)"
  for f in fs:
    db.exec(insertSql, f)
  doAssert db.getValue(sql"select count(*) from tb_float") == $fs.len

  test "query":
    let res = query:
      select tb_float(typfloat)
    check res == fs

  test "where":
    let res = query:
      select tb_float(typfloat)
      where typfloat == ?fs[0]
    check res == [fs[0]]

  test "json":
    let res = query:
      select tb_float(typfloat)
      produce json
    check res == %*fs.mapIt(%*{"typfloat": it})

  test "abs":
    let res = query:
      select tb_float(abs(typfloat))
    check res == fs.mapIt(abs(it))

let ss = ["one", "Two", "three", "第四", "four'th"]

suite "string_insert":
  setup:
    db.dropTable(sqlFile, "tb_string")
    db.createTable(sqlFile, "tb_string")

  test "insert":
    for v in ss:
      query:
        insert tb_string(typstring = ?v)
    check db.getValue(sql"select count(*) from tb_string") == $ss.len

  test "insert":
    for v in ss:
      query:
        insert tb_string(typstring = "can't touch this")
    check db.getValue(sql"select count(*) from tb_string") == $ss.len

  test "json":
    for v in ss:
      let j = %*{"typstring": v}
      query:
        insert tb_string(typstring = %j["typstring"])
    check db.getValue(sql"select count(*) from tb_string") == $ss.len

suite "string":
  db.dropTable(sqlFile, "tb_string")
  db.createTable(sqlFile, "tb_string")

  let insertsql = sql"insert into tb_string(typstring) values (?)"
  for v in ss:
    db.exec(insertsql, v)
  doAssert db.getValue(sql"select count(*) from tb_string") == $ss.len

  test "query":
    let res = query:
      select tb_string(typstring)
    check res == ss

  test "where":
    let res = query:
      select tb_string(typstring)
      where typstring == ?ss[0]
    check res[0] == ss[0]

  test "json":
    let res = query:
      select tb_string(typstring)
      produce json
    check res == %*ss.mapIt(%*{"typstring": it})

  test "concat_op":
    let
      s = "typstring: "
    let res = query:
      select tb_string(?s & typstring)
    check res == ss.mapIt(s & it)
    
  test "length":
    let res = query:
      select tb_string(length(typstring))
    check res == ss.mapIt(it.runeLen)

  test "lower":
    let res = query:
      select tb_string(lower(typstring))
    check res == ss.mapIt(it.toLowerAscii)

  test "upper":
    let res = query:
      select tb_string(upper(typstring))
    check res == ss.mapIt(it.toUpperAscii)

  test "replace":
    let res = query:
      select tb_string(replace(typstring, "e", "o"))
    check res == ss.mapIt(it.replace("e", "o"))

  test "importSql substr length 0":
    let res = query:
      select tb_string(substr(typstring, 1, 0))
    let expected = ss.mapIt($(toRunes(it)[0..<0]))
    check res == expected

  test "importSql substr length no arg types checked":
    # TODO: fixme!
    # the `functions` array only contains the types of the return
    # but sqlite doesn't really care...
    let res = query:
      select tb_string(substr(typstring, "1", 2))
    let expected = ss.mapIt($(toRunes(it)[0..<2]))
    check res == expected

let js = [
  %*{"name": "tom", "age": 30},
  %*{"name": "bob", "age": 35},
  %*{"name": "jack", "age": 23} 
]

suite "insert_json":
  setup:
    db.dropTable(sqlFile, "tb_json")
    db.createTable(sqlFile, "tb_json")

  test "insert":
    for j in js:
      query:
        insert tb_json(typjson = ?j)
    check db.getValue(sql"select count(*) from tb_json") == $js.len

suite "json":
  db.dropTable(sqlFile, "tb_json")
  db.createTable(sqlFile, "tb_json")
  
  let insertSql = sql"insert into tb_json(typjson) values (?)"
  for j in js:
    db.exec(insertSql, j)
  doAssert db.getValue(sql"select count(*) from tb_json") == $js.len

  test "query":
    let res = query:
      select tb_json(typjson)
    check res == js

  test "json":
    let res = query:
      select tb_json(typjson)
      produce json
    check res == %*js.mapIt(%*{"typjson": it})


let cps = [
  (1, 1, "one-one"),
  (1, 2, "one-two"),
  (2, 1, "two-one")
]

suite "composite_pk_insert":
  setup:
    db.dropTable(sqlFile, "tb_composite_pk")
    db.createTable(sqlFile, "tb_composite_pk")

  test "insert":
    for r in cps:
      query:
        insert tb_composite_pk(pk1 = ?r[0], pk2 = ?r[1], message = ?r[2])
    check db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len

  test "json":
    for r in cps:
      let v = %*{"pk1": r[0], "pk2": r[1], "message": r[2]}
      query:
        insert tb_composite_pk(pk1 = %v["pk1"], pk2 = %v["pk2"], message = %v["message"])
    check db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len


suite "composite_pk":
  db.dropTable(sqlFile, "tb_composite_pk")
  db.createTable(sqlFile, "tb_composite_pk")

  let insertSql = sql"insert into tb_composite_pk(pk1, pk2, message) values (?, ?, ?)"
  for r in cps:
    db.exec(insertSql, r[0], r[1], r[2])
  doAssert db.getValue(sql"select count(*) from tb_composite_pk") == $cps.len

  test "query":
    let res = query:
      select tb_composite_pk(pk1, pk2, message)
    check res == cps

  test "where":
    let res = query:
      select tb_composite_pk(pk1, pk2, message)
      where pk1 == ?cps[0][0]
    check res == cps.filterIt(it[0] == cps[0][0])

  test "json":
    let res = query:
      select tb_composite_pk(pk1, pk2, message)
      produce json
    check res == %*cps.mapIt(%*{"pk1": it[0], "pk2": it[1], "message": it[2]})


suite "nullable":
  setup:
    db.dropTable(sqlFile, "tb_nullable")
    db.createTable(sqlFile, "tb_nullable")
    query:
      insert tb_nullable(id = 1, note = nil)
    query:
      insert tb_nullable(id = 2, note = "hello")

  test "where_null":
    let res = query:
      select tb_nullable(id)
      where note == nil
    check res == @[1]

  test "where_not_null":
    let res = query:
      select tb_nullable(id)
      where note != null
    check res == @[2]

  test "update_to_null":
    query:
      update tb_nullable(note = null)
      where id == 2
    let res = query:
      select tb_nullable(id)
      where note == nil
    check res == @[1, 2]

  test "json_null":
    let res = query:
      select tb_nullable(id, note)
      orderby id
      produce json
    check res[0]["note"].kind == JNull
    check res[1]["note"].getStr == "hello"

suite "upsert":
  setup:
    db.dropTable(sqlFile, "tb_nullable")
    db.createTable(sqlFile, "tb_nullable")

  test "onconflict do nothing":
    let first = "first value"
    query:
      insert tb_nullable(id = 1, note = ?first)

    let ignored = "should not overwrite"
    query:
      insert tb_nullable(id = 1, note = ?ignored)
      onconflict(id)
      donothing()

    let note = db.getValue(sql"select note from tb_nullable where id = 1")
    check note == first

  test "onconflict do update":
    let first = "old note"
    query:
      insert tb_nullable(id = 2, note = ?first)

    let replacement = "new note"
    query:
      insert tb_nullable(id = 2, note = ?replacement)
      onconflict(id)
      doupdate(note = ?replacement)

    let note = db.getValue(sql"select note from tb_nullable where id = 2")
    check note == replacement

  test "onconflict do update where":
    query:
      insert tb_nullable(id = 3, note = "keep-me")

    query:
      insert tb_nullable(id = 3, note = "replace-me")
      onconflict(id)
      doupdate(note = "replace-me")
      where note != "keep-me"

    let note = db.getValue(sql"select note from tb_nullable where id = 3")
    check note == "keep-me"

static:
  doAssert not compiles(
    (block:
      query:
        insert tb_nullable(id = 100, note = "x")
        where id == 100
    )
  )
