###############################################################################
#
# session.eagle --
#
# Written by Joe Mistachkin.
# Released to the public domain, use at your own risk!
#
###############################################################################

package require Eagle
package require Eagle.Library
package require Eagle.Test

runTestPrologue

###############################################################################

package require System.Data.SQLite.Test
runSQLiteTestPrologue

###############################################################################

proc getSomeText { random count } {
  set items [list \
      Alpha Bravo Charlie Delta Echo Foxtrot Golf Hotel \
      India Juliet Kilo Lima Mike November Oscar Papa \
      Quebec Romeo Sierra Tango Uniform Victor Whiskey X-ray \
      Yankee Zulu]

  if {!$random} then {
    variable some_text_index

    if {![info exists some_text_index]} then {
      set some_text_index 0
    }
  }

  set length [llength $items]
  set result [list]

  for {set i 0} {$i < $count} {incr i} {
    if {$random} then {
      set item [lindex $items [expr {int(rand() * $length)}]]
    } else {
      set item [lindex $items $some_text_index]
      incr some_text_index

      if {$some_text_index >= $length} then {
        set some_text_index 0
      }
    }

    lappend result $item
  }

  return $result
}

###############################################################################

proc cleanupSomeText {} {
  variable some_text_index
  unset -nocomplain some_text_index
}

###############################################################################

proc forDisplay { object member } {
  set isArray false

  if {[isObjectHandle $object] && $object ne "null"} then {
    set value [object invoke -create -- $object $member]

    if {[isObjectHandle $value] && $value ne "null"} then {
      set isArray [object invoke -- $value GetType.IsArray]
    }
  } else {
    set value null
  }

  if {$isArray} then {
    return [object invoke -flags +NonPublic \
        -parametertypes Array System.Data.SQLite.HelperMethods \
        ToDisplayString $value]
  } else {
    return [object invoke -flags +NonPublic \
        System.Data.SQLite.HelperMethods ToDisplayString $value]
  }
}

###############################################################################

proc createChangeSetForSession { session } {
  if {[isObjectHandle $session] && $session ne "null"} then {
    set byteArray null
    $session -alias CreateChangeSet byteArray

    set rawData [createByteArray [arrayToList byteArray]]
    object removeref $rawData

    return $rawData
  }

  return null
}

###############################################################################

proc metadataItemToString { item includeValues } {
  set result [list]

  if {[isObjectHandle $item] && $item ne "null"} then {
    lappend result TableName [$item TableName]
    lappend result NumberOfColumns [$item NumberOfColumns]
    lappend result OperationCode [$item OperationCode]
    lappend result Indirect [$item Indirect]

    lappend result PrimaryKeyColumns \
        [forDisplay $item PrimaryKeyColumns]

    if {$includeValues} then {
      set numberOfColumns [$item NumberOfColumns]

      for {set index 0} {$index < $numberOfColumns} {incr index} {
        set oldValue [$item GetOldValue $index]

        lappend result OldValue $index \
            [forDisplay $oldValue GetObject]

        set newValue [$item GetNewValue $index]

        lappend result NewValue $index \
            [forDisplay $newValue GetObject]

        set conflictValue [$item GetConflictValue $index]

        lappend result ConflictValue $index \
            [forDisplay $conflictValue GetObject]
      }
    }
  }

  return $result
}

###############################################################################

proc changeSetToString { changeSet includeValues } {
  set result [list]

  if {[isObjectHandle $changeSet] && $changeSet ne "null"} then {
    object foreach -alias item $changeSet {
      lappend result [metadataItemToString $item $includeValues]
    }
  }

  return $result
}

###############################################################################

proc matchSession { connection session expr } {
  if {[isObjectHandle $session] && $session ne "null"} then {
    if {![$session IsEmpty]} then {
      set rawData [createChangeSetForSession $session]
      object removeref $rawData

      if {[isObjectHandle $rawData] && $rawData ne "null"} then {
        return [matchChangeSet [set changeSet \
            [$connection -alias CreateChangeSet $rawData]] $expr]
      }
    }
  }

  return false
}

