{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE DeriveDataTypeable         #-}
{-# OPTIONS_GHC -XNoCPP -fno-warn-unused-binds  #-}

module Data.API.Test.Migration
    ( migrationTests
    ) where

import           Data.API.Changes
import           Data.API.PP
import           Data.API.Tools
import           Data.API.Test.MigrationData
import           Data.API.Types
import           Data.API.Utils
import qualified Data.API.Value           as Value

import qualified Data.Aeson               as JS
import qualified Data.Aeson.Encode.Pretty as JS
import qualified Codec.Serialise as CBOR
import qualified Codec.CBOR.FlatTerm as CBOR
import qualified Data.ByteString.Char8    as B
import qualified Data.ByteString.Base64   as B64
import qualified Data.ByteString.Lazy.Char8 as BL
import qualified Data.HashMap.Strict      as HMap
import qualified Data.Map                 as Map
import qualified Data.Text                as T
import qualified Data.Text.Encoding       as TE
import           Data.Version
import           Test.Tasty               as Test
import           Test.Tasty.HUnit
import qualified Test.Tasty.QuickCheck    as QC
import           Test.QuickCheck.Property as P


$(generateMigrationKinds changelog "TestDatabaseMigration" "TestRecordMigration" "TestFieldMigration")


-- Test of a whole-database migration: copy data between tables
testDatabaseMigration :: TestDatabaseMigration -> JS.Object -> Either ValueError JS.Object
testDatabaseMigration DuplicateBar x = do
    bar <- HMap.lookup "bar" x ?! CustomMigrationError "missing bar" (JS.Object x)
    return $ HMap.insert "bar2" bar x
testDatabaseMigration DuplicateRecursive x = do
    recur <- HMap.lookup "recur" x ?! CustomMigrationError "missing recur" (JS.Object x)
    return $ HMap.insert "recur2" recur x

testDatabaseMigration' :: TestDatabaseMigration -> Value.Record -> Either ValueError Value.Record
testDatabaseMigration' DuplicateBar r = do
    let x = Value.recordToMap r
    bar <- Map.lookup "bar" x ?! CustomMigrationError "missing bar" JS.Null
    return $ Value.mapToRecord $ Map.insert "bar2" bar x
testDatabaseMigration' DuplicateRecursive r = do
    let x = Value.recordToMap r
    recur <- Map.lookup "recur" x ?! CustomMigrationError "missing recur" JS.Null
    return $ Value.mapToRecord $ Map.insert "recur2" recur x

testDatabaseMigrationSchema :: TestDatabaseMigration -> NormAPI -> Either ApplyFailure (Maybe NormAPI)
testDatabaseMigrationSchema DuplicateBar _ = Right Nothing
testDatabaseMigrationSchema DuplicateRecursive napi =
    let Just recur = Map.lookup (TypeName "Recursive") napi
        Just (NRecordType dbs) = Map.lookup root_ napi
        dbs' = Map.insert (FieldName "recur2") (TyMaybe (TyList (TyName "DuplicateRecursive"))) dbs
    in Right $ Just $ Map.insert (TypeName "DuplicateRecursive") recur $
                      Map.insert root_ (NRecordType dbs') napi


-- Test of a single-record migration: copy the value in the id field
-- onto the end of the c field
testRecordMigration :: TestRecordMigration -> JS.Value -> Either ValueError JS.Value
testRecordMigration CopyIDtoC = mkRecordMigration $ \ x -> do
    i <- HMap.lookup "id" x ?! CustomMigrationError "missing id" (JS.Object x)
    b <- HMap.lookup "c" x  ?! CustomMigrationError "missing b" (JS.Object x)
    r <- case (i, b) of
        (JS.Number j, JS.String t)
            -> return $ JS.String $ t `T.append` T.pack (show (round j :: Int))
        _   -> Left $ CustomMigrationError "bad data" (JS.Object x)
    return $ HMap.insert "c" r x
testRecordMigration DuplicateNew = mkRecordMigration $ \ x -> do
    new <- HMap.lookup "new" x ?! CustomMigrationError "missing new" (JS.Object x)
    return $ HMap.insert "newnew" new x

testRecordMigration' :: TestRecordMigration -> Value.Value -> Either ValueError Value.Value
testRecordMigration' CopyIDtoC = mkRecordMigration' $ \ rec -> do
    let x = Value.recordToMap rec
    i <- Map.lookup "id" x ?! CustomMigrationError "missing id" JS.Null
    b <- Map.lookup "c" x  ?! CustomMigrationError "missing b" JS.Null
    r <- case (i, b) of
        (Value.Int j, Value.String t)
            -> return $ Value.String $ t `T.append` T.pack (show j)
        _   -> Left $ CustomMigrationError "bad data" JS.Null
    return $ Value.mapToRecord $ Map.insert "c" r x
testRecordMigration' DuplicateNew = mkRecordMigration' $ \ rec -> do
    let x = Value.recordToMap rec
    new <- Map.lookup "new" x ?! CustomMigrationError "missing new" JS.Null
    return $ Value.mapToRecord $ Map.insert "newnew" new x

testRecordMigrationSchema :: TestRecordMigration -> NormTypeDecl -> Either ApplyFailure (Maybe NormTypeDecl)
testRecordMigrationSchema CopyIDtoC    = noSchemaChanges
testRecordMigrationSchema DuplicateNew = mkRecordMigrationSchema "Recursive" $ \ r ->
    return $ Just $ Map.insert (FieldName "newnew") (TyBasic BTstring) r

-- Test of a single-field migration: change the type of the field from
-- binary to string by base64-decoding the contents
testFieldMigration :: TestFieldMigration -> JS.Value -> Either ValueError JS.Value
testFieldMigration ConvertBinaryToString v@(JS.String s) =
    case B64.decode (B.pack (T.unpack s)) of
        Left err  -> Left (CustomMigrationError err v)
        Right x -> return (JS.String (T.pack (B.unpack x)))
testFieldMigration ConvertBinaryToString v = Left $ CustomMigrationError "bad data" v

testFieldMigration' :: TestFieldMigration -> Value.Value -> Either ValueError Value.Value
testFieldMigration' ConvertBinaryToString (Value.Bytes bs) = return (Value.String (TE.decodeUtf8 (_Binary bs)))
testFieldMigration' ConvertBinaryToString v = Left $ CustomMigrationError "bad data" (JS.toJSON v)


testMigration :: CustomMigrations JS.Object JS.Value TestDatabaseMigration TestRecordMigration TestFieldMigration
testMigration = CustomMigrations testDatabaseMigration
                                 testDatabaseMigrationSchema
                                 testRecordMigration
                                 testRecordMigrationSchema
                                 testFieldMigration

testMigration' :: CustomMigrations Value.Record Value.Value TestDatabaseMigration TestRecordMigration TestFieldMigration
testMigration' = CustomMigrations testDatabaseMigration'
                                  testDatabaseMigrationSchema
                                  testRecordMigration'
                                  testRecordMigrationSchema
                                  testFieldMigration'


assertMatchesAPI :: String -> API -> JS.Value -> Assertion
assertMatchesAPI x a v = case dataMatchesAPI root_ a v of
    Right () -> return ()
    Left err -> assertFailure (x ++ ": " ++ prettyValueErrorPosition err)

basicMigrationTest :: Assertion
basicMigrationTest = do
    assertMatchesAPI "Start data does not match start API" startSchema startData
    assertMatchesAPI "End data does not match end API"     endSchema   endData
    case migrateDataDump (startSchema, startVersion) (endSchema, DevVersion)
                         changelog testMigration root_ CheckAll startData of
      Right (v, []) | endData == v -> return ()
                    | otherwise    -> assertFailure $ "expected:\n"
                                      ++ BL.unpack (JS.encodePretty endData)
                                      ++ "\nbut got:\n"
                                      ++ BL.unpack (JS.encodePretty v)
      Right (_, ws) -> assertFailure $ "Unexpcted warnings: " ++ show ws
      Left err      -> assertFailure (prettyMigrateFailure err)

applyFailureTest :: (Version, Version, ApplyFailure) -> Test.TestTree
applyFailureTest (ver, ver', expected) =
    testCase (showVersion ver ++ " -> " ++ showVersion ver') $
          case migrateDataDump (startSchema, ver) (endSchema, Release ver')
                               badChangelog testMigration root_ CheckAll startData of
            Right _ -> assertFailure $ "Successful migration!"
            Left (ValidateFailure (ChangelogEntryInvalid _ _ err))
                | err == expected -> return ()
            Left err -> assertFailure $ unlines $ ["Unexpected failure:"]
                        ++ indent (ppLines err) ++ ["Expecting:"]
                        ++ indent (ppLines expected)

migrateFailureTest :: MigrateFailureTest
                    -> Test.TestTree
migrateFailureTest (s, start, end, clog, db, expected) =
    testCase s $ case migrateDataDump start end clog testMigration root_ CheckAll db of
        Right _                 -> assertFailure $ "Successful migration!"
        Left err | expected err -> return ()
                 | otherwise    -> assertFailure $ unlines $ ["Unexpected failure:"]
                                                             ++ indent (ppLines err)


$(generate         startSchema)
$(generateAPITools startSchema
                   [ enumTool
                   , jsonTool'
                   , cborTool
                   , quickCheckTool
                   ])

validMigrationProperty :: DatabaseSnapshot -> P.Result
validMigrationProperty db =
    case migrateDataDump (startSchema, startVersion) (endSchema, DevVersion)
                         changelog testMigration root_ CheckStartAndEnd (JS.toJSON db) of
    Right (v, []) -> case dataMatchesAPI root_ endSchema v of
        Right _   -> succeeded
        Left  err -> failedBecause ("end data does not match API: "
                                    ++ prettyValueErrorPosition err)
    Right (_, ws) -> failedBecause ("migration generated warnings: " ++ show ws)
    Left err      -> failedBecause ("migration failed: " ++ prettyMigrateFailure err)
  where
    failedBecause e = failed { reason = e }

validMigrationProperty' :: DatabaseSnapshot -> P.Result
validMigrationProperty' db =
    case migrateDataDump' (startSchema, startVersion) (endSchema, DevVersion)
                         changelog testMigration' root_ CheckStartAndEnd db_generic of
    Right (v, []) -> case dataMatchesAPI root_ endSchema (JS.toJSON v) of
        Right _   -> succeeded
        Left  err -> failedBecause ("end data does not match API: "
                                    ++ prettyValueErrorPosition err)
    Right (_, ws) -> failedBecause ("migration generated warnings: " ++ show ws)
    Left err      -> failedBecause ("migration failed: " ++ prettyMigrateFailure err)
  where
    failedBecause e = failed { reason = e }

    db_generic = case CBOR.fromFlatTerm (Value.decode (apiNormalForm startSchema) (TyName root_))
                                        (CBOR.toFlatTerm (CBOR.encode db)) of
                   Right v  -> v
                   Left err -> error err

migrationTests :: TestTree
migrationTests = testGroup "Migration"
  [ testCase     "Basic migration using sample changelog" basicMigrationTest
  , testGroup    "Invalid changes"    $ map applyFailureTest   expectedApplyFailures
  , testGroup    "Invalid migrations" $ map migrateFailureTest expectedMigrateFailures
  , QC.testProperty "Valid migrations (JSON)" validMigrationProperty
  , QC.testProperty "Valid migrations (generic)" validMigrationProperty'
  ]