{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}

module Main where

import Control.Concurrent
import Control.Exception
import Control.Monad
import Data.Aeson
import qualified Data.ByteString.Lazy.Char8 as BS
import Data.List (sort, isSuffixOf, isInfixOf)
import Data.Maybe
import Network.Socket hiding (KeepAlive, send, recv)
import Network.Socket.ByteString.Lazy as Sock (sendAll, recv)
import System.Directory
import System.Environment (unsetEnv)
import System.Exit (ExitCode(..))
import System.FilePath (FilePath(..), (</>))
import System.FilePath.Glob (glob)
import System.IO
import System.IO.Error (catchIOError)
import System.Process
import Test.Tasty
import Test.Tasty.HUnit

import FastString (mkFastString)
import SrcLoc (SrcSpan(..), mkSrcSpan, mkSrcLoc)

import Language.Haskell.Tools.Daemon (runDaemon')
import Language.Haskell.Tools.Daemon.Options as Options (SharedDaemonOptions(..), DaemonOptions(..))
import Language.Haskell.Tools.Daemon.PackageDB (PackageDB(..))
import Language.Haskell.Tools.Daemon.Protocol
import Language.Haskell.Tools.Refactor.Builtin (builtinRefactorings, builtinQueries)

pORT_NUM_START = 4100
pORT_NUM_END = 4200

main :: IO ()
main = do unsetEnv "GHC_PACKAGE_PATH"
          portCounter <- newMVar pORT_NUM_START
          tr <- canonicalizePath testRoot
          hasStack <- isJust <$> findExecutable "stack"
          hasCabal <- isJust <$> findExecutable "cabal"
          defaultMain (allTests (hasStack && hasCabal) tr portCounter)

allTests :: Bool -> FilePath -> MVar Int -> TestTree
allTests isSource testRoot portCounter
  = localOption (mkTimeout ({- 30s -} 1000 * 1000 * 30))
      $ testGroup "daemon-tests"
          [ testGroup "simple-tests"
              $ map (makeDaemonTest portCounter) simpleTests
          , testGroup "loading-tests"
              $ map (makeDaemonTest portCounter) (loadingTests testRoot)
          , testGroup "complex-loading-tests"
              $ map (makeComplexLoadTest portCounter) (complexLoadingTests testRoot)
          , testGroup "refactor-tests"
              $ map (makeRefactorTest portCounter) (refactorTests testRoot)
          , testGroup "reload-tests"
              $ map (makeReloadTest portCounter) (reloadingTests testRoot)
          , testGroup "undo-tests"
              $ map (makeUndoTest portCounter) (undoTests testRoot)
          , testGroup "compilation-problem-tests"
              $ map (makeCompProblemTest portCounter) (compProblemTests isSource testRoot)
          , testGroup "special-tests"
              $ map (makeSpecialTest portCounter) (specialTests testRoot)
          -- if not a stack build, we cannot guarantee that stack is on the path
          , if isSource
             then testGroup "pkg-db-tests" $ map (makePkgDbTest portCounter) (pkgDbTests testRoot)
             else testCase "IGNORED pkg-db-tests" (return ())
          -- cannot execute this when the source is not present
          -- , if isSource then selfLoadingTest portCounter else testCase "IGNORED self-load" (return ())
          ]

testSuffix = "_test"

simpleTests :: [(String, [ClientMessage], [ResponseMsg])]
simpleTests =
  [ ( "empty-test", [], [] )
  , ( "keep-alive", [KeepAlive], [KeepAliveResponse] )
  ]

loadingTests :: FilePath -> [(String, [ClientMessage], [ResponseMsg])]
loadingTests testRoot =
  [ ( "load-package"
    , [AddPackages [testRoot </> "has-cabal"]]
    , [ LoadingModules [testRoot </> "has-cabal" </> "A.hs"]
      , LoadedModule (testRoot </> "has-cabal" </> "A.hs") "A" ] )
  , ( "same-module-in-two-mod-coll"
    , [AddPackages [testRoot </> "same-module-in-two-mod-coll"]]
    , [ LoadingModules [testRoot </> "same-module-in-two-mod-coll" </> "A.hs"
      , testRoot </> "same-module-in-two-mod-coll" </> "Main.hs"]
      , LoadedModule (testRoot </> "same-module-in-two-mod-coll" </> "A.hs") "A"
      , LoadedModule (testRoot </> "same-module-in-two-mod-coll" </> "Main.hs") "Main" ] )
  , ( "exposed-mod-as-main"
    , [AddPackages [testRoot </> "exposed-mod-as-main"]]
    , [ LoadingModules [testRoot </> "exposed-mod-as-main" </> "A.hs"]
      , LoadedModule (testRoot </> "exposed-mod-as-main" </> "A.hs") "A" ] )
  , ( "no-cabal"
    , [AddPackages [testRoot </> "no-cabal"]]
    , [ LoadingModules [testRoot </> "no-cabal" </> "A.hs"]
      , LoadedModule (testRoot </> "no-cabal" </> "A.hs") "A"] )
  , ( "code-gen"
    , [AddPackages [testRoot </> "code-gen"]]
    , [ LoadingModules [testRoot </> "code-gen" </> "B.hs", testRoot </> "code-gen" </> "A.hs"]
      , LoadedModule (testRoot </> "code-gen" </> "B.hs") "B"
      , LoadedModule (testRoot </> "code-gen" </> "A.hs") "A" ] )
  ,( "th-typecheck"
    , [AddPackages [testRoot </> "th-typecheck"]]
    , [ LoadingModules [testRoot </> "th-typecheck" </> "A.hs"]
      , LoadedModule (testRoot </> "th-typecheck" </> "A.hs") "A"] )
  , ( "source-dir"
    , [AddPackages [testRoot </> "source-dir"]]
    , [ LoadingModules [testRoot </> "source-dir" </> "src" </> "A.hs"]
      , LoadedModule (testRoot </> "source-dir" </> "src" </> "A.hs") "A"] )
  , ( "source-dir-outside"
    , [AddPackages [testRoot </> "source-dir-outside"]]
    , [ LoadingModules [testRoot </> "source-dir-outside" </> ".." </> "src" </> "A.hs"]
      , LoadedModule (testRoot </> "source-dir-outside" </> ".." </> "src" </> "A.hs") "A"] )
  , ( "multi-packages"
    , [ AddPackages [ testRoot </> "multi-packages" </> "package1"
                    , testRoot </> "multi-packages" </> "package2" ]]
    , [ LoadingModules [ testRoot </> "multi-packages" </> "package1" </> "A.hs"
                       , testRoot </> "multi-packages" </> "package2" </> "B.hs" ]
      , LoadedModule (testRoot </> "multi-packages" </> "package1" </> "A.hs") "A"
      , LoadedModule (testRoot </> "multi-packages" </> "package2" </> "B.hs") "B" ] )
  , ( "multi-packages-flags"
    , [ AddPackages [ testRoot </> "multi-packages-flags" </> "package1"
                    , testRoot </> "multi-packages-flags" </> "package2" ]]
    , [ LoadingModules [ testRoot </> "multi-packages-flags" </> "package1" </> "A.hs"
                       , testRoot </> "multi-packages-flags" </> "package2" </> "B.hs" ]
      , LoadedModule (testRoot </> "multi-packages-flags" </> "package1" </> "A.hs") "A"
      , LoadedModule (testRoot </> "multi-packages-flags" </> "package2" </> "B.hs") "B" ] )
  , ( "multi-packages-dependent"
    , [ AddPackages [ testRoot </> "multi-packages-dependent" </> "package1"
                    , testRoot </> "multi-packages-dependent" </> "package2" ]]
    , [ LoadingModules [ testRoot </> "multi-packages-dependent" </> "package1" </> "A.hs"
                       , testRoot </> "multi-packages-dependent" </> "package2" </> "B.hs" ]
      , LoadedModule (testRoot </> "multi-packages-dependent" </> "package1" </> "A.hs") "A"
      , LoadedModule (testRoot </> "multi-packages-dependent" </> "package2" </> "B.hs") "B" ] )
  , ( "has-th"
    , [AddPackages [testRoot </> "has-th"]]
    , [ LoadingModules [ testRoot </> "has-th" </> "TH.hs", testRoot </> "has-th" </> "A.hs" ]
      , LoadedModule (testRoot </> "has-th" </> "TH.hs") "TH"
      , LoadedModule (testRoot </> "has-th" </> "A.hs") "A" ] )
  , ( "th-added-later"
    , [ AddPackages [testRoot </> "th-added-later" </> "package1"]
      , AddPackages [testRoot </> "th-added-later" </> "package2"]
      ]
    , [ LoadingModules [ testRoot </> "th-added-later" </> "package1" </> "A.hs" ]
      , LoadedModule (testRoot </> "th-added-later" </> "package1" </> "A.hs") "A"
      , LoadingModules [ testRoot </> "th-added-later" </> "package2" </> "B.hs" ]
      , LoadedModule (testRoot </> "th-added-later" </> "package2" </> "B.hs") "B" ] )
  , ( "unused-module"
    , [ AddPackages [testRoot </> "unused-mod"] ]
    , [ LoadingModules [ testRoot </> "unused-mod" </> "Main.hs" ]
      , LoadedModule (testRoot </> "unused-mod" </> "Main.hs") "Main" ] )
  , ( "additional-files"
    , [ SetPackageDB DefaultDB, AddPackages [testRoot </> "additional-files"] ]
    , [ LoadingModules [ testRoot </> "additional-files" </> "B.hs"
                       , testRoot </> "additional-files" </> "A.hs" ]
      , LoadedModule (testRoot </> "additional-files" </> "B.hs") "B"
      , LoadedModule (testRoot </> "additional-files" </> "A.hs") "A"
      ])
  ]

complexLoadingTests :: FilePath -> [(String, FilePath, [ClientMessage], [ResponseMsg] -> Bool)]
complexLoadingTests testRoot =
  [ ( "paths-module", testRoot </> "paths-module"
    , [AddPackages [testRoot </> "paths-module"]]
    , \case [ LoadingModules [paths, a], LoadedModule{}, LoadedModule{}
              ] -> "Paths_some_test_package.hs" `isSuffixOf` paths
                      && "A.hs" `isSuffixOf` a
              ; _ -> False)
  , ( "paths-codegen", testRoot </> "paths-codegen"
    , [AddPackages [testRoot </> "paths-codegen"]]
    , \case [ LoadingModules [paths, a], LoadedModule{}, LoadedModule{}
              ] -> "Paths_some_test_package.hs" `isSuffixOf` paths
                      && "A.hs" `isSuffixOf` a
              ; _ -> False)
  ]

compProblemTests :: Bool -> FilePath -> [(String, [Either (IO ()) ClientMessage], [ResponseMsg] -> Bool)]
compProblemTests isSource testRoot =
  [ ( "load-error"
    , [ Right $ SetPackageDB DefaultDB, Right $ AddPackages [testRoot </> "load-error"] ]
    , \case [LoadingModules{}, CompilationProblem {}] -> True; _ -> False)
  , ( "source-error"
    , [ Right $ SetPackageDB DefaultDB, Right $ AddPackages [testRoot </> "source-error"] ]
    , \case [LoadingModules{}, LoadedModule{}, CompilationProblem {}] -> True; _ -> False)
  , ( "reload-error"
    , [ Right $ SetPackageDB DefaultDB, Right $ AddPackages [testRoot </> "empty"]
      , Left $ appendFile (testRoot </> "empty" </> "A.hs") "\n\nimport No.Such.Module"
      , Right $ ReLoad [] [testRoot </> "empty" </> "A.hs"] []
      , Left $ writeFile (testRoot </> "empty" </> "A.hs") "module A where"]
    , \case [LoadingModules {}, LoadedModule {}, LoadingModules {}, CompilationProblem {}] -> True; _ -> False)
  , ( "reload-source-error"
    , [ Right $ SetPackageDB DefaultDB, Right $ AddPackages [testRoot </> "empty"]
      , Left $ appendFile (testRoot </> "empty" </> "A.hs") "\n\naa = 3 + ()"
      , Right $ ReLoad [] [testRoot </> "empty" </> "A.hs"] []
      , Left $ writeFile (testRoot </> "empty" </> "A.hs") "module A where"]
    , \case [LoadingModules {}, LoadedModule {}, LoadingModules {}, LoadedModule{}, CompilationProblem {}] -> True; _ -> False)
  , ( "warning"
    , [ Right $ SetPackageDB DefaultDB, Right $ AddPackages [testRoot </> "warning"] ]
    , \case [LoadingModules{}, LoadedModule {}, CompilationProblem [Marker _ Warning _] []] -> True; _ -> False)
  , ( "no-such-file"
    , [ Right $ SetPackageDB DefaultDB
      , Right $ PerformRefactoring "RenameDefinition" (testRoot </> "simple-refactor" ++ testSuffix </> "A.hs") "3:1-3:2" ["y"] False False]
    , \case [ ErrorMessage _ ] -> True; _ -> False )
  ] ++ if isSource then
         [ ( "cabal-sandbox-defaulted"
           , [ Left $ withCurrentDirectory (testRoot </> "cabal-sandbox") initCabalSandbox
             , Right $ SetPackageDB DefaultDB
             , Right $ AddPackages [testRoot </> "cabal-sandbox"] ]
           , \case [ErrorMessage{}] -> True; _ -> False )
         , ( "package-db-conflict"
           , [ Left $ withCurrentDirectory (testRoot </> "cabal-sandbox") initCabalSandbox
             , Right $ AddPackages [testRoot </> "cabal-sandbox", testRoot </> "has-cabal"] ]
           , \case [ErrorMessage{}] -> True; _ -> False )
         , ( "package-db-conflict-2"
           , [ Left $ withCurrentDirectory (testRoot </> "cabal-sandbox") initCabalSandbox
             , Right $ AddPackages [testRoot </> "cabal-sandbox"]
             , Right $ AddPackages [testRoot </> "has-cabal"] ]
           , \case [LoadingModules{}, LoadedModule{}, ErrorMessage{}] -> True; _ -> False )
         ] else []

refactorTests :: FilePath -> [(String, FilePath, [ClientMessage], [ResponseMsg] -> Bool)]
refactorTests testRoot =
  [ ( "simple-refactor", testRoot </> "simple-refactor"
    , [ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
      , PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["y"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule aPath _, LoadingModules{}, LoadedModule aPath' _ ]
              -> aPath == testRoot </> "simple-refactor" ++ testSuffix </> "A.hs" && aPath == aPath'; _ -> False )
  , ( "refactor-file", testRoot </> "simple-refactor"
    , [ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
      , PerformRefactoring "RenameDefinition" "A.hs" "3:1-3:2" ["y"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule aPath _, LoadingModules{}, LoadedModule aPath' _]
              -> aPath == testRoot </> "simple-refactor" ++ testSuffix </> "A.hs" && aPath == aPath'; _ -> False )
  , ( "refactor-with-th", testRoot </> "th-imports-normal"
      , [ AddPackages [ testRoot </> "th-imports-normal" ++ testSuffix ]
        , PerformRefactoring "RenameDefinition" "A" "3:6" ["A'"] False False
        ]
      , \case [ LoadingModules{}, LoadedModule a _, LoadedModule c _, LoadedModule b _
                , LoadingModules{}, LoadedModule a' _, LoadedModule c' _, LoadedModule b' _
                ] -> [a,b,c] == map ((testRoot </> "th-imports-normal" ++ testSuffix) </>) ["A.hs","B.hs","C.hs"] && [a',b',c'] == [a,b,c]
              _ -> False )
  , ( "dry-refactor", testRoot </> "reloading"
    , [ AddPackages [ testRoot </> "reloading" ++ testSuffix ]
      , PerformRefactoring "RenameDefinition" "C" "3:1-3:2" ["cc"] False True
      ]
    , \case [ LoadingModules{}, LoadedModule{}, LoadedModule{}, LoadedModule{}
              , DiffInfo diff'
              ] -> let [cFile,bFile] = map ((testRoot </> "reloading" ++ testSuffix) </>) ["C.hs","B.hs"]
                       diff = "--- " ++ cFile ++ "\n+++ " ++ cFile ++ "\n@@\n module C where\r\n \r\n-c = ()\n+cc = ()\n--- "
                                ++ bFile ++ "\n+++ " ++ bFile ++ "\n@@\n module B where\r\n \r\n import C\r\n \r\n-b = c\n+b = cc\n"
                    in diff' == diff; _ -> False )
  , ( "hs-boots", testRoot </> "hs-boots"
    , [ AddPackages [ testRoot </> "hs-boots" ++ testSuffix ]
      , PerformRefactoring "RenameDefinition" "A" "5:1-5:2" ["aa"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule{}, LoadedModule{}, LoadedModule{}, LoadedModule{}
              , LoadingModules{}, LoadedModule path1 _, LoadedModule path2 _
              , LoadedModule path3 _, LoadedModule path4 _
              ] -> let allPathes = map ((testRoot </> "hs-boots" ++ testSuffix) </>) ["A.hs","B.hs","A.hs-boot","B.hs-boot"]
                    in sort [path1,path2,path3,path4] == sort allPathes
            _ -> False )
  , ( "refactor-boot", testRoot </> "hs-boots"
    , [ AddPackages [ testRoot </> "hs-boots" ++ testSuffix ]
      , PerformRefactoring "RenameDefinition" (testRoot </> "hs-boots" ++ testSuffix </> "A.hs-boot") "3:1-3:2" ["aa"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule{}, LoadedModule{}, LoadedModule{}, LoadedModule{}
              , LoadingModules{}, LoadedModule path1 _, LoadedModule path2 _
              , LoadedModule path3 _, LoadedModule path4 _
              ] -> let allPathes = map ((testRoot </> "hs-boots" ++ testSuffix) </>) ["A.hs","B.hs","A.hs-boot","B.hs-boot"]
                    in sort [path1,path2,path3,path4] == sort allPathes
            _ -> False )
  , ( "remove-module", testRoot </> "simple-refactor"
    , [ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
      , PerformRefactoring "RenameDefinition" "A" "1:8-1:9" ["AA"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule aPath _, LoadingModules{}, LoadedModule aaPath _]
              -> aPath == testRoot </> "simple-refactor" ++ testSuffix </> "A.hs"
                   && aaPath == testRoot </> "simple-refactor" ++ testSuffix </> "AA.hs"
            _ -> False )
  , ( "refactor-type-error", testRoot </> "source-error"
    , [ AddPackages [testRoot </> "source-error"] 
      , PerformRefactoring "RenameDefinition" (testRoot </> "source-error" ++ testSuffix </> "A.hs") "3:1-3:2" ["aa"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule{}, CompilationProblem {}
              , LoadingModules{}, LoadedModule{}, CompilationProblem {}
              ] -> True; _ -> False)
  ]

reloadingTests :: FilePath -> [(String, FilePath, [ClientMessage], IO (), [ClientMessage], [ResponseMsg] -> Bool)]
reloadingTests testRoot =
  [ ( "reloading-module", testRoot </> "reloading", [ AddPackages [ testRoot </> "reloading" ++ testSuffix ]]
    , writeFile (testRoot </> "reloading" ++ testSuffix </> "C.hs") "module C where\nc = ()"
    , [ ReLoad [] [testRoot </> "reloading" ++ testSuffix </> "C.hs"] []
      , PerformRefactoring "RenameDefinition" "C" "2:1-2:2" ["d"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule pathC'' _, LoadedModule pathB'' _, LoadedModule pathA'' _
              , LoadingModules{}, LoadedModule pathC _, LoadedModule pathB _, LoadedModule pathA _
              , LoadingModules{},LoadedModule pathC' _, LoadedModule pathB' _, LoadedModule pathA' _
              ] -> let allPathes = map ((testRoot </> "reloading" ++ testSuffix) </>) ["C.hs","B.hs","A.hs"]
                    in [pathC,pathB,pathA] == allPathes
                         && [pathC',pathB',pathA'] == allPathes
                         && [pathC'',pathB'',pathA''] == allPathes
            _ -> False )
  , ( "reloading-only-one-module", testRoot </> "reloading"
    , [ AddPackages [ testRoot </> "reloading" ++ testSuffix ]] , return ()
    , [ ReLoad [] [testRoot </> "reloading" ++ testSuffix </> "A.hs"] [] ]
    , \case [ LoadingModules{}, LoadedModule pathC _, LoadedModule pathB _, LoadedModule pathA _
              , LoadingModules{}, LoadedModule pathA' _
              ] -> [pathC,pathB,pathA] == map ((testRoot </> "reloading" ++ testSuffix) </>) ["C.hs","B.hs","A.hs"]
                     && pathA' == pathA
            _ -> False )
  , ( "reloading-package", testRoot </> "changing-cabal"
    , [ AddPackages [ testRoot </> "changing-cabal" ++ testSuffix ]]
    , appendFile (testRoot </> "changing-cabal" ++ testSuffix </> "some-test-package.cabal") ", B"
    , [ AddPackages [testRoot </> "changing-cabal" ++ testSuffix]
      , PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["z"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule pathA _, LoadingModules{}, LoadedModule pathA' _
              , LoadedModule pathB' _
              , LoadingModules{}, LoadedModule pathA'' _, LoadedModule pathB'' _
              ] -> let [pA,pB] = map ((testRoot </> "changing-cabal" ++ testSuffix) </>) ["A.hs","B.hs"]
                    in pA == pathA && pA == pathA' && pA == pathA'' && pB == pathB' && pB == pathB''
            _ -> False )
  , ( "reloading-th", testRoot </> "has-th", [], return ()
    , [ AddPackages [testRoot </> "has-th" ++ testSuffix]
      , ReLoad [] [testRoot </> "has-th" ++ testSuffix </> "TH.hs"] []
      ]
    , \case [ LoadingModules{}, LoadedModule th _, LoadedModule a _
              , LoadingModules{}, LoadedModule th' _, LoadedModule a' _
              ] -> let [pth,pa] = map ((testRoot </> "has-th" ++ testSuffix) </>) ["TH.hs","A.hs"]
                    in pa == a && pa == a' && pth == th && pth == th'
            _ -> False )
  -- TODO: This test runs correctly in itself. Probably a testing bug?
  -- , ( "reset-th", testRoot </> "has-th", [], return ()
  --   , [ AddPackages [testRoot </> "has-th" ++ testSuffix]
  --     , Reset
  --     ]
  --   , \case [ LoadingModules{}, LoadedModule th _, LoadedModule a _
  --             , LoadingModules{}, LoadedModule th' _, LoadedModule a' _
  --             ] -> let [pth,pa] = map ((testRoot </> "has-th" ++ testSuffix) </>) ["TH.hs","A.hs"]
  --                   in pa == a && pa == a' && pth == th && pth == th'
  --           _ -> False )
  , ( "reloading-ghc", testRoot </> "has-ghc", [], return ()
    , [ AddPackages [testRoot </> "has-ghc" ++ testSuffix]
      , ReLoad [] [testRoot </> "has-ghc" ++ testSuffix </> "A.hs"] []
      ]
    , \case [ LoadingModules{}, LoadedModule a _
              , LoadingModules{}, LoadedModule a' _
              ] -> a == testRoot </> "has-ghc" ++ testSuffix </> "A.hs" && a' == a
            _ -> False )
  , ( "reloading-unloadable-project", testRoot </> "load-error"
    , [ AddPackages [testRoot </> "load-error" ++ testSuffix] ]
    , writeFile (testRoot </> "load-error" ++ testSuffix </> "A.hs") "module A where\n\na = ()"
    , [ ReLoad [] [testRoot </> "load-error" ++ testSuffix </> "A.hs"] [] ]
    , \case [ LoadingModules{}, CompilationProblem{}
              , LoadingModules{}, LoadedModule a _
              ] -> a == testRoot </> "load-error" ++ testSuffix </> "A.hs"
            _ -> False )
  , ( "reloading-unloadable-project-multi-modules", testRoot </> "load-error-multi"
    , [ AddPackages [testRoot </> "load-error-multi" ++ testSuffix] ]
    , writeFile (testRoot </> "load-error-multi" ++ testSuffix </> "B.hs") "module B where\n\nimport A\n\nb = a"
    , [ ReLoad [] [testRoot </> "load-error-multi" ++ testSuffix </> "B.hs"] [] ]
    , \case [ LoadingModules{}, LoadedModule a _, LoadedModule d _, CompilationProblem{}
              , LoadingModules{}, LoadedModule b _, LoadedModule c _
              ] -> [a,b,c,d] == map ((testRoot </> "load-error-multi" ++ testSuffix) </>) ["A.hs", "B.hs","C.hs","D.hs"]
            _ -> False )
  , ( "change-cabal", testRoot </> "two-modules", [], return ()
    , [ AddPackages [testRoot </> "two-modules" ++ testSuffix]
      , ReLoad [] [testRoot </> "two-modules" ++ testSuffix </> "some-test-package.cabal"] []
      ]
    , \case [ LoadingModules{}, LoadedModule a _, LoadedModule b _
              , LoadingModules{}, LoadedModule a' _, LoadedModule b' _
              ] -> [a,b] == map ((testRoot </> "two-modules" ++ testSuffix) </>) ["A.hs","B.hs"] && [a',b'] == [a,b]
            _ -> False )
  , ( "change-incomplete-cabal", testRoot </> "incomplete-cabal"
    , [ AddPackages [testRoot </> "incomplete-cabal" ++ testSuffix] ]
    , appendFile (testRoot </> "incomplete-cabal" ++ testSuffix </> "some-test-package.cabal") ", B"
    , [ ReLoad [] [testRoot </> "incomplete-cabal" ++ testSuffix </> "some-test-package.cabal"] [] ]
    , \case [ LoadingModules{}, LoadedModule a _
              , LoadingModules{}, LoadedModule a' _, LoadedModule b' _
              ] -> [a',b'] == map ((testRoot </> "incomplete-cabal" ++ testSuffix) </>) ["A.hs","B.hs"] && a == a'
            _ -> False )
  , ( "adding-module", testRoot </> "reloading", [AddPackages [ testRoot </> "reloading" ++ testSuffix ]]
    , writeFile (testRoot </> "reloading" ++ testSuffix </> "D.hs") "module D where\nd = ()"
    , [ ReLoad [testRoot </> "reloading" ++ testSuffix </> "D.hs"] [] [] ]
    , \case [ LoadingModules {}, LoadedModule {}, LoadedModule {}, LoadedModule {}, LoadingModules {}
              , LoadingModules {}, LoadedModule {}, LoadedModule {}, LoadedModule {}, LoadedModule {}] -> True
            _ -> False )
  , ( "reloading-remove", testRoot </> "reloading", [ AddPackages [ testRoot </> "reloading" ++ testSuffix ]]
    , do removeFile (testRoot </> "reloading" ++ testSuffix </> "A.hs")
         removeFile (testRoot </> "reloading" ++ testSuffix </> "B.hs")
    , [ ReLoad [] [testRoot </> "reloading" ++ testSuffix </> "C.hs"]
                  [testRoot </> "reloading" ++ testSuffix </> "A.hs", testRoot </> "reloading" ++ testSuffix </> "B.hs"]
      , PerformRefactoring "RenameDefinition" "C" "3:1-3:2" ["d"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule pathC _, LoadedModule pathB _, LoadedModule pathA _
              , LoadingModules{}, LoadedModule pathC' _, LoadingModules{}, LoadedModule pathC'' _ ]
              -> let [pC,pB,pA] = map ((testRoot </> "reloading" ++ testSuffix) </>) ["C.hs","B.hs","A.hs"]
                  in pA == pathA && pB == pathB && pC == pathC && pC == pathC' && pC == pathC''
            _ -> False )
  , ( "remove-package", testRoot </> "multi-packages-dependent"
    , [ AddPackages [ testRoot </> "multi-packages-dependent" ++ testSuffix </> "package1"
                    , testRoot </> "multi-packages-dependent" ++ testSuffix </> "package2" ]]
    , removeDirectoryRecursive (testRoot </> "multi-packages-dependent" ++ testSuffix </> "package2")
    , [ RemovePackages [testRoot </> "multi-packages-dependent" ++ testSuffix </> "package2"]
      , PerformRefactoring "RenameDefinition" "A"
                                              "3:1-3:2" ["d"] False False
      ]
    , \case [ LoadingModules{}, LoadedModule pathA' _, LoadedModule pathB' _
              , LoadingModules{}, LoadedModule pathA _ ]
              -> let [pA,pB] = map ((testRoot </> "multi-packages-dependent" ++ testSuffix) </>) [ "package1" </> "A.hs", "package2" </> "B.hs"]
                  in pA == pathA && pA == pathA' && pB == pathB'
            _ -> False )
  ]

undoTests :: FilePath -> [(String, FilePath, [Either (IO ()) ClientMessage], [ResponseMsg] -> Bool)]
undoTests testRoot
  = [ ( "nothing-to-undo", testRoot </> "simple-refactor"
      , [ Right $ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
        , Right UndoLast
        ] , \case [ LoadingModules{}, LoadedModule{}, ErrorMessage{}] -> True; _ -> False )
    , ( "simple-undo", testRoot </> "simple-refactor"
      , [ Right $ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
        , Right $ PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["y"] False False
        , Left $ do -- if the test does not succeed, insert a delay here
                    res <- readFile (testRoot </> "simple-refactor" ++ testSuffix </> "A.hs")
                    when (filter (`notElem` ['\r','\n']) res /= "module A wherey = ()")
                      $ assertFailure ("Module content after refactoring is not the expected:\n" ++ res)
        , Right UndoLast
        , Left $ do -- if the test does not succeed, insert a delay here
                    res <- readFile (testRoot </> "simple-refactor" ++ testSuffix </> "A.hs")
                    when (filter (`notElem` ['\r','\n']) res /= "module A wherex = ()")
                      $ assertFailure ("Module content after undoing is not the expected:\n" ++ res)
        ]
      , \case [ LoadingModules{}, LoadedModule{}, LoadingModules{}, LoadedModule{}, LoadingModules{}, LoadedModule{}]
                -> True; _ -> False )
    , ( "undo-invalidated", testRoot </> "simple-refactor"
      , [ Right $ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
        , Right $ PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["y"] False False
        , Right (ReLoad [] [testRoot </> "simple-refactor" ++ testSuffix </> "A.hs"] [])
        , Right UndoLast
        ]
      , \case [ LoadingModules{}, LoadedModule{}, LoadingModules{}, LoadedModule{}
                , LoadingModules{}, LoadedModule{}, ErrorMessage{} ] -> True; _ -> False )
    , ( "undo-invalidated-other-module", testRoot </> "two-modules"
      , [ Right $ AddPackages [ testRoot </> "two-modules" ++ testSuffix ]
        , Right $ PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["y"] False False
        , Right (ReLoad [] [testRoot </> "two-modules" ++ testSuffix </> "B.hs"] [])
        , Right UndoLast
        ]
      , \case [ LoadingModules{}, LoadedModule{}, LoadedModule{}, LoadingModules{}, LoadedModule{}
                , LoadingModules{}, LoadedModule{}, ErrorMessage{} ] -> True; _ -> False )
    , ( "undo-invalidated-revalidate", testRoot </> "simple-refactor"
      , [ Right $ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
        , Right $ PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["y"] False False
        , Right (ReLoad [] [testRoot </> "simple-refactor" ++ testSuffix </> "A.hs"] [])
        , Right $ PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["x"] False False
        , Right UndoLast
        ]
      , \case [ LoadingModules{}, LoadedModule{}, LoadingModules{}, LoadedModule{}
                , LoadingModules{}, LoadedModule{}, LoadingModules{}, LoadedModule{}
                , LoadingModules{}, LoadedModule{} ] -> True; _ -> False )
    , ( "multi-module-undo", testRoot </> "reloading"
        , [ Right $ AddPackages [ testRoot </> "reloading" ++ testSuffix ]
          , Right $ PerformRefactoring "RenameDefinition" "C" "3:1-3:2" ["d"] False False
          , Right UndoLast
          , Left $ do -- if the test does not succeed, insert a delay here
                      resC <- readFile (testRoot </> "reloading" ++ testSuffix </> "C.hs")
                      resB <- readFile (testRoot </> "reloading" ++ testSuffix </> "B.hs")
                      when (filter (`notElem` ['\r','\n']) resC /= "module C wherec = ()")
                        $ assertFailure ("C.hs content after undoing is not the expected:\n" ++ resC)
                      when (filter (`notElem` ['\r','\n']) resB /= "module B whereimport Cb = c")
                        $ assertFailure ("B.hs content after undoing is not the expected:\n" ++ resC)
          ]
        , \case [ LoadingModules{}, LoadedModule{}, LoadedModule{}, LoadedModule{}
                  , LoadingModules{}, LoadedModule{}, LoadedModule{}, LoadedModule{}
                  , LoadingModules{}, LoadedModule{}, LoadedModule{}, LoadedModule{} ]
                  -> True; _ -> False )
    , ( "multiple-undo", testRoot </> "simple-refactor"
      , [ Right $ AddPackages [ testRoot </> "simple-refactor" ++ testSuffix ]
        , Right $ PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["y"] False False
        , Right $ PerformRefactoring "RenameDefinition" "A" "3:1-3:2" ["z"] False False
        , Right UndoLast
        , Right UndoLast
        , Left $ do -- if the test does not succeed, insert a delay here
                    res <- readFile (testRoot </> "simple-refactor" ++ testSuffix </> "A.hs")
                    when (filter (`notElem` ['\r','\n']) res /= "module A wherex = ()")
                      $ assertFailure ("Module content after undoing is not the expected:\n" ++ res)
        ]
      , \case [ LoadingModules{}, LoadedModule{} -- initial load
                , LoadingModules{}, LoadedModule{} -- re-load after first refactor
                , LoadingModules{}, LoadedModule{} -- re-load after second refactor
                , LoadingModules{}, LoadedModule{} -- re-load after first undo
                , LoadingModules{}, LoadedModule{} -- re-load after second undo
                ]
                -> True; _ -> False )
    ]

pkgDbTests :: FilePath -> [(String, IO (), [ClientMessage], [ResponseMsg])]
pkgDbTests testRoot
  = [ ( "cabal-sandbox"
      , withCurrentDirectory (testRoot </> "cabal-sandbox") initCabalSandbox
      , [ SetPackageDB CabalSandboxDB, AddPackages [testRoot </> "cabal-sandbox"] ]
      , [ LoadingModules [testRoot </> "cabal-sandbox" </> "UseGroups.hs"]
        , LoadedModule (testRoot </> "cabal-sandbox" </> "UseGroups.hs") "UseGroups"] )
    , ( "cabal-sandbox-auto"
      , withCurrentDirectory (testRoot </> "cabal-sandbox") initCabalSandbox
      , [ AddPackages [testRoot </> "cabal-sandbox"] ]
      , [ LoadingModules [testRoot </> "cabal-sandbox" </> "UseGroups.hs"]
        , LoadedModule (testRoot </> "cabal-sandbox" </> "UseGroups.hs") "UseGroups"] )
    , ( "pkg-db-reload"
      , withCurrentDirectory (testRoot </> "cabal-sandbox") initCabalSandbox
      , [ AddPackages [testRoot </> "cabal-sandbox"]
        , ReLoad [] [testRoot </> "cabal-sandbox" </> "UseGroups.hs"] []]
      , [ LoadingModules [testRoot </> "cabal-sandbox" </> "UseGroups.hs"]
        , LoadedModule (testRoot </> "cabal-sandbox" </> "UseGroups.hs") "UseGroups"
        , LoadingModules [testRoot </> "cabal-sandbox" </> "UseGroups.hs"]
        , LoadedModule (testRoot </> "cabal-sandbox" </> "UseGroups.hs") "UseGroups" ])
    ]

initCabalSandbox = do
  sandboxExists <- doesDirectoryExist ".cabal-sandbox"
  when sandboxExists $ tryToExecute "cabal" ["sandbox", "delete"]
  execute "cabal" ["sandbox", "init"]
  withCurrentDirectory ("groups-0.4.0.0") $ do
    execute "cabal" ["sandbox", "init", "--sandbox", ".." </> ".cabal-sandbox"]
    execute "cabal" ["install"]

specialTests :: FilePath -> [(String, FilePath, Int -> Maybe FilePath -> DaemonOptions, [ClientMessage], [ResponseMsg] -> Bool)]
specialTests testRoot =
  [ ( "force-code-gen-with-th", testRoot </> "th-imports-normal", codeGenOpts
      , [ AddPackages [ testRoot </> "th-imports-normal" ++ testSuffix ]
        , PerformRefactoring "RenameDefinition" "A" "3:6" ["A'"] False False
        ]
      , \case [ LoadingModules{}, LoadedModule a _, LoadedModule c _, LoadedModule b _
                , LoadingModules{}, LoadedModule a' _, LoadedModule c' _, LoadedModule b' _
                ] -> [a,b,c] == map ((testRoot </> "th-imports-normal" ++ testSuffix) </>) ["A.hs","B.hs","C.hs"] && [a',b',c'] == [a,b,c]
              _ -> False )
  ]
  where codeGenOpts p fs = addCodeGen (daemonOpts p fs)
        addCodeGen opts = opts { sharedOptions = (sharedOptions opts) { generateCode = True } }

-------------------------------------------------------
-- * Helper functions for test code -------------------
-------------------------------------------------------

sourceRoot = ".." </> ".." </> "src"

execute :: String -> [String] -> IO ()
execute cmd args
  = do let command = (cmd ++ concat (map (" " ++) args))
       (_, Just stdOut, Just stdErr, handle) <- createProcess ((shell command) { std_out = CreatePipe, std_err = CreatePipe })
       exitCode <- waitForProcess handle
       when (exitCode /= ExitSuccess) $ do
         output <- hGetContents stdOut
         errors <- hGetContents stdErr
         error ("Command exited with nonzero: " ++ command ++ " output:\n" ++ output ++ "\nerrors:\n" ++ errors)

tryToExecute :: String -> [String] -> IO ()
tryToExecute cmd args
  = do let command = (cmd ++ concat (map (" " ++) args))
       (_, _, _, handle) <- createProcess ((shell command) { std_out = NoStream, std_err = NoStream })
       void $ waitForProcess handle

makeDaemonTest :: MVar Int -> (String, [ClientMessage], [ResponseMsg]) -> TestTree
makeDaemonTest port (label, input, expected) = testCase label $ restartOnNetworkError $ do
    actual <- communicateWithDaemon False port daemonOpts (map Right (SetPackageDB DefaultDB : input))
    assertEqual "" expected actual

makeComplexLoadTest :: MVar Int -> (String, FilePath, [ClientMessage], [ResponseMsg] -> Bool) -> TestTree
makeComplexLoadTest port (label, dir, inputs, validator) = testCase label $ restartOnNetworkError $ do
    exists <- doesDirectoryExist (dir ++ testSuffix)
    -- clear the target directory from possible earlier test runs
    when exists $ removeDirectoryRecursive (dir ++ testSuffix)
    copyDir dir (dir ++ testSuffix)
    actual <- communicateWithDaemon False port daemonOpts (map Right (SetPackageDB DefaultDB : inputs))
    assertBool ("The responses are not the expected: " ++ show actual) (validator actual)
  `finally` removeDirectoryRecursive (dir ++ testSuffix)

makeRefactorTest :: MVar Int -> (String, FilePath, [ClientMessage], [ResponseMsg] -> Bool) -> TestTree
makeRefactorTest port (label, dir, input, validator) = testCase label $ restartOnNetworkError $ do
    exists <- doesDirectoryExist (dir ++ testSuffix)
    -- clear the target directory from possible earlier test runs
    when exists $ removeDirectoryRecursive (dir ++ testSuffix)
    copyDir dir (dir ++ testSuffix)
    actual <- communicateWithDaemon False port daemonOpts (map Right (SetPackageDB DefaultDB : input))
    assertBool ("The responses are not the expected: " ++ show actual) (validator actual)
  `finally` removeDirectoryRecursive (dir ++ testSuffix)

makeReloadTest :: MVar Int -> (String, FilePath, [ClientMessage], IO (), [ClientMessage], [ResponseMsg] -> Bool) -> TestTree
makeReloadTest port (label, dir, input1, io, input2, validator) = testCase label $ restartOnNetworkError $ do
    exists <- doesDirectoryExist (dir ++ testSuffix)
    -- clear the target directory from possible earlier test runs
    when exists $ removeDirectoryRecursive (dir ++ testSuffix)
    copyDir dir (dir ++ testSuffix)
    actual <- communicateWithDaemon False port daemonOpts (map Right (SetPackageDB DefaultDB : input1) ++ [Left io] ++ map Right input2)
    assertBool ("The responses are not the expected: " ++ show actual) (validator actual)
  `finally` removeDirectoryRecursive (dir ++ testSuffix)

makeUndoTest :: MVar Int -> (String, FilePath, [Either (IO ()) ClientMessage], [ResponseMsg] -> Bool) -> TestTree
makeUndoTest port (label, dir, inputs, validator) = testCase label $ restartOnNetworkError $ do
    exists <- doesDirectoryExist (dir ++ testSuffix)
    -- clear the target directory from possible earlier test runs
    when exists $ removeDirectoryRecursive (dir ++ testSuffix)
    copyDir dir (dir ++ testSuffix)
    actual <- communicateWithDaemon False port daemonOpts (Right (SetPackageDB DefaultDB) : inputs)
    assertBool ("The responses are not the expected: " ++ show actual) (validator actual)
  `finally` removeDirectoryRecursive (dir ++ testSuffix)

makePkgDbTest :: MVar Int -> (String, IO (), [ClientMessage], [ResponseMsg]) -> TestTree
makePkgDbTest port (label, prepare, inputs, expected)
  = testCase label $ restartOnNetworkError $ do
      actual <- communicateWithDaemon False port daemonOpts ([Left prepare] ++ map Right inputs)
      assertEqual "" expected actual

makeCompProblemTest :: MVar Int -> (String, [Either (IO ()) ClientMessage], [ResponseMsg] -> Bool) -> TestTree
makeCompProblemTest port (label, actions, validator) = testCase label $ restartOnNetworkError $ do
  actual <- communicateWithDaemon False port daemonOpts actions
  assertBool ("The responses are not the expected: " ++ show actual) (validator actual)

makeSpecialTest :: MVar Int -> (String, FilePath, Int -> Maybe FilePath -> DaemonOptions, [ClientMessage], [ResponseMsg] -> Bool) -> TestTree
makeSpecialTest port (label, dir, opts, input, validator) = testCase label $ restartOnNetworkError $ do
    exists <- doesDirectoryExist (dir ++ testSuffix)
    -- clear the target directory from possible earlier test runs
    when exists $ removeDirectoryRecursive (dir ++ testSuffix)
    copyDir dir (dir ++ testSuffix)
    actual <- communicateWithDaemon False port opts (map Right (SetPackageDB DefaultDB : input))
    assertBool ("The responses are not the expected: " ++ show actual) (validator actual)
  `finally` removeDirectoryRecursive (dir ++ testSuffix)

communicateWithDaemon :: Bool -> MVar Int -> (Int -> Maybe FilePath -> DaemonOptions) -> [Either (IO ()) ClientMessage] -> IO [ResponseMsg]
communicateWithDaemon watch port opts msgs = withSocketsDo $ do
    portNum <- retryConnect port
    addrInfo <- getAddrInfo Nothing (Just "127.0.0.1") (Just (show portNum))
    let serverAddr = head addrInfo
    sock <- socket (addrFamily serverAddr) Stream defaultProtocol
    waitToConnect sock (addrAddress serverAddr)
    intermedRes <- sequence (map (either (\io -> do sendAll sock (encode KeepAlive)
                                                    r <- readSockResponsesUntil sock KeepAliveResponse BS.empty
                                                    io
                                                    return r)
                                 ((>> return []) . sendAll sock . (`BS.snoc` '\n') . encode)) msgs)
    sendAll sock (encode Disconnect)
      `catchIOError` \e -> hPutStrLn stderr $ "Cannot send Disconnect: " ++ show e
    resps <- readSockResponsesUntil sock Disconnected BS.empty
    close sock
    return (concat intermedRes ++ resps)
  where waitToConnect sock addr
          = connect sock addr `catch` \(_ :: SomeException) -> threadDelay 10000 >> waitToConnect sock addr
        retryConnect port = do portNum <- readMVar port
                               watchDir <- (++) <$> glob watchPath <*> glob linuxWatchPath
                               case (watch, watchDir) of
                                 (True, []) -> error "The watch executable is not found."
                                 (True, w:_) -> forkIO $ runDaemon' builtinRefactorings builtinQueries (opts portNum (Just w))
                                 (False, _) -> forkIO $ runDaemon' builtinRefactorings builtinQueries (opts portNum Nothing)
                               return portNum
          `catch` \(e :: SomeException) -> do putStrLn ("exception caught: `" ++ show e ++ "` trying with a new port")
                                              modifyMVar_ port (\i -> if i < pORT_NUM_END
                                                                        then return (i+1)
                                                                        else error "The port number reached the maximum")
                                              retryConnect port

restartOnNetworkError = tryNTimes 5

tryNTimes :: Int -> IO () -> IO ()
tryNTimes 0 action = action
tryNTimes n action = action `catch` \e -> if "Network.Socket.sendBuf: failed" `isInfixOf` show e
                                            then do putStrLn (show e)
                                                    threadDelay 1000000
                                                    tryNTimes (n-1) action
                                            else throwIO (e :: SomeException)


daemonOpts :: Int -> Maybe FilePath -> DaemonOptions
daemonOpts n we = DaemonOptions { daemonVersion = False
                                , portNumber = n
                                , silentMode = True
                                , sharedOptions = SharedDaemonOptions { noWatch = isNothing we
                                                                      , watchExe = we
                                                                      , generateCode = False
                                                                      , disableHistory = False
                                                                      , Options.ghcFlags = Nothing
                                                                      , projectType = Nothing
                                                                      }
                                }

-- this must be changed once watch is in stackage
watchPath = "../../.stack-work/downloaded/*/watch-master/.stack-work/dist/*/build/watch"
linuxWatchPath = "../../.stack-work/downloaded/*/watch-master/.stack-work/dist/*/*/build/watch"

readSockResponsesUntil :: Socket -> ResponseMsg -> BS.ByteString -> IO [ResponseMsg]
readSockResponsesUntil sock rsp bs
  = do resp <- recv sock 2048
       let fullBS = bs `BS.append` resp
       if BS.null resp
         then return []
         else
           let splitted = BS.split '\n' fullBS
               recognized = catMaybes $ map decode splitted
            in if rsp `elem` recognized
                 then return $ takeWhile (/= rsp) recognized
                 else readSockResponsesUntil sock rsp fullBS

testRoot = "examples" </> "Project"

deriving instance Eq UndoRefactor
deriving instance Eq ResponseMsg
instance FromJSON UndoRefactor
instance FromJSON ResponseMsg
instance FromJSON Marker
instance FromJSON Severity
instance ToJSON ClientMessage
instance ToJSON PackageDB

copyDir ::  FilePath -> FilePath -> IO ()
copyDir src dst = do
  createDirectory dst
  content <- getDirectoryContents src
  let xs = filter (`notElem` [".", ".."]) content
  forM_ xs $ \name -> do
    let srcPath = src </> name
    let dstPath = dst </> name
    isDirectory <- doesDirectoryExist srcPath
    if isDirectory
      then copyDir srcPath dstPath
      else copyFile srcPath dstPath

commonPrefix :: (Eq e) => [e] -> [e] -> [e]
commonPrefix _ [] = []
commonPrefix [] _ = []
commonPrefix (x:xs) (y:ys)
  | x == y    = x : commonPrefix xs ys
  | otherwise = []

longestCommonPrefix :: (Eq a) => [[a]] -> [a]
longestCommonPrefix = foldl1 commonPrefix

instance FromJSON SrcSpan where
    parseJSON (Object v) = mkSrcSpanReal <$> v .: "file"
                                         <*> v .: "startRow"
                                         <*> v .: "startCol"
                                         <*> v .: "endRow"
                                         <*> v .: "endCol"
    parseJSON _          = fail "not an object"


mkSrcSpanReal :: String -> Int -> Int -> Int -> Int -> SrcSpan
mkSrcSpanReal file startRow startCol endRow endCol
  = mkSrcSpan (mkSrcLoc (mkFastString file) startRow startCol)
              (mkSrcLoc (mkFastString file) endRow endCol)