###############################################################################

proc matchChangeSet { changeSet expr } {
  if {[isObjectHandle $changeSet] && $changeSet ne "null"} then {
    object foreach -alias item $changeSet {
      if {[expr $expr]} then {return true}
    }
  }

  return false
}

###############################################################################

proc createTheSchema { db } {
  sql execute $db {
    CREATE TABLE t1(x INTEGER PRIMARY KEY, y TEXT);
    CREATE TABLE t2(x INTEGER PRIMARY KEY, y TEXT);
  }
}

###############################################################################

proc makeSomeChanges { db table types random {rowId ""} {count 5} } {
  foreach type $types {
    switch -nocase -- $type {
      insert {
        set text [appendArgs "inserted: " [getSomeText $random $count]]

        if {[string is integer -strict $rowId]} then {
          sql execute $db [subst {
            INSERT INTO ${table}(x, y) VALUES(?, ?);
          }] [list param1 Int32 $rowId] [list param2 String $text]
        } else {
          sql execute $db [subst {
            INSERT INTO ${table}(y) VALUES(?);
          }] [list param1 String $text]
        }
      }
      update {
        set text [appendArgs "updated: " [getSomeText $random $count]]

        if {[string is integer -strict $rowId]} then {
          sql execute $db [subst {
            UPDATE ${table} SET y = ? WHERE x = ?;
          }] [list param1 String $text] [list param2 Int32 $rowId]
        } else {
          sql execute $db [subst {
            UPDATE ${table} SET y = ? WHERE x NOT IN (
              (SELECT MIN(x) FROM ${table}), (SELECT MAX(x) FROM ${table})
            );
          }] [list param1 String $text]
        }
      }
      delete {
        if {[string is integer -strict $rowId]} then {
          sql execute $db [subst {
            DELETE FROM ${table} WHERE x = ?;
          }] [list param1 Int32 $rowId]
        } else {
          sql execute $db [subst {
            DELETE FROM ${table} WHERE x = (SELECT MIN(x) FROM ${table});
          }]
        }
      }
    }
  }
}

###############################################################################

proc captureChangeSetRawData { connection databaseName tableName script } {
  if {![isObjectHandle $connection] || $connection eq "null"} then {
    error "connection is invalid"
  }

  set session [$connection -alias CreateSession $databaseName]
  $session AttachTable $tableName

  if {![isObjectHandle $session] || $session eq "null"} then {
    error "cannot create session"
  }

  catch {uplevel 1 $script}
  set rawData [createChangeSetForSession $session]
  object removeref $rawData

  return $rawData
}

###############################################################################

proc getChangeSetFileName { {suffix ""} } {
  return [file join \
      [getTemporaryDirectory] [appendArgs changes $suffix .bin]]
}

###############################################################################

proc arrayToList { varName } {
  set result [list]

  upvar 1 $varName array

  if {[array exists array]} then {
    foreach name [lsort -integer [array names array]] {
      lappend result $array($name)
    }
  }

  return $result
}

###############################################################################

proc createByteArray { list } {
  set length [llength $list]
  set result [object create -alias System.Byte\[\] $length]

  for {set index 0} {$index < $length} {incr index} {
    set element [lindex $list $index]
    set value [object invoke -create Byte Parse $element]
    $result SetValue $value $index
  }

  return $result
}

###############################################################################

proc tableFilterCallbackT1 { clientData name } {
  lappend ::callbackResults [object invoke -create \
      System.Boolean Parse [expr {[$name ToString] in [list t1]}]]

  return [lindex $::callbackResults end]
}

###############################################################################

