{-# LANGUAGE OverloadedStrings #-}

-- | A test that is not executed as part of this package's test suite but rather
-- acts as a conformance test suit for database specific backend
-- implementations. All backend specific executable packages are expected to
-- have a test suite that runs this test.
module Database.Schema.Migrations.Test.BackendTest
    ( BackendConnection (..)
    , tests
    ) where

import Data.ByteString ( ByteString )

import Control.Monad ( forM_ )
import Test.HUnit

import Database.Schema.Migrations.Migration ( Migration(..), newMigration )
import Database.Schema.Migrations.Backend ( Backend(..) )

-- | A typeclass for database connections that needs to implemented for each
-- specific database type to use this test.
class BackendConnection c where

    -- | Whether this backend supports transactional DDL; if it doesn't,
    -- we'll skip any tests that rely on that behavior.
    supportsTransactionalDDL :: c -> Bool

    -- | Commits the current transaction.
    commit :: c -> IO ()

    -- | Executes an IO action inside a transaction.
    withTransaction :: c -> (c -> IO a) -> IO a

    -- | Retrieves a list of all tables in the current database/scheme.
    getTables :: c -> IO [ByteString]

    catchAll :: c -> (IO a -> IO a -> IO a)

    -- | Returns a backend instance.
    makeBackend :: c -> Backend

testSuite :: BackendConnection bc => Bool -> [bc -> IO ()]
testSuite transactDDL =
    [ isBootstrappedFalseTest
    , bootstrapTest
    , isBootstrappedTrueTest
    , if transactDDL then applyMigrationFailure else (const $ return ())
    , applyMigrationSuccess
    , revertMigrationFailure
    , revertMigrationNothing
    , revertMigrationJust
    ]

tests :: BackendConnection bc => bc -> IO ()
tests conn = do
  let acts = testSuite $ supportsTransactionalDDL conn
  forM_ acts $ \act -> do
               commit conn
               act conn

bootstrapTest :: BackendConnection bc => bc -> IO ()
bootstrapTest conn = do
  let backend = makeBackend conn
  bs <- getBootstrapMigration backend
  applyMigration backend bs
  assertEqual "installed_migrations table exists" ["installed_migrations"] =<< getTables conn
  assertEqual "successfully bootstrapped" [mId bs] =<< getMigrations backend

isBootstrappedTrueTest :: BackendConnection bc => bc -> IO ()
isBootstrappedTrueTest conn = do
  result <- isBootstrapped $ makeBackend conn
  assertBool "Bootstrapped check" result

isBootstrappedFalseTest :: BackendConnection bc => bc -> IO ()
isBootstrappedFalseTest conn = do
  result <- isBootstrapped $ makeBackend conn
  assertBool "Bootstrapped check" $ not result

ignoreSqlExceptions :: BackendConnection bc => bc -> IO a -> IO (Maybe a)
ignoreSqlExceptions conn act =
  (catchAll conn)
    (act >>= return . Just)
    (return Nothing)

applyMigrationSuccess :: BackendConnection bc => bc -> IO ()
applyMigrationSuccess conn = do
    let backend = makeBackend conn

    let m1 = (newMigration "validMigration") { mApply = "CREATE TABLE valid1 (a int)" }

    -- Apply the migrations, ignore exceptions
    withTransaction conn $ \conn' -> applyMigration (makeBackend conn') m1

    -- Check that none of the migrations were installed
    assertEqual "Installed migrations" ["root", "validMigration"] =<< getMigrations backend
    assertEqual "Installed tables" ["installed_migrations", "valid1"] =<< getTables conn

-- |Does a failure to apply a migration imply a transaction rollback?
applyMigrationFailure :: BackendConnection bc => bc -> IO ()
applyMigrationFailure conn = do
    let backend = makeBackend conn

    let m1 = (newMigration "second") { mApply = "CREATE TABLE validButTemporary (a int)" }
        m2 = (newMigration "third") { mApply = "INVALID SQL" }

    -- Apply the migrations, ignore exceptions
    _ <- ignoreSqlExceptions conn $ withTransaction conn $ \conn' -> do
        let backend' = makeBackend conn'
        applyMigration backend' m1
        applyMigration backend' m2

    -- Check that none of the migrations were installed
    assertEqual "Installed migrations" ["root"] =<< getMigrations backend
    assertEqual "Installed tables" ["installed_migrations"] =<< getTables conn

revertMigrationFailure :: BackendConnection bc => bc -> IO ()
revertMigrationFailure conn = do
    let backend = makeBackend conn

    let m1 = (newMigration "second") { mApply = "CREATE TABLE validRMF (a int)"
                                     , mRevert = Just "DROP TABLE validRMF"}
        m2 = (newMigration "third") { mApply = "alter table validRMF add column b int"
                                    , mRevert = Just "INVALID REVERT SQL"}

    applyMigration backend m1
    applyMigration backend m2

    installedBeforeRevert <- getMigrations backend

    commitBackend backend

    -- Revert the migrations, ignore exceptions; the revert will fail,
    -- but withTransaction will roll back.
    _ <- ignoreSqlExceptions conn $ withTransaction conn $ \conn' -> do
        let backend' = makeBackend conn'
        revertMigration backend' m2
        revertMigration backend' m1

    -- Check that none of the migrations were reverted
    assertEqual "successfully roll back failed revert" installedBeforeRevert
        =<< getMigrations backend

revertMigrationNothing :: BackendConnection bc => bc -> IO ()
revertMigrationNothing conn = do
    let backend = makeBackend conn

    let m1 = (newMigration "second") { mApply = "create table revert_nothing (a int)"
                                     , mRevert = Nothing }

    applyMigration backend m1

    installedAfterApply <- getMigrations backend
    assertBool "Check that the migration was applied" $ "second" `elem` installedAfterApply

    -- Revert the migration, which should do nothing EXCEPT remove it
    -- from the installed list
    revertMigration backend m1

    installed <- getMigrations backend
    assertBool "Check that the migration was reverted" $ not $ "second" `elem` installed

revertMigrationJust :: BackendConnection bc => bc -> IO ()
revertMigrationJust conn = do
    let name = "revertable"
        backend = makeBackend conn

    let m1 = (newMigration name) { mApply = "CREATE TABLE the_test_table (a int)"
                                 , mRevert = Just "DROP TABLE the_test_table" }

    applyMigration backend m1

    installedAfterApply <- getMigrations backend
    assertBool "Check that the migration was applied" $ name `elem` installedAfterApply

    -- Revert the migration, which should do nothing EXCEPT remove it
    -- from the installed list
    revertMigration backend m1

    installed <- getMigrations backend
    assertBool "Check that the migration was reverted" $ not $ name `elem` installed
