{-# LANGUAGE TupleSections #-}
{-# LANGUAGE ViewPatterns  #-}

module Test.Database.Setup (
    withTestDB
  , testTomlPath
  , testEnvToPgConnectionInfo
  ) where

import Async.Worker qualified as Worker
import Control.Concurrent.STM.TVar (newTVarIO)
import Data.Maybe (fromJust)
import Data.Pool hiding (withResource)
import Data.Pool qualified as Pool
import Data.String (fromString)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Database.PostgreSQL.Simple qualified as PG
import Database.PostgreSQL.Simple.Options qualified as Client
import Database.Postgres.Temp qualified as Tmp
import Gargantext.Core.Config
import Gargantext.Core.Config.Types
import Gargantext.Core.Config.Utils (readConfig)
import Gargantext.Core.Config.Worker (wsDatabase, wsDefinitions)
import Gargantext.Core.NLP (nlpServerMap)
import Gargantext.Core.NodeStory (mkNodeStoryEnv)
import Gargantext.Core.Worker (initWorkerState)
import Gargantext.Core.Worker.Env (WorkerEnv(..))
import Gargantext.Prelude
import Gargantext.System.Logging (withLoggerIO)
import Paths_gargantext
import Prelude qualified
import Shelly hiding (FilePath, run)
import Shelly qualified as SH
import System.Environment
import Test.Database.Types
import Test.Utils.Db (tmpDBToConnInfo)
import UnliftIO.Temporary (withTempFile)


-- | Test DB settings.
dbUser, dbPassword, dbName :: Prelude.String
dbUser = "gargantua"
dbPassword = "gargantua_test"
dbName = "gargandb_test"

testTomlPath :: IO SettingsFile
testTomlPath = SettingsFile <$> getDataFileName "test-data/test_config.toml"

gargDBSchema :: IO FilePath
gargDBSchema = getDataFileName "devops/postgres/schema.sql"

teardown :: TestEnv -> IO ()
teardown TestEnv{ .. } = do
  killThread test_worker_tid
  destroyAllResources $ _DBHandle test_db
  Tmp.stop $ _DBTmp test_db

-- | Bootstraps the DB, by creating the DB and the schema.
bootstrapDB :: Tmp.DB -> Pool PG.Connection -> GargConfig -> IO ()
bootstrapDB tmpDB pool _cfg = Pool.withResource pool $ \conn -> do
  void $ PG.execute_ conn (fromString $ "ALTER USER \"" <> dbUser <> "\" with PASSWORD '" <> dbPassword <> "'")
  schemaPath <- gargDBSchema
  let connString = Tmp.toConnectionString tmpDB
  (res,ec) <- shelly $ silently $ escaping False $ do
    result <- SH.run "psql" ["-d", "\"" <> TE.decodeUtf8 connString <> "\"", "<", fromString schemaPath]
    (result,) <$> lastExitCode
  unless (ec == 0) $ throwIO (Prelude.userError $ show ec <> ": " <> T.unpack res)

tmpPgConfig :: Tmp.Config
tmpPgConfig = Tmp.defaultConfig <>
  Tmp.optionsToDefaultConfig mempty
    { Client.dbname   = pure dbName
    , Client.user     = pure dbUser
    , Client.password = pure dbPassword
    }

-- | Overrides the 'NotificationsConfig' of the input 'GargConfig' with something suitable
-- for testing. It will replace the bind sites for the CE and the dispatcher with random temp files
-- to be used for IPC communication.
withTestNotificationConfig :: GargConfig -> (GargConfig -> IO a) -> IO a
withTestNotificationConfig cfg action = do
  tmpDir <- fromMaybe "/tmp" <$> lookupEnv "TMPDIR"
  withTempFile tmpDir "ce_test.ipc" $ \(T.pack -> ce_fp) _hdl1 -> do
    withTempFile tmpDir "ds_test.ipc" $ \(T.pack -> ds_fp) _hdl2 -> do

      action $ cfg & gc_notifications_config
                   .~ NotificationsConfig { _nc_central_exchange_bind = "ipc://" <> ce_fp
                                          , _nc_central_exchange_connect = "ipc://" <> ce_fp
                                          , _nc_dispatcher_bind = "ipc://" <> ds_fp
                                          , _nc_dispatcher_connect = "ipc://" <> ds_fp
                                          }

setup :: IO TestEnv
setup = do
  res <- Tmp.startConfig tmpPgConfig
  case res of
    Left err -> Prelude.fail $ show err
    Right db -> do
      let connInfo = tmpDBToConnInfo db
      gargConfig0 <- testTomlPath >>= readConfig
                    -- fix db since we're using tmp-postgres
                    <&> (gc_database_config .~ connInfo)
                    -- <&> (gc_worker . wsDatabase .~ connInfo)
                    <&> (gc_worker . wsDatabase .~ (connInfo { PG.connectDatabase = "pgmq_test" }))
      -- putText $ "[setup] database: " <> show (gargConfig ^. gc_database_config)
      -- putText $ "[setup] worker db: " <> show (gargConfig ^. gc_worker . wsDatabase)
      withTestNotificationConfig gargConfig0 $ \gargConfig -> do
        let log_cfg = gargConfig ^. gc_logging
        let idleTime = 60.0
        let maxResources = 2
        let poolConfig = defaultPoolConfig (PG.connectPostgreSQL (Tmp.toConnectionString db))
                                           PG.close
                                           idleTime
                                           maxResources
        pool <- newPool (setNumStripes (Just 2) poolConfig)
        bootstrapDB db pool gargConfig
        ugen <- emptyCounter
        let test_nodeStory = mkNodeStoryEnv
        withLoggerIO log_cfg $ \logger -> do

          let wPoolConfig = defaultPoolConfig (PG.connectPostgreSQL (Tmp.toConnectionString db))
                                              PG.close
                                              idleTime
                                              maxResources
          wPool <- newPool (setNumStripes (Just 2) wPoolConfig)
          let wNodeStory = mkNodeStoryEnv
          _w_env_job_state <- newTVarIO Nothing
          withLoggerIO log_cfg $ \wioLogger -> do
            let wEnv = WorkerEnv { _w_env_config = gargConfig
                                 , _w_env_logger = wioLogger
                                 , _w_env_pool = wPool
                                 , _w_env_nodeStory = wNodeStory
                                 , _w_env_mail = errorTrace "[wEnv] w_env_mail requested but not available"
                                 , _w_env_nlp = nlpServerMap $ gargConfig ^. gc_nlp_config
                                 , _w_env_job_state }

            wState <- initWorkerState wEnv (fromJust $ head $ gargConfig ^. gc_worker . wsDefinitions)
            test_worker_tid <- forkIO $ Worker.run wState
            pure $ TestEnv { test_db = DBHandle pool db
                           , test_config = gargConfig
                           , test_nodeStory
                           , test_usernameGen = ugen
                           , test_logger = logger
                           , test_worker_tid
                         }

withTestDB :: (TestEnv -> IO ()) -> IO ()
withTestDB = bracket setup teardown

testEnvToPgConnectionInfo :: TestEnv -> PG.ConnectInfo
testEnvToPgConnectionInfo TestEnv{..} =
  PG.ConnectInfo { PG.connectHost     = "0.0.0.0"
                 , PG.connectPort     = fromIntegral $ fromMaybe 5432
                                                     $ getLast
                                                     $ Client.port
                                                     $ Tmp.toConnectionOptions
                                                     $ _DBTmp test_db
                 , PG.connectUser     = dbUser
                 , PG.connectPassword = dbPassword
                 , PG.connectDatabase = dbName
                 }