proc conflictCallback { clientData type item } {
  set result Abort

  if {[isObjectHandle $item] && $item ne "null"} then {
    set result Omit

    if {[$item OperationCode] ne "Delete"} then {
      if {[isObjectHandle $type] && $type ne "null" && \
          [$type ToString] in [list Data Conflict]} then {
        set result Replace
      }
    }
  }

  lappend ::callbackResults [object invoke -create Enum Parse \
      System.Data.SQLite.SQLiteChangeSetConflictResult $result false]

  return [lindex $::callbackResults end]
}

###############################################################################

runTest {test session-1.1 {basic session extension usage} -setup {
  setupDb [set fileName(0) session-1.1.db]

  set fileName(1) [getChangeSetFileName 1]
  set fileName(2) [getChangeSetFileName 2]

  cleanupSomeText
} -body {
  createTheSchema $db
  makeSomeChanges $db t1 [list insert] true

  set connection [getDbConnection]

  set session [$connection -alias CreateSession main]
  $session AttachTable null

  makeSomeChanges $db t1 [list insert update delete] true

  set rawData [createChangeSetForSession $session]
  object removeref $rawData

  set stream(1) [object create -alias \
      System.IO.FileStream $fileName(1) Create Write]

  $stream(1) Write $rawData 0 [$rawData Length]
  $stream(1) Flush; $stream(1) Close

  set stream(2) [object create -alias \
      System.IO.FileStream $fileName(2) Create Write]

  $session -alias CreateChangeSet $stream(2)
  $stream(2) Flush; $stream(2) Close

  list [expr {[file size $fileName(1)] > 0}] \
      [string equal [readFile $fileName(1)] [readFile $fileName(2)]]
} -cleanup {
  cleanupSomeText

  unset -nocomplain stream rawData byteArray session

  freeDbConnection

  unset -nocomplain connection

  cleanupFile $fileName(2)
  cleanupFile $fileName(1)

  cleanupDb $fileName(0)

  unset -nocomplain db fileName
} -constraints {eagle command.object monoBug28 command.sql compile.DATA SQLite\
System.Data.SQLite SQLiteInterop\
defineConstant.System.Data.SQLite.INTEROP_SESSION_EXTENSION} -result \
{True True}}

###############################################################################

