{-# LANGUAGE FlexibleInstances #-}
module Test.Hspec.Discover {-# WARNING
  "This module is used by @hspec-discover@.  It is not part of the public API and may change at any time."
  #-} (
  Spec
, hspec
, IsFormatter (..)
, hspecWithFormatter
, postProcessSpec
, describe
, module Prelude
) where

import           Prelude hiding (mapM)
import           Control.Applicative
import           Data.Maybe
import           Data.List
import           Data.Traversable
import           Control.Monad.Trans.State

import           Test.Hspec.Core.Spec
import           Test.Hspec.Core.Runner
import           Test.Hspec.Formatters
import           Test.Hspec.Core.Util (safeTry)

class IsFormatter a where
  toFormatter :: a -> IO Formatter

instance IsFormatter (IO Formatter) where
  toFormatter = id

instance IsFormatter Formatter where
  toFormatter = return

hspecWithFormatter :: IsFormatter a => a -> Spec -> IO ()
hspecWithFormatter formatter spec = do
  f <- toFormatter formatter
  hspecWith defaultConfig {configFormatter = Just f} spec

postProcessSpec :: FilePath -> Spec -> Spec
postProcessSpec = locationHeuristicFromFile

locationHeuristicFromFile :: FilePath -> Spec -> Spec
locationHeuristicFromFile file spec = do
  mInput <- either (const Nothing) Just <$> (runIO . safeTry . readFile) file
  let lookupLoc = maybe (\_ _ _ -> Nothing) (lookupLocation file)  mInput
  runIO (runSpecM spec) >>= fromSpecList . addLoctions lookupLoc

addLoctions :: (Int -> Int -> String -> Maybe Location) -> [SpecTree a] -> [SpecTree a]
addLoctions lookupLoc = map (fmap f) . enumerate
  where
    f :: ((Int, Int), Item a) -> Item a
    f ((n, total), item) = item {itemLocation = itemLocation item <|> lookupLoc n total (itemRequirement item)}

type EnumerateM = State [(String, Int)]

enumerate :: [SpecTree a] -> [Tree (ActionWith a) ((Int, Int), (Item a))]
enumerate tree = (mapM (traverse addPosition) tree >>= mapM (traverse addTotal)) `evalState` []
  where
    addPosition :: Item a -> EnumerateM (Int, Item a)
    addPosition item = (,) <$> getOccurrence (itemRequirement item) <*> pure item

    addTotal :: (Int, Item a) -> EnumerateM ((Int, Int), Item a)
    addTotal (n, item) = do
      total <- getTotal (itemRequirement item)
      return ((n, total), item)

    getTotal :: String -> EnumerateM Int
    getTotal requirement = do
      gets $ fromMaybe err . lookup requirement
      where
        err = error ("Test.Hspec.Discover.getTotal: No entry for requirement " ++ show requirement ++ "!")

    getOccurrence :: String -> EnumerateM Int
    getOccurrence requirement = do
      xs <- get
      let n = maybe 1 succ (lookup requirement xs)
      put ((requirement, n) : filter ((/= requirement) . fst) xs)
      return n

lookupLocation :: FilePath -> String -> Int -> Int -> String -> Maybe Location
lookupLocation file input n total requirement = loc
  where
    loc :: Maybe Location
    loc = Location file <$> line <*> pure 0 <*> pure BestEffort

    line :: Maybe Int
    line = case occurrences of
      xs | length xs == total -> Just (xs !! pred n)
      _ -> Nothing

    occurrences :: [Int]
    occurrences = map fst (filter p inputLines)
      where
        p :: (Int, String) -> Bool
        p = isInfixOf (show requirement) . snd

    inputLines :: [(Int, String)]
    inputLines = zip [1..] (lines input)
