content: -- | Rather than doing spot-checks for specific features, load some bigger
  -- projects and make sure nothing goes horribly wrong
  module TestSuite.Tests.Integration (testGroupIntegration) where
  
  import Control.Exception
  import Control.Monad
  import Data.Monoid
  import System.Exit
  import Test.Tasty
  import Test.HUnit hiding (test)
  
  import IdeSession
  import IdeSession.Util.PortableProcess (sigKillExitCode)
  import TestSuite.Assertions
  import TestSuite.Session
  import TestSuite.State
  
  testGroupIntegration :: TestSuiteEnv -> TestTree
  testGroupIntegration env = testGroup "Integration" $ integrationTests env [
      integrationTest env "Run the sample code; succeed or raise an exception"         test_runSampleCode
    , integrationTest env "Overwrite with error"                                       test_overwriteWithError
    , integrationTest env "Overwrite with the same module name in all files"           test_overwriteWithSameModuleName
    , integrationTest env "Overwrite modules many times"                               test_overwriteModulesManyTimes
    , integrationTest env "Overwrite all with exception-less code and run it"          test_overwriteWithExceptionFreeCode
    , integrationTest env "Make sure deleting modules removes them from the directory" test_deletingModulesRemovesFiles
    , integrationTest env "Make sure restartSession does not lose source files"        test_dontLoseFilesInRestart
    ]
  
  {-------------------------------------------------------------------------------
    "Projects" setup
  -------------------------------------------------------------------------------}
  
  type IntegrationTest = IdeSession -> IdeSessionUpdate -> [String] -> Assertion
  
  integrationTest :: TestSuiteEnv -> String -> IntegrationTest -> TestTree
  integrationTest env name test = testGroup name $
    map (testWithProject env test) (projects env)
  
  testWithProject :: TestSuiteEnv -> IntegrationTest -> Project -> TestTree
  testWithProject env test Project{..} = do
      stdTest env projectName $ \env' -> do
        let withPath
              | testInputPathCabal env' == projectSourcesDir = withTestInputPathCabal env'
              | otherwise = (\f -> f projectSourcesDir)
        withPath $ \path -> do
          (upd, lm) <- getModulesFrom path
          withAvailableSession' env' cfg $ \session -> test session upd lm
    where
      cfg = withModInfo False
          . withGhcOpts projectOptions
  
  data Project = Project {
       projectName       :: String
     , projectSourcesDir :: FilePath
     , projectOptions    :: [String]
     }
  
  -- Set of projects and options to use for them.
  projects :: TestSuiteEnv -> [Project]
  projects env = [
      Project {
          projectName       = "A depends on B, throws exception"
        , projectSourcesDir = "TestSuite/inputs/ABnoError"
        , projectOptions    = []
        }
    , Project {
          projectName       = "Cabal code"
        , projectSourcesDir = testInputPathCabal env
        , projectOptions    = []
        }
    , Project {
          projectName       = "A single file with a code to run in parallel"
        , projectSourcesDir = "TestSuite/inputs/MainModule"
        , projectOptions    = []
        }
    ]
  
  {-------------------------------------------------------------------------------
    The tests we run on each project
  -------------------------------------------------------------------------------}
  
  test_overwriteWithError :: IntegrationTest
  test_overwriteWithError session originalUpdate lm = do
      updateSessionD session originalUpdate (length lm)
      -- No errors in the original test code.
      assertNoErrors session
      -- Overwrite one of the copied files.
      (_, ms) <- getModules session
      let update = loadModule (head ms) "a = unknownX"
      updateSessionD session update (length ms)  -- we don't know how many modules get recompiled
      assertSourceErrors' session ["Not in scope: `unknownX'"]
  
  test_overwriteWithSameModuleName :: IntegrationTest
  test_overwriteWithSameModuleName session originalUpdate lm = do
      updateSessionD session update 2
      if length lm >= 2
        then assertSourceErrors' session ["defined in multiple files"]
        else assertNoErrors session
    where
      upd m  = updateSourceFile m "module Wrong where\na = 1"
      update = originalUpdate <> mconcat (map upd lm)
  
  test_overwriteModulesManyTimes :: IntegrationTest
  test_overwriteModulesManyTimes session originalUpdate lm0 = do
      let doubleUpdate = mempty <> originalUpdate <> originalUpdate <> mempty
      -- Updates are idempotent, so no errors and no recompilation.
      updateSessionD session doubleUpdate (length lm0)
      assertNoErrors session
      -- Overwrite one of the copied files with an error.
      (_, lm) <- getModules session
      let update1 = updateCodeGeneration False
                 <> loadModule (head lm) "a = unknownX"
      updateSessionD session update1 (length lm)
      assertSourceErrors' session ["Not in scope: `unknownX'"]
      updateSessionD session mempty 1  -- was an error, so trying again
      assertSourceErrors' session ["Not in scope: `unknownX'"]
      -- Overwrite all files, many times, with correct code eventually.
      let upd m   = loadModule m "x = unknownX"
                 <> loadModule m "y = 2"
                 <> updateCodeGeneration True
          update2 = mconcat $ map upd lm
      updateSessionD session update2 (length lm)
      assertNoErrors session
      -- Overwrite again with the error.
      updateSessionD session update1 1  -- drop bytecode, don't recompile
      assertSourceErrors' session ["Not in scope: `unknownX'"]
      assertRaises "runStmt session Main main"
        (== userError "Cannot run before the code is generated.")
        (runStmt session "Main" "main")
  
  test_runSampleCode :: IntegrationTest
  test_runSampleCode session originalUpdate lm = do
      updateSessionD session originalUpdate (length lm)
      updateSessionD session update (length lm)  -- all recompiled
      assertNoErrors session
      mex <- try $ runStmt session "Main" "main"
      case mex of
        Right runActions -> void $ runWaitAll runActions
        Left ex -> assertEqual "runStmt" (userError "Module \"Main\" not successfully loaded, when trying to run code.") ex
    where
      update = updateCodeGeneration True
  
  test_overwriteWithExceptionFreeCode :: IntegrationTest
  test_overwriteWithExceptionFreeCode session originalUpdate lm = do
      -- Compile from scratch, generating code from the start.
      updateSessionD session update2 (length lm + 1)
      assertNoErrors session
      runActions <- runStmt session "Central.TotallyMain" "main"
      (output, result) <- runWaitAll runActions
      assertEqual "" RunOk result
      assertEqual "" output "\"test run\"\n"
    where
      upd m   = loadModule m "x = 1"
      update  = originalUpdate
             <> updateSourceFile
                  "Central/TotallyMain.hs"
                  "module Central.TotallyMain where\nmain = print \"test run\""
             <> mconcat (map upd lm)
      update2 = update <> updateCodeGeneration True
  
  test_deletingModulesRemovesFiles :: IntegrationTest
  test_deletingModulesRemovesFiles session originalUpdate lm = do
      updateSessionD session originalUpdate (length lm)
      updateSessionD session updateDel 0
      assertNoErrors session
      -- The updates cancel each other out.
      updateSessionD session (originalUpdate <> updateDel) 0
      updateSessionD session update2 0  -- 0: nothing to generate code from
      assertNoErrors session
    where
      updateDel = mconcat $ map updateSourceFileDelete lm
      update2   = updateCodeGeneration True
  
  test_dontLoseFilesInRestart :: IntegrationTest
  test_dontLoseFilesInRestart session originalUpdate lm = do
      updateSessionD session update (length lm)
      serverBefore <- getGhcServer session
      mex <- try $ runStmt session "Main" "main"
      case mex of
        Right _runActions -> return ()  -- don't runWaitAll
        Left ex -> assertEqual "runStmt" (userError "Module \"Main\" not successfully loaded, when trying to run code.") ex
      restartSession session
      updateSessionD session mempty (length lm)  -- all compiled anew
      assertNoErrors session
      mex2 <- try $ runStmt session "Main" "main"
      case mex2 of
        Right runActions -> void $ runWaitAll runActions  -- now runWaitAll
        Left ex -> assertEqual "runStmt" (userError "Module \"Main\" not successfully loaded, when trying to run code.") ex
      restartSession session
      updateSessionD session update2 0  -- if any file missing, would yell
      assertNoErrors session
      updateSessionD session update3 0  -- 0: nothing to generate code from
      exitCodeBefore <- getGhcExitCode serverBefore
      assertEqual "exitCodeBefore" (Just (ExitFailure sigKillExitCode)) exitCodeBefore -- SIGKILL
    where
      update  = originalUpdate <> updateCodeGeneration True
      update2 = mconcat $ map updateSourceFileDelete lm
      update3 = updateCodeGeneration True
  