runTest {test session-1.2 {session change set enumeration} -setup {
  setupDb [set fileName session-1.2.db]

  cleanupSomeText
} -body {
  createTheSchema $db
  makeSomeChanges $db t1 [list insert insert insert] false

  set connection [getDbConnection]

  set session [$connection -alias CreateSession main]
  $session AttachTable null

  makeSomeChanges $db t1 [list insert update delete] false

  set rawData [createChangeSetForSession $session]
  object removeref $rawData

  set changeSet(1) [$connection -alias CreateChangeSet $rawData]
  set changeSet(2) [$changeSet(1) -alias Invert]

  list [changeSetToString $changeSet(1) true] \
      [changeSetToString $changeSet(2) true]
} -cleanup {
  cleanupSomeText

  unset -nocomplain changeSet rawData byteArray session

  freeDbConnection

  unset -nocomplain connection

  cleanupDb $fileName

  unset -nocomplain db fileName
} -constraints {eagle command.object monoBug28 command.sql compile.DATA SQLite\
System.Data.SQLite SQLiteInterop\
defineConstant.System.Data.SQLite.INTEROP_SESSION_EXTENSION} -result \
{{{TableName t1 NumberOfColumns 2 OperationCode Delete Indirect False\
PrimaryKeyColumns {[True, False]} OldValue 0 1 NewValue 0 <nullObject>\
ConflictValue 0 <nullObject> OldValue 1 {"inserted: Alpha Bravo Charlie Delta\
Echo"} NewValue 1 <nullObject> ConflictValue 1 <nullObject>} {TableName t1\
NumberOfColumns 2 OperationCode Update Indirect False PrimaryKeyColumns {[True,\
False]} OldValue 0 2 NewValue 0 <nullObject> ConflictValue 0 <nullObject>\
OldValue 1 {"inserted: Foxtrot Golf Hotel India Juliet"} NewValue 1 {"updated:\
Uniform Victor Whiskey X-ray Yankee"} ConflictValue 1 <nullObject>} {TableName\
t1 NumberOfColumns 2 OperationCode Update Indirect False PrimaryKeyColumns\
{[True, False]} OldValue 0 3 NewValue 0 <nullObject> ConflictValue 0\
<nullObject> OldValue 1 {"inserted: Kilo Lima Mike November Oscar"} NewValue 1\
{"updated: Uniform Victor Whiskey X-ray Yankee"} ConflictValue 1 <nullObject>}\
{TableName t1 NumberOfColumns 2 OperationCode Insert Indirect False\
PrimaryKeyColumns {[True, False]} OldValue 0 <nullObject> NewValue 0 4\
ConflictValue 0 <nullObject> OldValue 1 <nullObject> NewValue 1 {"inserted:\
Papa Quebec Romeo Sierra Tango"} ConflictValue 1 <nullObject>}} {{TableName t1\
NumberOfColumns 2 OperationCode Insert Indirect False PrimaryKeyColumns {[True,\
False]} OldValue 0 <nullObject> NewValue 0 1 ConflictValue 0 <nullObject>\
OldValue 1 <nullObject> NewValue 1 {"inserted: Alpha Bravo Charlie Delta Echo"}\
ConflictValue 1 <nullObject>} {TableName t1 NumberOfColumns 2 OperationCode\
Update Indirect False PrimaryKeyColumns {[True, False]} OldValue 0 2 NewValue 0\
<nullObject> ConflictValue 0 <nullObject> OldValue 1 {"updated: Uniform Victor\
Whiskey X-ray Yankee"} NewValue 1 {"inserted: Foxtrot Golf Hotel India Juliet"}\
ConflictValue 1 <nullObject>} {TableName t1 NumberOfColumns 2 OperationCode\
Update Indirect False PrimaryKeyColumns {[True, False]} OldValue 0 3 NewValue 0\
<nullObject> ConflictValue 0 <nullObject> OldValue 1 {"updated: Uniform Victor\
Whiskey X-ray Yankee"} NewValue 1 {"inserted: Kilo Lima Mike November Oscar"}\
ConflictValue 1 <nullObject>} {TableName t1 NumberOfColumns 2 OperationCode\
Delete Indirect False PrimaryKeyColumns {[True, False]} OldValue 0 4 NewValue 0\
<nullObject> ConflictValue 0 <nullObject> OldValue 1 {"inserted: Papa Quebec\
Romeo Sierra Tango"} NewValue 1 <nullObject> ConflictValue 1 <nullObject>}}}}

###############################################################################

runTest {test session-1.3 {session enabled/disabled state} -setup {
  setupDb [set fileName session-1.3.db]

  cleanupSomeText
} -body {
  createTheSchema $db
  makeSomeChanges $db t1 [list insert] false

  set connection [getDbConnection]

  set session [$connection -alias CreateSession main]
  lappend result IsEnabled [$session IsEnabled]

  $session AttachTable null
  lappend result IsEnabled [$session IsEnabled]

  $session SetToDisabled
  lappend result IsEnabled [$session IsEnabled]

  makeSomeChanges $db t1 [list insert] false
  lappend result IsEmpty [$session IsEmpty]

  set rawData [createChangeSetForSession $session]
  object removeref $rawData

  lappend result Length [$rawData Length]

  $session SetToEnabled
  lappend result IsEnabled [$session IsEnabled]

  makeSomeChanges $db t1 [list insert] false
  lappend result IsEmpty [$session IsEmpty]

  set rawData [createChangeSetForSession $session]
  object removeref $rawData

  set changeSet(1) [$connection -alias CreateChangeSet $rawData]
  lappend result [changeSetToString $changeSet(1) false]
} -cleanup {
  cleanupSomeText

  unset -nocomplain result changeSet rawData byteArray session

  freeDbConnection

  unset -nocomplain connection

  cleanupDb $fileName

  unset -nocomplain db fileName
} -constraints {eagle command.object monoBug28 command.sql compile.DATA SQLite\
System.Data.SQLite SQLiteInterop\
defineConstant.System.Data.SQLite.INTEROP_SESSION_EXTENSION} -result {IsEnabled\
True IsEnabled True IsEnabled False IsEmpty True Length 0 IsEnabled True\
IsEmpty False {{TableName t1 NumberOfColumns 2 OperationCode Insert Indirect\
False PrimaryKeyColumns {[True, False]}}}}}

