module Internal.TestResult
  ( TestResult (Passed, Failed, Skipped),
    TestFailure (TestFailure),
    concat,
    join,
    passed,
    failed,
    skipped,
    onFail,
    fromPredicate,
    throwFailingTest,
  )
where

import Control.Exception.Safe (Exception, throwIO)
import Data.Dynamic (Typeable)
import qualified Data.Text
import List (List)
import qualified List
import NriPrelude
import qualified Pretty.Diff as Diff
import qualified System.Console.Terminal.Size as Terminal
import qualified Text
import qualified Text.Show.Pretty
import Prelude (IO, Monoid, Semigroup ((<>)), Show, pure, show)

data TestResult
  = Passed
  | Skipped
  | Failed TestFailure

passed :: TestResult
passed = Passed

failed :: Text -> TestResult
failed = Failed << TestFailure

skipped :: TestResult
skipped = Skipped

onFail :: Text -> TestResult -> TestResult
onFail message result =
  case result of
    Passed -> result
    Skipped -> result
    Failed _ -> failed message

concat :: List TestResult -> TestResult
concat xs =
  case xs of
    [] -> Passed
    x : rest -> join x (concat rest)

join :: TestResult -> TestResult -> TestResult
join a b =
  case (a, b) of
    (Passed, Passed) -> Passed
    (Passed, Failed x) -> Failed x
    (Passed, Skipped) -> Skipped
    (Failed x, Failed y) -> Failed (x <> y)
    (Failed x, Passed) -> Failed x
    (Failed x, Skipped) -> Failed x
    (Skipped, Skipped) -> Skipped
    (Skipped, Passed) -> Skipped
    (Skipped, Failed x) -> Failed x

fromPredicate :: Show a => (a -> a -> Bool) -> Text -> a -> a -> IO TestResult
fromPredicate pred funcName actual expected =
  if pred actual expected
    then pure Passed
    else do
      window <- Terminal.size
      let terminalWidth = case window of
            Just Terminal.Window {Terminal.width} -> width - 4 -- indentation
            Nothing -> 80
      Diff.pretty
        Diff.Config
          { Diff.separatorText = Just funcName,
            Diff.wrapping = Diff.Wrap terminalWidth
          }
        (PrettyShow expected)
        (PrettyShow actual)
        |> failed
        |> pure

newtype PrettyShow a = PrettyShow a

instance Show a => Show (PrettyShow a) where
  show (PrettyShow x) = Text.Show.Pretty.ppShow x

-- | Error generated when a test expectation is not met.
newtype TestFailure = TestFailure Text
  deriving (Typeable, Semigroup, Monoid)

instance Exception TestFailure

instance Show TestFailure where
  show (TestFailure err) = Data.Text.unpack (indent err)

throwFailingTest :: TestResult -> IO ()
throwFailingTest result =
  case result of
    Passed -> pure ()
    Skipped -> pure ()
    Failed err -> throwIO err

indent :: Text -> Text
indent = Data.Text.lines >> List.map ("    " <>) >> Text.join "\n"