 import XCTest
 #if GRDBCUSTOMSQLITE
-    import GRDBCustomSQLite
+@testable import GRDBCustomSQLite
 #else
-    import GRDB
+@testable import GRDB
 #endif
 
 class DatabaseSnapshotTests: GRDBTestCase {
     
+    /// A helper class
+    private class Counter {
+        let dbPool: DatabasePool
+        init(dbPool: DatabasePool) throws {
+            self.dbPool = dbPool
+            try dbPool.write { db in
+                try db.execute(sql: "CREATE TABLE counter(id INTEGER PRIMARY KEY)")
+            }
+        }
+        
+        func increment(_ db: Database) throws {
+            try db.execute(sql: "INSERT INTO counter DEFAULT VALUES")
+        }
+        
+        func value(_ db: Database) throws -> Int {
+            return try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM counter")!
+        }
+    }
+    
+    // MARK: - Creation
+    
+    func testSnapshotCanReadBeforeDatabaseModification() throws {
+        let dbPool = try makeDatabasePool()
+        let snapshot = try dbPool.makeSnapshot()
+        try XCTAssertEqual(snapshot.read { try $0.tableExists("foo") }, false)
+    }
+    
+    func testSnapshotCreatedFromMainQueueCanRead() throws {
+        let dbPool = try makeDatabasePool()
+        let counter = try Counter(dbPool: dbPool)
+        let snapshot = try dbPool.makeSnapshot()
+        try XCTAssertEqual(snapshot.read(counter.value), 0)
+    }
+    
+    func testSnapshotCreatedFromWriterOutsideOfTransactionCanRead() throws {
+        let dbPool = try makeDatabasePool()
+        let counter = try Counter(dbPool: dbPool)
+        let snapshot = try dbPool.writeWithoutTransaction { db -> DatabaseSnapshot in
+            XCTAssertFalse(db.isInsideTransaction)
+            let snapshot = try dbPool.makeSnapshot()
+            try counter.increment(db)
+            return snapshot
+        }
+        try XCTAssertEqual(snapshot.read(counter.value), 0)
+    }
+    
+    func testSnapshotCreatedFromReaderTransactionCanRead() throws {
+        let dbPool = try makeDatabasePool()
+        let counter = try Counter(dbPool: dbPool)
+        let snapshot = try dbPool.read { db -> DatabaseSnapshot in
+            XCTAssertTrue(db.isInsideTransaction)
+            return try dbPool.makeSnapshot()
+        }
+        try XCTAssertEqual(snapshot.read(counter.value), 0)
+    }
+    
+    func testSnapshotCreatedFromReaderOutsideOfTransactionCanRead() throws {
+        let dbPool = try makeDatabasePool()
+        let counter = try Counter(dbPool: dbPool)
+        let snapshot = try dbPool.unsafeRead { db -> DatabaseSnapshot in
+            XCTAssertFalse(db.isInsideTransaction)
+            return try dbPool.makeSnapshot()
+        }
+        try XCTAssertEqual(snapshot.read(counter.value), 0)
+    }
+    
+    func testSnapshotCreatedFromTransactionObserver() throws {
+        // Creating a snapshot from a didCommit callback is an important use
+        // case. But we know SQLite snapshots created with
+        // sqlite3_snapshot_get() require a transaction. This means that
+        // creating a snapshot will open a transaction. We must make sure this
+        // transaction does not create any deadlock of reentrancy issue with
+        // transaction observers.
+        class Observer: TransactionObserver {
+            let dbPool: DatabasePool
+            var snapshot: DatabaseSnapshot
+            init(dbPool: DatabasePool, snapshot: DatabaseSnapshot) {
+                self.dbPool = dbPool
+                self.snapshot = snapshot
+            }
+            
+            func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool { return false }
+            func databaseDidChange(with event: DatabaseEvent) { }
+            func databaseDidCommit(_ db: Database) {
+                snapshot = try! dbPool.makeSnapshot()
+            }
+            func databaseDidRollback(_ db: Database) { }
+        }
+        let dbPool = try makeDatabasePool()
+        let counter = try Counter(dbPool: dbPool)
+        let observer = try Observer(dbPool: dbPool, snapshot: dbPool.makeSnapshot())
+        dbPool.add(transactionObserver: observer)
+        try XCTAssertEqual(observer.snapshot.read(counter.value), 0)
+        try dbPool.write(counter.increment)
+        try XCTAssertEqual(observer.snapshot.read(counter.value), 1)
+    }
+    
+    // MARK: - Behavior
+    
     func testSnapshotIsReadOnly() throws {
         let dbPool = try makeDatabasePool()
         let snapshot = try dbPool.makeSnapshot()
         do {
             try snapshot.read { db in
-                try db.create(table: "t") { $0.column("id", .integer).primaryKey() }
+                try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY")
             }
             XCTFail("Expected error")
-        } catch is DatabaseError {
-        } catch {
-            XCTFail("Unexpected error: \(error)")
-        }
-    }
-    
-    func testSnapshotSeesLatestTransaction() throws {
-        let dbPool = try makeDatabasePool()
+        } catch is DatabaseError { }
+    }
+    
+    func testSnapshotIsImmutable() throws {
+        let dbPool = try makeDatabasePool()
+        let counter = try Counter(dbPool: dbPool)
         try dbPool.writeWithoutTransaction { db in
-            try db.create(table: "t") { $0.column("id", .integer).primaryKey() }
-            try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
+            try counter.increment(db)
             let snapshot = try dbPool.makeSnapshot()
-            try snapshot.read { db in
-                try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 1)
-            }
-            try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
-            try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 2)
-            try snapshot.read { db in
-                try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 1)
-            }
-        }
-    }
-    
-    func testSnapshotCreatedOutsideOfWriterQueue() throws {
-        let dbPool = try makeDatabasePool()
-        try dbPool.write { db in
-            try db.create(table: "t") { $0.column("id", .integer).primaryKey() }
-            try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
-        }
-        
-        let snapshot = try dbPool.makeSnapshot()
-        try snapshot.read { db in
-            try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 1)
-        }
-        try dbPool.write { db in
-            try db.execute(sql: "INSERT INTO t DEFAULT VALUES")
-            try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 2)
-        }
-        try snapshot.read { db in
-            try XCTAssertEqual(Int.fetchOne(db, sql: "SELECT COUNT(*) FROM t")!, 1)
-        }
-    }
+            try counter.increment(db)
+            try XCTAssertEqual(counter.value(db), 2)
+            try XCTAssertEqual(snapshot.read(counter.value), 1)
+            try XCTAssertEqual(dbPool.read(counter.value), 2)
+            try XCTAssertEqual(snapshot.read(counter.value), 1)
+            try XCTAssertEqual(counter.value(db), 2)
+            try XCTAssertEqual(dbPool.read(counter.value), 2)
+        }
+    }
+    
+    // MARK: - Functions
     
     func testSnapshotInheritPoolFunctions() throws {
         let dbPool = try makeDatabasePool()

            XCTAssertEqual(try String.fetchAll(db, sql: "SELECT text FROM items ORDER BY text COLLATE reverse"), ["c", "b", "a"])
        }
    }

    func testSnapshotCollations() throws {
        let dbPool = try makeDatabasePool()
        try dbPool.write { db in

            XCTAssertEqual(label, "Toreador.snapshot.2")
        }
    }
    
    // MARK: - Checkpoints
    
    func testAutomaticCheckpointDoesNotInvalidateSnapshot() throws {
        let dbPool = try makeDatabasePool()
        let counter = try Counter(dbPool: dbPool)
        try dbPool.write(counter.increment)
        let snapshot = try dbPool.makeSnapshot()
        try XCTAssertEqual(snapshot.read(counter.value), 1)
        try dbPool.writeWithoutTransaction { db in
            // 1000 is enough to trigger automatic snapshot
            for _ in 0..<1000 {
                try counter.increment(db)
            }
        }
        try XCTAssertEqual(snapshot.read(counter.value), 1)
    }
    
    func testPassiveCheckpointDoesNotInvalidateSnapshot() throws {
        let dbPool = try makeDatabasePool()
        let counter = try Counter(dbPool: dbPool)
        try dbPool.write(counter.increment)
        let snapshot = try dbPool.makeSnapshot()
        try? dbPool.checkpoint(.passive) // ignore if error or not, that's not the point
        try XCTAssertEqual(snapshot.read(counter.value), 1)
        try dbPool.write(counter.increment)
        try XCTAssertEqual(snapshot.read(counter.value), 1)
    }
    
    func testFullCheckpointDoesNotInvalidateSnapshot() throws {
        let dbPool = try makeDatabasePool()
        let counter = try Counter(dbPool: dbPool)
        try dbPool.write(counter.increment)
        let snapshot = try dbPool.makeSnapshot()
        try? dbPool.checkpoint(.full) // ignore if error or not, that's not the point
        try XCTAssertEqual(snapshot.read(counter.value), 1)
        try dbPool.write(counter.increment)
        try XCTAssertEqual(snapshot.read(counter.value), 1)
    }
    
    func testRestartCheckpointDoesNotInvalidateSnapshot() throws {
        let dbPool = try makeDatabasePool()
        let counter = try Counter(dbPool: dbPool)
        try dbPool.write(counter.increment)
        let snapshot = try dbPool.makeSnapshot()
        try? dbPool.checkpoint(.restart) // ignore if error or not, that's not the point
        try XCTAssertEqual(snapshot.read(counter.value), 1)
        try dbPool.write(counter.increment)
        try XCTAssertEqual(snapshot.read(counter.value), 1)
    }
    
    func testTruncateCheckpointDoesNotInvalidateSnapshot() throws {
        let dbPool = try makeDatabasePool()
        let counter = try Counter(dbPool: dbPool)
        try dbPool.write(counter.increment)
        let snapshot = try dbPool.makeSnapshot()
        try? dbPool.checkpoint(.truncate) // ignore if error or not, that's not the point
        try XCTAssertEqual(snapshot.read(counter.value), 1)
        try dbPool.write(counter.increment)
        try XCTAssertEqual(snapshot.read(counter.value), 1)
    }
    
    // MARK: - Schema Cache
    
    func testSnapshotSchemaCache() throws {
        let dbPool = try makeDatabasePool()
        try dbPool.write { db in
            try db.execute(sql: "CREATE TABLE t(id INTEGER PRIMARY KEY)")
        }
        let snapshot = try dbPool.makeSnapshot()
        try snapshot.read { db in
            // Schema cache is updated
            XCTAssertNil(db.schemaCache.primaryKey("t"))
            _ = try db.primaryKey("t")
            XCTAssertNotNil(db.schemaCache.primaryKey("t"))
        }
        snapshot.read { db in
            // Schema cache is not cleared between reads
            XCTAssertNotNil(db.schemaCache.primaryKey("t"))
        }
    }
}