###############################################################################

runTest {test session-1.4 {session direct/indirect state} -setup {
  setupDb [set fileName session-1.4.db]

  cleanupSomeText
} -body {
  createTheSchema $db
  makeSomeChanges $db t1 [list insert] false

  set connection [getDbConnection]

  set session [$connection -alias CreateSession main]
  lappend result IsIndirect [$session IsIndirect]

  $session AttachTable null
  lappend result IsIndirect [$session IsIndirect]

  $session SetToIndirect
  lappend result IsIndirect [$session IsIndirect]

  makeSomeChanges $db t1 [list insert] false
  lappend result IsEmpty [$session IsEmpty]

  set rawData [createChangeSetForSession $session]
  object removeref $rawData

  $session SetToDirect
  lappend result IsIndirect [$session IsIndirect]

  makeSomeChanges $db t1 [list insert] false
  lappend result IsEmpty [$session IsEmpty]

  set rawData [createChangeSetForSession $session]
  object removeref $rawData

  set changeSet(1) [$connection -alias CreateChangeSet $rawData]
  lappend result [changeSetToString $changeSet(1) false]
} -cleanup {
  cleanupSomeText

  unset -nocomplain result changeSet rawData byteArray session

  freeDbConnection

  unset -nocomplain connection

  cleanupDb $fileName

  unset -nocomplain db fileName
} -constraints {eagle command.object monoBug28 command.sql compile.DATA SQLite\
System.Data.SQLite SQLiteInterop\
defineConstant.System.Data.SQLite.INTEROP_SESSION_EXTENSION} -result \
{IsIndirect False IsIndirect False IsIndirect True IsEmpty False IsIndirect\
False IsEmpty False {{TableName t1 NumberOfColumns 2 OperationCode Insert\
Indirect True PrimaryKeyColumns {[True, False]}} {TableName t1 NumberOfColumns\
2 OperationCode Insert Indirect False PrimaryKeyColumns {[True, False]}}}}}

###############################################################################

runTest {test session-1.5 {session table filter} -setup {
  setupDb [set fileName session-1.5.db]

  cleanupSomeText
} -body {
  createTheSchema $db
  makeSomeChanges $db t1 [list insert] false

  set connection [getDbConnection]

  set session [$connection -alias CreateSession main]

  $session -marshalflags +DynamicCallback \
      SetTableFilter tableFilterCallbackT1 null

  makeSomeChanges $db t2 [list insert] false
  lappend result IsEmpty [$session IsEmpty]

  lappend result MatchT2 [matchSession $connection $session {
    [$item TableName] eq "t2"
  }]

  makeSomeChanges $db t1 [list insert] false
  lappend result IsEmpty [$session IsEmpty]

  lappend result MatchT1 [matchSession $connection $session {
    [$item TableName] eq "t1"
  }]

  $session SetTableFilter null null

  makeSomeChanges $db t2 [list insert] false
  lappend result IsEmpty [$session IsEmpty]

  lappend result MatchT2 [matchSession $connection $session {
    [$item TableName] eq "t2"
  }]

  makeSomeChanges $db t1 [list insert] false
  lappend result IsEmpty [$session IsEmpty]

  lappend result MatchT1 [matchSession $connection $session {
    [$item TableName] eq "t1"
  }]

  set result
} -cleanup {
  cleanupSomeText

  unset -nocomplain result session

  freeDbConnection

  unset -nocomplain connection

  cleanupDb $fileName

  catch {object removecallback tableFilterCallbackT1}

  catch {
    foreach callbackResult $callbackResults {
      catch {object dispose $callbackResult}
    }
  }

  unset -nocomplain callbackResult callbackResults db fileName
} -constraints {eagle command.object monoBug28 command.sql compile.DATA SQLite\
System.Data.SQLite SQLiteInterop\
defineConstant.System.Data.SQLite.INTEROP_SESSION_EXTENSION} -result {IsEmpty\
True MatchT2 false IsEmpty False MatchT1 true IsEmpty False MatchT2 true\
IsEmpty False MatchT1 true}}

