import unittest, os, strformat
import ormin
import ormin/db_utils
when NimVersion < "1.2.0": import ./compat

let testDir = currentSourcePath.parentDir()

when defined postgre:
  when defined(macosx):
    {.passL: "-Wl,-rpath,/opt/homebrew/lib/postgresql@14".}
  from db_connector/db_postgres import exec, getValue
  const backend = DbBackend.postgre
  importModel(backend, "forum_model_postgres")
  const sqlFileName = "forum_model_postgres.sql"
  let db {.global.} = open("localhost", "test", "test", "test_ormin")
else:
  from db_connector/db_sqlite import exec, getValue
  const backend = DbBackend.sqlite
  importModel(backend, "forum_model_sqlite")
  const sqlFileName = "forum_model_sqlite.sql"
  var memoryPath = testDir & "/" & ":memory:"
  let db {.global.} = open(memoryPath, "", "", "")

var sqlFilePath = Path(testDir & "/" & sqlFileName)

# Fresh schema
db.dropTable(sqlFilePath)
db.createTable(sqlFilePath)

suite &"Transactions ({backend})":

  test "commit on success":
    transaction:
      query:
        insert person(id = ?(101), name = ?"john101", password = ?"p101", email = ?"john101@mail.com", salt = ?"s101", status = ?"ok")
    check db.getValue(sql"select count(*) from person where id = 101") == "1"

  test "rollback on error with manual try except":
    # prepare one row
    query:
      insert person(id = ?(201), name = ?"john201", password = ?"p201", email = ?"john201@mail.com", salt = ?"s201", status = ?"ok")
    # in transaction insert a new row and then violate PK
    try:
      transaction:
        query:
          insert person(id = ?(202), name = ?"john202", password = ?"p202", email = ?"john202@mail.com", salt = ?"s202", status = ?"ok")
        # duplicate key error
        query:
          insert person(id = ?(201), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
      check false # should not reach
    except DbError as e:
      discard
    # both inserts inside the transaction should be rolled back
    check db.getValue(sql"select count(*) from person where id = 202") == "0"
    check db.getValue(sql"select count(*) from person where id = 201 and name = 'dup'") == "0"

  test "rollback on error with else":
    # prepare one row
    var failed = false
    query:
      insert person(id = ?(501), name = ?"john501", password = ?"p501", email = ?"john501@mail.com", salt = ?"s501", status = ?"ok")
    # in transaction insert a new row and then violate PK
    transaction:
      query:
        insert person(id = ?(502), name = ?"john502", password = ?"p502", email = ?"john502@mail.com", salt = ?"s502", status = ?"ok")
      # duplicate key error
      query:
        insert person(id = ?(501), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
      check false # should not reach
    else:
      echo "do something else..."
      failed = true

    check failed
    # both inserts inside the transaction should be rolled back
    check db.getValue(sql"select count(*) from person where id = 502") == "0"
    check db.getValue(sql"select count(*) from person where id = 501 and name = 'dup'") == "0"

  test "commit normally with else":
    # prepare one row
    var failed = false
    query:
      insert person(id = ?(601), name = ?"john601", password = ?"p601", email = ?"john601@mail.com", salt = ?"s601", status = ?"ok")
    # in transaction insert a new row and then violate PK
    transaction:
      query:
        insert person(id = ?(602), name = ?"john602", password = ?"p602", email = ?"john602@mail.com", salt = ?"s602", status = ?"ok")
      query:
        insert person(id = ?(603), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
    else:
      failed = true

    check not failed
    # both inserts inside the transaction should be rolled back
    check db.getValue(sql"select count(*) from person where id = 603") == "1"
    check db.getValue(sql"select count(*) from person where id = 602") == "1"
    check db.getValue(sql"select count(*) from person where id = 601") == "1"

  test "transaction set false on DbError":
    var failed = false
    transaction:
      query:
        insert person(id = ?(301), name = ?"john301", password = ?"p301", email = ?"john301@mail.com", salt = ?"s301", status = ?"ok")
      query:
        insert person(id = ?(301), name = ?"dup", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
    else:
      failed = true
    check failed
    check db.getValue(sql"select count(*) from person where id = 301") == "0"

  test "nested savepoints":
    var failed = false
    transaction:
      query:
        insert person(id = ?(401), name = ?"john401", password = ?"p401", email = ?"john401@mail.com", salt = ?"s401", status = ?"ok")
      var innerOk = true
      transaction:
        query:
          insert person(id = ?(402), name = ?"john402", password = ?"p402", email = ?"john402@mail.com", salt = ?"s402", status = ?"ok")
        query:
          insert person(id = ?(401), name = ?"dup401", password = ?"p", email = ?"e", salt = ?"s", status = ?"x")
      else:
        innerOk = false

      check innerOk == false

      # after inner rollback, we can still insert another row and commit outer
      query:
        insert person(id = ?(403), name = ?"john403", password = ?"p403", email = ?"john403@mail.com", salt = ?"s403", status = ?"ok")
    else:
      failed = true
    check db.getValue(sql"select count(*) from person where id in (401,402,403)") == "2"
