{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Main where

import Control.Exception (bracket, throw, throwIO, try)
import Control.Monad.Logger.CallStack (logWarn, runStderrLoggingT)
import Data.Function ((&))
import Data.Functor ((<&>), void)
import Data.Text (Text)
import Database.PostgreSQL.Simple
  ( Only(..), SqlError(sqlErrorMsg), Connection, close, connectPostgreSQL, execute_, query
  )
import Database.PostgreSQL.Simple.LTree (mkLabel)
import System.Environment (lookupEnv)
import Test.Hspec (describe, expectationFailure, hspec, it)
import Test.QuickCheck (Gen, arbitrary, property, suchThat)
import Test.QuickCheck.Monadic (forAllM, monadicIO, run)

import qualified Data.Bifunctor as Bifunctor
import qualified Data.ByteString.Char8 as C8
import qualified Data.Text as Text
import qualified Database.Postgres.Temp as TmpPostgres

main :: IO ()
main = withTmpPostgres $ \conn -> hspec $ do
  describe "mkLabel" $ do
    it "should only produce valid labels" $
      property $ monadicIO $ forAllM genPGText $ \t ->
        run $ mkLabel t & either
          (const $ verifyTextCannotBeALabel conn t)
          (const $ verifyTextCanBeALabel conn t)
  where
  withTmpPostgres f = do
    let go conn = do
          _ <- execute_ conn "create extension if not exists ltree"
          f conn
    -- Checks for the PGDATABASE env var. If it's not set, use
    -- tmp-postgres. Otherwise, connect to the database
    -- specified via env vars.
    lookupEnv "PGDATABASE" >>= \case
      Nothing ->
        either throwIO pure =<<
          TmpPostgres.with
            (\db ->
                bracket
                (connectPostgreSQL $ TmpPostgres.toConnectionString db)
                close
                go
            )
      Just _ ->
        -- Uses the env vars as specified here -
        -- https://www.postgresql.org/docs/13/libpq-envars.html
        bracket
          (connectPostgreSQL "")
          close
          go

  verifyTextCanBeALabel :: Connection -> Text -> IO ()
  verifyTextCanBeALabel conn t = tryConvertTextToLabelInPG conn t >>= \case
    Left msg -> expectationFailure msg
    Right () -> pure ()

  -- Making this throw an expectationFailure is a bit too strong of a test.
  -- It's probably ok if 'mkLabel' is too strict; it's mostly important that
  -- it's not too lenient. We'll just log a warning for cases found in which
  -- 'mkLabel' is too strict.
  verifyTextCannotBeALabel :: Connection -> Text -> IO ()
  verifyTextCannotBeALabel conn t = tryConvertTextToLabelInPG conn t >>= \case
    Left _ -> pure ()
    Right () -> runStderrLoggingT $
      logWarn $ "PostgreSQL unexpectedly parsed label: " <> Text.pack (show t)

  tryConvertTextToLabelInPG :: Connection -> Text -> IO (Either String ())
  tryConvertTextToLabelInPG conn t =
    try (void $ query @_ @[Int] conn "select ?::ltree where false" $ Only ltree)
      <&>
      Bifunctor.first
        (\(e :: SqlError) ->
          if "syntax error at position " `C8.isPrefixOf` sqlErrorMsg e then
            "Failed to parse label via PostgreSQL: " <> show t
          else
            throw e)
    where
    -- Produce an ltree by joining a label with itself
    ltree = t <> "." <> t

  genAnyText :: Gen Text
  genAnyText = Text.pack <$> arbitrary

  genPGText :: Gen Text
  genPGText = genAnyText `suchThat` (not . Text.isInfixOf "\0")