###############################################################################

runTest {test session-1.6 {combine and apply change sets} -setup {
  setupDb [set fileName session-1.6.db]

  cleanupSomeText
} -body {
  createTheSchema $db
  makeSomeChanges $db t1 [list insert insert] false

  set connection [getDbConnection]

  set rawData(1) [captureChangeSetRawData $connection main null {
    makeSomeChanges $db t1 [list delete] false 1
  }]; object removeref $rawData(1)

  makeSomeChanges $db t1 [list insert] false 1

  set rawData(2) [captureChangeSetRawData $connection main null {
    makeSomeChanges $db t1 [list insert] false
  }]; object removeref $rawData(2)

  set changeSet(1) [$connection -alias CreateChangeSet $rawData(1)]
  set changeSet(2) [$connection -alias CreateChangeSet $rawData(2)]
  set changeSet(3) [$changeSet(1) -alias CombineWith $changeSet(2)]

  $changeSet(3) -marshalflags +DynamicCallback Apply conflictCallback null

  list [changeSetToString $changeSet(3) true] Data [sql execute -execute \
      reader -format list $db {SELECT x, y FROM t1 ORDER BY x;}]
} -cleanup {
  cleanupSomeText

  unset -nocomplain changeSet rawData

  freeDbConnection

  unset -nocomplain connection

  cleanupDb $fileName

  catch {object removecallback conflictCallback}

  catch {
    foreach callbackResult $callbackResults {
      catch {object dispose $callbackResult}
    }
  }

  unset -nocomplain callbackResult callbackResults db fileName
} -constraints {eagle command.object monoBug28 command.sql compile.DATA SQLite\
System.Data.SQLite SQLiteInterop\
defineConstant.System.Data.SQLite.INTEROP_SESSION_EXTENSION} -result \
{{{TableName t1 NumberOfColumns 2 OperationCode Delete Indirect False\
PrimaryKeyColumns {[True, False]} OldValue 0 1 NewValue 0 <nullObject>\
ConflictValue 0 <nullObject> OldValue 1 {"inserted: Alpha Bravo Charlie Delta\
Echo"} NewValue 1 <nullObject> ConflictValue 1 <nullObject>} {TableName t1\
NumberOfColumns 2 OperationCode Insert Indirect False PrimaryKeyColumns {[True,\
False]} OldValue 0 <nullObject> NewValue 0 3 ConflictValue 0 <nullObject>\
OldValue 1 <nullObject> NewValue 1 {"inserted: Papa Quebec Romeo Sierra Tango"}\
ConflictValue 1 <nullObject>}} Data {1 {inserted: Kilo Lima Mike November\
Oscar} 2 {inserted: Foxtrot Golf Hotel India Juliet} 3 {inserted: Papa Quebec\
Romeo Sierra Tango}}}}

###############################################################################

rename conflictCallback ""
rename tableFilterCallbackT1 ""
rename createByteArray ""
rename arrayToList ""
rename getChangeSetFileName ""
rename captureChangeSetRawData ""
rename makeSomeChanges ""
rename createTheSchema ""
rename matchChangeSet ""
rename matchSession ""
rename changeSetToString ""
rename metadataItemToString ""
rename createChangeSetForSession ""
rename forDisplay ""
rename cleanupSomeText ""
rename getSomeText ""

###############################################################################

runSQLiteTestEpilogue
runTestEpilogue
