{-# LANGUAGE OverloadedStrings #-}

module Main (main) where

import Aihc.Cli.InstallV2 (InstallV2Result (..), installV2)
import Aihc.Cli.Options (InstallV2Options (..))
import Aihc.Cli.TypeArtifact (TypeArtifact (..), decodeTypeArtifact)
import Aihc.Fc qualified as Fc
import Aihc.Tc (TcInterface (..), tcTermKeyIdentifier)
import Control.Exception (bracket)
import Data.ByteString qualified as BS
import Data.List (isPrefixOf, sort)
import Data.Maybe (mapMaybe)
import Data.Text.IO qualified as TIO
import Hedgehog (Property, property, success)
import System.Directory
  ( createDirectory,
    createDirectoryIfMissing,
    doesFileExist,
    getTemporaryDirectory,
    listDirectory,
    removeDirectoryRecursive,
    removeFile,
  )
import System.FilePath ((</>))
import System.IO (hClose, openTempFile)
import Test.Tasty (defaultMain, testGroup)
import Test.Tasty.HUnit (Assertion, assertBool, assertEqual, assertFailure, testCase)
import Test.Tasty.Hedgehog (testProperty)

main :: IO ()
main =
  defaultMain . testGroup "aihc" $
    [ -- testGroup
      --   "cli"
      --   [ testCase "parses compile source" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing False False False False Nothing GcCalloc Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs"]),
      --     testCase "parses compile output and keep-asm" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" (Just "hello") False False True False Nothing GcCalloc Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs", "-o", "hello", "--keep-asm"]),
      --     testCase "parses keep-core" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing True False False False Nothing GcCalloc Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs", "--keep-core"]),
      --     testCase "parses keep-grin" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing False True False False Nothing GcCalloc Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs", "--keep-grin"]),
      --     testCase "parses whole-program compatibility mode" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing False False False True Nothing GcCalloc Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs", "--whole-program"]),
      --     testCase "parses a cross-compilation target" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing False False False False (Just LinuxAmd64) GcCalloc Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs", "--target", "linux-amd64"]),
      --     testCase "rejects the removed portable C target" $
      --       case parseCommandPure ["compile", "Main.hs", "--target", "portable-c"] of
      --         Left _ -> pure ()
      --         Right command -> assertFailure ("unexpected command: " <> show command),
      --     testCase "parses the LLVM target" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing False False False False (Just Llvm) GcCalloc Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs", "--target", "llvm"]),
      --     testCase "parses the WASI P3 target" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing False False False False (Just Wasm32Wasip3) GcCalloc Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs", "--target", "wasm32-wasip3"]),
      --     testCase "parses optional wasm-opt optimization" $
      --       case parseCommandPure ["compile", "Main.hs", "--target", "wasm32-wasip3", "--use-wasm-opt"] of
      --         Right (CmdCompile options) -> assertBool "uses wasm-opt" (compileUseWasmOpt options)
      --         result -> assertFailure ("expected compile options, got: " <> show result),
      --     testCase "selects the semispace collector" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing False False False False Nothing GcSemispace Nothing False)))
      --         (parseCommandPure ["compile", "Main.hs", "--gc", "semispace"]),
      --     testCase "selects an installed toolchain store" $
      --       assertEqual
      --         "command"
      --         (Right (CmdCompile (CompileOptions "Main.hs" Nothing False False False False Nothing GcCalloc (Just "/tmp/aihc-store") False)))
      --         (parseCommandPure ["compile", "Main.hs", "--store", "/tmp/aihc-store"]),
      --     testCase "prepares one installed runtime" $
      --       assertEqual
      --         "command"
      --         (Right (CmdPrepareRuntime (PrepareRuntimeOptions Llvm GcSemispace (Just "/tmp/aihc-store"))))
      --         (parseCommandPure ["prepare-runtime", "--target", "llvm", "--gc", "semispace", "--store", "/tmp/aihc-store"]),
      --     testCase "derives safe default compile output paths" $ do
      --       assertEqual "Haskell source" "src/Main" (compileOutputPath (CompileOptions "src/Main.hs" Nothing False False False False Nothing GcCalloc Nothing False))
      --       assertEqual "extensionless source" "program.out" (compileOutputPath (CompileOptions "program" Nothing False False False False Nothing GcCalloc Nothing False)),
      --     testCase "parses install package" $
      --       assertEqual
      --         "command"
      --         (Right (CmdInstall (InstallOptions "text" Nothing Nothing False False False False [] False InstallErrorsHuman)))
      --         (parseCommandPure ["install", "text"]),
      --     testCase "parses install version" $
      --       assertEqual
      --         "command"
      --         (Right (CmdInstall (InstallOptions "text" (Just "2.1") Nothing False False False False [] False InstallErrorsHuman)))
      --         (parseCommandPure ["install", "text", "--version", "2.1"]),
      --     testCase "parses install offline and store" $
      --       assertEqual
      --         "command"
      --         (Right (CmdInstall (InstallOptions "text" Nothing (Just "/tmp/aihc-store") True False False False [] False InstallErrorsHuman)))
      --         (parseCommandPure ["install", "text", "--offline", "--store", "/tmp/aihc-store"]),
      --     testCase "parses install dry run" $
      --       assertEqual
      --         "command"
      --         (Right (CmdInstall (InstallOptions "text" Nothing Nothing False True False False [] False InstallErrorsHuman)))
      --         (parseCommandPure ["install", "text", "--dry-run"]),
      --     testCase "parses first error module and JSON errors" $
      --       assertEqual
      --         "command"
      --         (Right (CmdInstall (InstallOptions "text" Nothing Nothing False False False False [] True InstallErrorsJson)))
      --         (parseCommandPure ["install", "text", "--first-error-module", "--json-errors"]),
      --     testCase "parses install targets" $
      --       assertEqual
      --         "command"
      --         (Right (CmdInstall (InstallOptions "text" Nothing (Just "/tmp/aihc-store") False False False False [Llvm, Wasm32Wasip3] False InstallErrorsHuman)))
      --         (parseCommandPure ["install", "text", "--store", "/tmp/aihc-store", "--target", "llvm", "--target", "wasm32-wasip3"]),
      --     testCase "parses install keep-core and keep-grin" $
      --       case parseCommandPure ["install", "text", "--target", "llvm", "--keep-core", "--keep-grin"] of
      --         Right (CmdInstall options) -> do
      --           assertBool "keeps Core" (installKeepCore options)
      --           assertBool "keeps GRIN" (installKeepGrin options)
      --         result -> assertFailure ("expected install options, got: " <> show result),
      --     testCase "parses install-v2 options" $
      --       assertEqual
      --         "command"
      --         (Right (CmdInstallV2 (InstallV2Options "core-libs/aihc-prim" (Just "/tmp/aihc-store") True)))
      --         (parseCommandPure ["install-v2", "core-libs/aihc-prim", "--store", "/tmp/aihc-store", "--verbose"]),
      --     testCase "parses repl" $
      --       assertEqual "command" (Right (CmdRepl (ReplOptions Nothing))) (parseCommandPure ["repl"]),
      --     testCase "parses repl store" $
      --       assertEqual "command" (Right (CmdRepl (ReplOptions (Just "/tmp/aihc-store")))) (parseCommandPure ["repl", "--store", "/tmp/aihc-store"])
      --   ],
      -- testGroup
      --   "compile"
      --   [ testCase "uses standard Clang for the WebAssembly target" $ do
      --       assertEqual "default command" ("clang", ["--target=wasm32-unknown-unknown"]) (wasmClangCommand Nothing)
      --       assertEqual "configured command" ("custom-clang", ["--target=wasm32-unknown-unknown"]) (wasmClangCommand (Just "custom-clang")),
      --     testCase "explains when Clang has no wasm32 target" test_missingWasm32ClangTarget,
      --     testCase "enables Binaryen optimization and tail calls" $
      --       assertEqual
      --         "wasm-opt arguments"
      --         ["input.wasm", "-O3", "--enable-tail-call", "--emit-target-features", "-o", "output.wasm"]
      --         (wasmOptArguments "input.wasm" "output.wasm"),
      --     testCase "compiles against an installed package without its sources" test_installedPackage,
      --     testCase "prepares an installed runtime archive" test_preparedRuntime,
      --     testCase "validates only primitives that survive GRIN lowering" test_runtimePrimitiveValidation,
      --     testCase "uses the shared XDG cache for compiled dependencies" test_compileDefaultEnvironment
      --   ],
      -- testGroup
      --   "repl"
      --   [ testCase "quits with long command" $ do
      --       session <- testReplSession
      --       step <- handleReplInput session ":quit"
      --       assertEqual "step" (ReplExit Nothing) step,
      --     testCase "quits with short command" $ do
      --       session <- testReplSession
      --       step <- handleReplInput session ":q"
      --       assertEqual "step" (ReplExit Nothing) step,
      --     testCase "prints help" $ do
      --       session <- testReplSession
      --       handleReplInput session ":help" >>= assertHelp
      --       handleReplInput session ":?" >>= assertHelp,
      --     testCase "ignores empty input" $ do
      --       session <- testReplSession
      --       step <- handleReplInput session "   "
      --       assertEqual "step" (ReplContinue Nothing) step,
      --     testCase "reports unknown commands" $ do
      --       session <- testReplSession
      --       step <- handleReplInput session ":unknown"
      --       assertEqual "step" (ReplContinue (Just "unknown command: :unknown")) step,
      --     testCase "browses exported Prelude types and terms" $ do
      --       session <- loadReplSession Nothing
      --       step <- handleReplInput session ":browse Prelude"
      --       case step of
      --         ReplContinue (Just output) -> do
      --           assertOutputLinePrefix "exported type" "type Bool" output
      --           assertOutputLine "local function" "id ∷ ∀ a. a → a" output
      --           assertOutputLine "symbolic function" "(++) ∷ ∀ a. [a] → [a] → [a]" output
      --           assertOutputLine "re-exported function" "not ∷ Bool → Bool" output
      --           assertBool "private helper should not be browsable" (not ("fmapList ∷" `isInfixOf` output))
      --         other -> assertFailure ("expected browse output, got " <> show other),
      --     testCase "reports browse usage and unknown modules" $ do
      --       session <- testReplSession
      --       handleReplInput session ":browse"
      --         >>= assertEqual "missing module" (ReplContinue (Just "usage: :browse <module>"))
      --       handleReplInput session ":browse Does.Not.Exist"
      --         >>= assertEqual "unknown module" (ReplContinue (Just "unknown module: Does.Not.Exist")),
      --     testCase "completes browse module names" $ do
      --       session <- loadReplSession Nothing
      --       (_, completions) <- replCompletion session (reverse ":browse Pre", "")
      --       assertEqual "module completions" ["Prelude"] (map Haskeline.replacement completions)
      --       (_, unrelatedCompletions) <- replCompletion session (reverse "Pre", "")
      --       assertEqual "unrelated completions" [] unrelatedCompletions,
      --     testCase "reports expression types with long and short commands" $ do
      --       session <- loadReplSession Nothing
      --       longStep <- handleReplInput session ":type True"
      --       assertEqual "long command" (ReplContinue (Just "True ∷ Bool")) longStep
      --       shortStep <- handleReplInput session ":t not"
      --       assertEqual "short command" (ReplContinue (Just "not ∷ Bool → Bool")) shortStep
      --       polymorphicStep <- handleReplInput session ":t \\x -> x"
      --       assertEqual "polymorphic expression" (ReplContinue (Just "\\x -> x ∷ ∀ a. a → a")) polymorphicStep,
      --     testCase "solves and normalizes type command constraints" $ do
      --       session <- loadReplSession Nothing
      --       operatorStep <- handleReplInput session ":t (+)"
      --       assertEqual "operator" (ReplContinue (Just "(+) ∷ ∀ a. (Num a) ⇒ a → a → a")) operatorStep
      --       partialStep <- handleReplInput session ":t (+) 1"
      --       assertEqual "polymorphic partial application" (ReplContinue (Just "(+) 1 ∷ ∀ a. (Num a) ⇒ a → a")) partialStep
      --       intStep <- handleReplInput session ":t (+) (1::Int)"
      --       assertEqual "concrete instance" (ReplContinue (Just "(+) (1::Int) ∷ Int → Int")) intStep
      --       boolStep <- handleReplInput session ":t (+) (1::Bool)"
      --       assertEqual "missing concrete instance" (ReplContinue (Just "type error: no instance for Num Bool")) boolStep,
      --     testCase "evaluates string expressions through the pipeline" $ do
      --       session <- loadReplSession Nothing
      --       step <- handleReplInput session "\"hello world\""
      --       assertEqual "step" (ReplContinue (Just "\"hello world\"")) step,
      --     testCase "evaluates let-bound strings through the pipeline" $ do
      --       session <- loadReplSession Nothing
      --       step <- handleReplInput session "let a = \"hello world\" in a"
      --       assertEqual "step" (ReplContinue (Just "\"hello world\"")) step,
      --     testCase "evaluateExpression returns string values" $ do
      --       session <- loadReplSession Nothing
      --       result <- evaluateExpression session "\"hello world\""
      --       assertEqual "result" (Right "\"hello world\"") result,
      --     testCase "evaluateExpression reports parse errors" $ do
      --       session <- testReplSession
      --       result <- evaluateExpression session "1 +"
      --       assertEqual "result" (Left ReplParseError) result,
      --     testCase "sets evaluation detail output from the repl" $ do
      --       session <- loadReplSession Nothing
      --       setStep <- handleReplInput session ":set +pretty"
      --       assertEqual "set step" (ReplContinue (Just "settings: +parsed-pretty -parsed-shorthand -type -system-fc")) setStep
      --       setStep2 <- handleReplInput session ":set +shorthand"
      --       assertEqual "set step" (ReplContinue (Just "settings: +parsed-pretty +parsed-shorthand -type -system-fc")) setStep2
      --       step <- handleReplInput session "\"hello\""
      --       case step of
      --         ReplContinue (Just output) -> do
      --           assertBool ("expected parsed output, got:\n" <> output) ("parsed:\n\"hello\"" `isInfixOf` output)
      --           assertBool ("expected shorthand output, got:\n" <> output) ("shorthand:\nEString \"hello\"" `isInfixOf` output)
      --           assertBool "expected final value" ("\"hello\"" `isSuffixOf` output)
      --         other -> assertFailure ("expected output, got " <> show other),
      --     testCase "sets type and system-fc output from the repl" $ do
      --       session <- loadReplSession Nothing
      --       _ <- handleReplInput session ":set +type"
      --       _ <- handleReplInput session ":set +fc"
      --       result <- evaluateExpression session "\"hello\""
      --       case result of
      --         Right output -> do
      --           assertBool ("expected type output, got:\n" <> T.unpack output) ("type:\n[Char]" `T.isInfixOf` output)
      --           assertBool
      --             ("expected system-fc output, got:\n" <> T.unpack output)
      --             ( "system-fc:\nmodule \"repl\" Aihc.Repl where" `T.isInfixOf` output
      --                 && "__aihc_repl_it : [Char] =" `T.isInfixOf` output
      --             )
      --           assertBool ("expected desugared char list, got:\n" <> T.unpack output) (not ("LitString" `T.isInfixOf` output))
      --         Left err -> assertFailure ("expected success, got " <> show err),
      --     testCase "loads bundled aihc-base Prelude by default" $ do
      --       session <- loadReplSession Nothing
      --       result <- evaluateExpression session "otherwise"
      --       assertEqual "result" (Right "True") result,
      --     testCase "keeps bundled package identities" $ do
      --       session <- loadReplSession Nothing
      --       let exports = replModuleExports session
      --       assertBool
      --         "aihc-base Prelude"
      --         (Map.member (ModuleKey (Package "aihc-base" (PackageId "aihc-base")) "Prelude") exports)
      --       assertBool
      --         "aihc-prim GHC.Types"
      --         (Map.member (ModuleKey (Package "aihc-prim" (PackageId "aihc-prim")) "GHC.Types") exports)
      --       assertBool
      --         "no unnamed Prelude"
      --         (Map.notMember (ModuleKey unnamedPackage "Prelude") exports),
      --     testCase "loads installed base interface for Prelude MVP scope" test_loadsInstalledBaseInterfaceForRepl,
      --     testCase "round-trips the shared package interface" test_packageInterfaceRoundTrip,
      --     testCase "rejects unsupported package interface schemas" $
      --       let decoded = Aeson.eitherDecode (Aeson.encode (object ["schemaVersion" .= (2 :: Int)])) :: Either String PackageInterface
      --        in assertLeftContains "unsupported package interface schema version" decoded
      --   ],
      -- testGroup
      --   "install"
      --   [ testCase "forms readable package-variant library identities" $
      --       assertEqual
      --         "library id"
      --         ["aihc", "base", "4", "21", "2", "0", "dephash"]
      --         (packageVariantLibraryId (PackageVariantKey (PackageSpec "aihc-base" "4.21.2.0") (PackageHash "dephash") [] [])),
      --     testCase "builds stable store paths" test_stableStorePath,
      --     testCase "recursive dependencies affect store paths" test_recursiveDependenciesAffectStorePaths,
      --     testCase "plans library components without executables" test_plansLibraryComponentsWithoutExecutables,
      --     testCase "writes scaffold artifacts" test_writeInstallScaffold,
      --     testCase "writes dependency scaffold artifacts first" test_writeInstallScaffoldWritesDependencies,
      --     testCase "keeps selected library intermediate files" test_keepsLibraryIntermediateFiles,
      --     testCase "reports parse errors and writes no artifacts" test_reportsParseErrorsAndWritesNoArtifacts,
      --     testCase "reports rename errors and writes no artifacts" test_reportsRenameErrorsAndWritesNoArtifacts,
      --     testCase "renders preprocessed source for rename errors" test_rendersPreprocessedSourceForRenameErrors,
      --     testCase "reports type-check errors and writes no artifacts" test_reportsTypeCheckErrorsAndWritesNoArtifacts,
      --     testCase "reports desugar errors and writes no artifacts" test_reportsDesugarErrorsAndWritesNoArtifacts,
      --     testCase "limits rendered install errors to first module" test_limitsRenderedInstallErrorsToFirstModule,
      --     testCase "renders human install errors" test_rendersHumanInstallErrors,
      --     testCase "renders JSON install errors on request" test_rendersJsonInstallErrors,
      --     testCase "does not install dependencies when root package fails" test_failedRootDoesNotInstallDependencies,
      --     testCase "type-checks references to dependency bindings" test_typeChecksReferencesToDependencyBindings,
      --     testCase "type-checks with dependency type environments" test_typeChecksWithDependencyTypeEnvironments,
      --     testCase "checks cast-style instance methods using dependency id" test_checksCastStyleDependencyId,
      --     testCase "checks constraint-kinded multi-parameter classes" test_checksConstraintKindedMultiParameterClasses,
      --     testCase "checks packages without writing install artifacts" test_checkPackagePlanWritesNoArtifacts,
      --     testCase "uses local provider for base dependencies" test_usesLocalProviderForBase,
      --     testCase "uses local provider for ghc-prim dependencies" test_usesLocalProviderForGhcPrim,
      --     testCase "uses local provider for ghc-internal dependencies" test_usesLocalProviderForGhcInternal,
      --     testCase "uses virtual provider for system-cxx-std-lib dependencies" test_usesVirtualProviderForSystemCxxStdLib,
      --     testCase "manifest records core provider dependency names" test_manifestRecordsCoreProviderDependencyNames,
      --     testCase "installs resolved base dependency closure" test_installsResolvedBaseDependencyClosure,
      --     testCase "dry run writes no scaffold artifacts" test_dryRunWritesNoScaffoldArtifacts,
      --     testCase "dry run planner does not generate source files" test_dryRunPlannerDoesNotGenerateSourceFiles
      --   ],
      testGroup
        "install-v2"
        [ testCase "writes one resolve artifact for each module and reuses equal SCC inputs" test_installV2ResolveArtifacts,
          testCase "rebuilds a module when a predecessor resolve artifact changes" test_installV2ResolveDependencies,
          testCase "rebuilds a module when a predecessor type interface changes" test_installV2TypeDependencies,
          testCase "duplicates re-exported term signatures in type interfaces" test_installV2TypeReexports,
          testCase "installs direct local dependencies" test_installV2LocalDependencies,
          testCase "rebuilds stale type artifact schemas" test_installV2StaleTypeArtifact,
          testCase "stops invalidation when a rebuilt scope stays equal" test_installV2StopsAtEqualScope
        ],
      testProperty "Hedgehog options" prop_dummy
    ]

prop_dummy :: Property
prop_dummy = property success

test_installV2ResolveArtifacts :: Assertion
test_installV2ResolveArtifacts =
  withTempDir "aihc-install-v2" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
        sourceDir = sourceRoot </> "src" </> "Demo"
        options = InstallV2Options sourceRoot (Just storeRoot) False
    createDirectoryIfMissing True sourceDir
    writeFile
      (sourceRoot </> "demo.cabal")
      ( unlines
          [ "cabal-version: 3.0",
            "name: demo",
            "version: 0.1.0.0",
            "library",
            "  exposed-modules: Demo.A, Demo.B",
            "  hs-source-dirs: src",
            "  default-language: Haskell2010"
          ]
      )
    writeFile (sourceDir </> "A.hs") "module Demo.A where\nimport Demo.B\na x = x\n"
    writeFile (sourceDir </> "B.hs") "module Demo.B where\nimport Demo.A\nb x = x\n"
    first <- installV2 options
    assertEqual "written modules" ["Demo.A", "Demo.B"] (sort (installV2WrittenModules first))
    assertFileExists (installV2StorePath first </> "Demo" </> "A" </> "resolve.cbor")
    assertFileExists (installV2StorePath first </> "Demo" </> "B" </> "resolve.cbor")
    assertFileExists (installV2StorePath first </> "Demo" </> "A" </> "type.cbor")
    assertFileExists (installV2StorePath first </> "Demo" </> "B" </> "type.cbor")
    assertCoreFile (installV2StorePath first </> "Demo" </> "A" </> "core")
    assertCoreFile (installV2StorePath first </> "Demo" </> "B" </> "core")
    second <- installV2 options
    assertEqual "reused modules" ["Demo.A", "Demo.B"] (sort (installV2ReusedModules second))
    assertEqual "stable package directory" (installV2StorePath first) (installV2StorePath second)
    removeFile (installV2StorePath first </> "Demo" </> "A" </> "core")
    coreRepaired <- installV2 options
    assertEqual "repairs the complete SCC with cached types" ["Demo.A", "Demo.B"] (sort (installV2WrittenModules coreRepaired))
    writeFile (sourceDir </> "B.hs") "module Demo.B where\nimport Demo.A\nb x = (x)\n"
    changed <- installV2 options
    assertEqual "source changes keep the package directory" (installV2StorePath first) (installV2StorePath changed)
    assertEqual "source changes rebuild the complete SCC" ["Demo.A", "Demo.B"] (sort (installV2WrittenModules changed))
    let artifact = installV2StorePath first </> "Demo" </> "A" </> "resolve.cbor"
    artifactBytes <- BS.readFile artifact
    BS.writeFile artifact (BS.init artifactBytes)
    repaired <- installV2 options
    assertEqual "repairs the complete corrupt SCC" ["Demo.A", "Demo.B"] (sort (installV2WrittenModules repaired))
    assertEqual "does not reuse a corrupt SCC" [] (installV2ReusedModules repaired)

assertCoreFile :: FilePath -> Assertion
assertCoreFile path = do
  assertFileExists path
  core <- TIO.readFile path
  case Fc.parseProgram core of
    Left parseError -> assertFailure ("invalid Core file " <> path <> ": " <> Fc.renderParseError parseError)
    Right program -> assertEqual ("Core lint errors in " <> path) [] (Fc.lintProgram Fc.emptyLintEnv program)

test_installV2TypeDependencies :: Assertion
test_installV2TypeDependencies =
  withTempDir "aihc-install-v2-type-dependencies" $ \root -> do
    let sourceRoot = root </> "source"
        sourceDir = sourceRoot </> "src" </> "Demo"
        options = InstallV2Options sourceRoot (Just (root </> "store")) False
    createDirectoryIfMissing True sourceDir
    writeFile
      (sourceRoot </> "demo.cabal")
      ( unlines
          [ "cabal-version: 3.0",
            "name: demo",
            "version: 0.1.0.0",
            "library",
            "  exposed-modules: Demo.A, Demo.B",
            "  hs-source-dirs: src",
            "  default-language: Haskell2010"
          ]
      )
    writeFile (sourceDir </> "A.hs") "module Demo.A where\nimport Demo.B\na = b\n"
    writeFile (sourceDir </> "B.hs") "module Demo.B where\nb x = x\n"
    _ <- installV2 options
    writeFile (sourceDir </> "B.hs") "module Demo.B where\nb x y = x\n"
    changed <- installV2 options
    assertEqual "type change and direct dependent" ["Demo.A", "Demo.B"] (sort (installV2WrittenModules changed))
    assertEqual "no reused dependent after type change" [] (installV2ReusedModules changed)

test_installV2TypeReexports :: Assertion
test_installV2TypeReexports =
  withTempDir "aihc-install-v2-type-reexports" $ \root -> do
    let sourceRoot = root </> "source"
        sourceDir = sourceRoot </> "src" </> "Demo"
        options = InstallV2Options sourceRoot (Just (root </> "store")) False
    createDirectoryIfMissing True sourceDir
    writeFile
      (sourceRoot </> "demo.cabal")
      ( unlines
          [ "cabal-version: 3.0",
            "name: demo",
            "version: 0.1.0.0",
            "library",
            "  exposed-modules: Demo.A, Demo.B",
            "  hs-source-dirs: src",
            "  default-language: Haskell2010"
          ]
      )
    writeFile
      (sourceDir </> "A.hs")
      "module Demo.A where\ndata Box a = Box a\nclass Identity a where\n  identity :: a -> a\nfn x = x\n"
    writeFile (sourceDir </> "B.hs") "module Demo.B (module Demo.A) where\nimport Demo.A\n"
    result <- installV2 options
    bytes <- BS.readFile (installV2StorePath result </> "Demo" </> "B" </> "type.cbor")
    artifact <- either assertFailure pure (decodeTypeArtifact bytes)
    let termNames = mapMaybe (tcTermKeyIdentifier . fst) (tcInterfaceTerms (typeArtifactInterface artifact))
    assertBool "re-exported signature" ("fn" `elem` termNames)

test_installV2LocalDependencies :: Assertion
test_installV2LocalDependencies =
  withTempDir "aihc-install-v2-local-dependencies" $ \root -> do
    let sourceRoot = root </> "demo"
        dependencyRoot = root </> "dep"
        storeRoot = root </> "store"
        options = InstallV2Options sourceRoot (Just storeRoot) False
    createDirectoryIfMissing True (sourceRoot </> "src")
    createDirectoryIfMissing True (dependencyRoot </> "src")
    writeFile
      (dependencyRoot </> "dep.cabal")
      ( unlines
          [ "cabal-version: 3.0",
            "name: dep",
            "version: 1.0.0",
            "library",
            "  exposed-modules: Dep",
            "  hs-source-dirs: src",
            "  default-language: Haskell2010"
          ]
      )
    writeFile (dependencyRoot </> "src" </> "Dep.hs") "module Dep where\nidentity x = x\n"
    writeFile
      (sourceRoot </> "demo.cabal")
      ( unlines
          [ "cabal-version: 3.0",
            "name: demo",
            "version: 0.1.0.0",
            "library",
            "  exposed-modules: Demo",
            "  hs-source-dirs: src",
            "  build-depends: dep",
            "  default-language: Haskell2010"
          ]
      )
    writeFile (sourceRoot </> "src" </> "Demo.hs") "module Demo where\nimport Dep\nresult = identity\n"
    _ <- installV2 options
    storeEntries <- listDirectory storeRoot
    let dependencyStores = filter ("dep-1.0.0-" `isPrefixOf`) storeEntries
    case dependencyStores of
      [dependencyStore] -> do
        assertFileExists (storeRoot </> dependencyStore </> "Dep" </> "resolve.cbor")
        assertFileExists (storeRoot </> dependencyStore </> "Dep" </> "type.cbor")
      _ -> assertFailure ("expected one installed dependency, got " <> show dependencyStores)

test_installV2StaleTypeArtifact :: Assertion
test_installV2StaleTypeArtifact =
  withTempDir "aihc-install-v2-stale-type" $ \root -> do
    let sourceRoot = root </> "source"
        sourceDir = sourceRoot </> "src"
        options = InstallV2Options sourceRoot (Just (root </> "store")) False
    createDirectoryIfMissing True sourceDir
    writeFile
      (sourceRoot </> "demo.cabal")
      ( unlines
          [ "cabal-version: 3.0",
            "name: demo",
            "version: 0.1.0.0",
            "library",
            "  exposed-modules: Demo",
            "  hs-source-dirs: src",
            "  default-language: Haskell2010"
          ]
      )
    writeFile (sourceDir </> "Demo.hs") "module Demo where\nvalue x = x\n"
    first <- installV2 options
    let artifactPath = installV2StorePath first </> "Demo" </> "type.cbor"
    artifact <- BS.readFile artifactPath
    BS.writeFile artifactPath (BS.take 11 artifact <> BS.singleton 1 <> BS.drop 12 artifact)
    rebuilt <- installV2 options
    assertEqual "rebuilt module" ["Demo"] (installV2WrittenModules rebuilt)

test_installV2ResolveDependencies :: Assertion
test_installV2ResolveDependencies =
  withTempDir "aihc-install-v2-dependencies" $ \root -> do
    let sourceRoot = root </> "source"
        sourceDir = sourceRoot </> "src" </> "Demo"
        options = InstallV2Options sourceRoot (Just (root </> "store")) False
    createDirectoryIfMissing True sourceDir
    writeFile
      (sourceRoot </> "demo.cabal")
      ( unlines
          [ "cabal-version: 3.0",
            "name: demo",
            "version: 0.1.0.0",
            "library",
            "  exposed-modules: Demo.A, Demo.B",
            "  hs-source-dirs: src",
            "  default-language: Haskell2010"
          ]
      )
    writeFile (sourceDir </> "A.hs") "module Demo.A where\nimport Demo.B\na = b\n"
    writeFile (sourceDir </> "B.hs") "module Demo.B where\nb x = x\n"
    _ <- installV2 options
    writeFile (sourceDir </> "B.hs") "module Demo.B where\nb x = (x)\n"
    sourceChanged <- installV2 options
    assertEqual "source-only change" ["Demo.B"] (sort (installV2WrittenModules sourceChanged))
    assertEqual "dependent with equal scope" ["Demo.A"] (sort (installV2ReusedModules sourceChanged))
    writeFile (sourceDir </> "B.hs") "module Demo.B where\nb x = x\nc x = x\n"
    scopeChanged <- installV2 options
    assertEqual "changed scope and dependent modules" ["Demo.A", "Demo.B"] (sort (installV2WrittenModules scopeChanged))
    assertEqual "no reused dependent after scope change" [] (installV2ReusedModules scopeChanged)

test_installV2StopsAtEqualScope :: Assertion
test_installV2StopsAtEqualScope =
  withTempDir "aihc-install-v2-scope-boundary" $ \root -> do
    let sourceRoot = root </> "source"
        sourceDir = sourceRoot </> "src" </> "Demo"
        options = InstallV2Options sourceRoot (Just (root </> "store")) False
    createDirectoryIfMissing True sourceDir
    writeFile
      (sourceRoot </> "demo.cabal")
      ( unlines
          [ "cabal-version: 3.0",
            "name: demo",
            "version: 0.1.0.0",
            "library",
            "  exposed-modules: Demo.A, Demo.B, Demo.C",
            "  hs-source-dirs: src",
            "  default-language: Haskell2010"
          ]
      )
    writeFile (sourceDir </> "A.hs") "module Demo.A where\na x = x\n"
    writeFile (sourceDir </> "B.hs") "module Demo.B where\nimport Demo.A\nb x = x\n"
    writeFile (sourceDir </> "C.hs") "module Demo.C where\nimport Demo.B\nc x = x\n"
    _ <- installV2 options
    writeFile (sourceDir </> "A.hs") "module Demo.A where\na x = x\na2 x = x\n"
    changed <- installV2 options
    assertEqual "changed module and direct dependent" ["Demo.A", "Demo.B"] (sort (installV2WrittenModules changed))
    assertEqual "transitive dependent with equal direct scope" ["Demo.C"] (sort (installV2ReusedModules changed))

{- Disabled with the related test groups in main.
assertHelp :: ReplStep -> Assertion
assertHelp (ReplContinue (Just output)) =
  assertBool "help mentions :quit" (":quit" `isInfixOf` output)
assertHelp other =
  assertFailure ("expected help output, got " <> show other)

assertOutputLine :: String -> String -> String -> Assertion
assertOutputLine label expected output =
  assertBool (label <> ": expected line " <> show expected <> ", got:\n" <> output) (expected `elem` lines output)

assertOutputLinePrefix :: String -> String -> String -> Assertion
assertOutputLinePrefix label expectedPrefix output =
  assertBool (label <> ": expected line starting with " <> show expectedPrefix <> ", got:\n" <> output) (any (expectedPrefix `isPrefixOf`) (lines output))

assertLeftContains :: String -> Either String a -> Assertion
assertLeftContains expected (Left actual) =
  assertBool ("expected error to contain " <> show expected <> ", got " <> show actual) (expected `isInfixOf` actual)
assertLeftContains _ (Right _) =
  assertFailure "expected parse failure"

testReplSession :: IO ReplSession
testReplSession = do
  settingsRef <- newIORef defaultReplSettings
  pure
    ReplSession
      { replModuleExports = Map.empty,
        replImportedTerms = [],
        replBindingTypes = Map.empty,
        replImportedInstances = [],
        replTcInterface = emptyTcInterface,
        replDependencyProgram = FcProgram (FcModuleId "test" "Test") [],
        replSettings = settingsRef
      }
-}

{- Disabled with the related test groups in main.
test_loadsInstalledBaseInterfaceForRepl :: Assertion
test_loadsInstalledBaseInterfaceForRepl =
  withTempDir "aihc-repl" $ \root -> do
    let storeRoot = root </> "store"
        packageRoot = storeRoot </> "0000000000000000-base-4_22_0_0"
        interfacePath = packageRoot </> "interfaces" </> "package-interface.json"
        manifestPath = packageRoot </> "manifest.json"
    createDirectoryIfMissing True (takeDirectory interfacePath)
    BL8.writeFile
      manifestPath
      ( Aeson.encode
          ( object
              [ "package" .= object ["name" .= ("aihc-base" :: String), "version" .= ("4.21.2.0" :: String)],
                "interfacePath" .= interfacePath
              ]
          )
      )
    BL8.writeFile
      interfacePath
      ( Aeson.encode
          ( object
              [ "schemaVersion" .= (1 :: Int),
                "packageKey"
                  .= object
                    [ "hash" .= ("dephash" :: String),
                      "package" .= object ["name" .= ("aihc-base" :: String), "version" .= ("4.21.2.0" :: String)],
                      "flags" .= ([] :: [Aeson.Value]),
                      "dependencies" .= ([] :: [Aeson.Value])
                    ],
                "status" .= ("complete" :: String),
                "contains" .= (["name-resolution", "types", "fixities"] :: [String]),
                "sourceFiles" .= ([] :: [FilePath]),
                "moduleCount" .= (2 :: Int),
                "modules"
                  .= [ object
                         [ "module" .= ("Prelude" :: String),
                           "terms" .= ([] :: [String]),
                           "types" .= ([] :: [String]),
                           "constructors" .= object [],
                           "recordFields" .= object [],
                           "methods" .= object []
                         ],
                       object
                         [ "module" .= ("Data.Installed" :: String),
                           "terms" .= ([] :: [String]),
                           "types" .= ([] :: [String]),
                           "constructors" .= object [],
                           "recordFields" .= object [],
                           "methods" .= object []
                         ]
                     ],
                "diagnostics"
                  .= object
                    [ "cpp" .= ([] :: [Aeson.Value]),
                      "parse" .= ([] :: [Aeson.Value]),
                      "resolve" .= ([] :: [Aeson.Value]),
                      "typecheck" .= ([] :: [Aeson.Value])
                    ],
                "typecheck" .= ([] :: [Aeson.Value])
              ]
          )
      )
    session <- loadReplSession (Just storeRoot)
    assertBool
      "installed package identity"
      ( Map.member
          (ModuleKey (Package "aihc-base" (PackageId "aihc-base-4-21-2-0-dephash")) "Data.Installed")
          (replModuleExports session)
      )
    case Map.lookup (ModuleKey (Package "aihc-base" (PackageId "aihc-base")) "Prelude") (replModuleExports session) of
      Nothing -> assertFailure "Prelude scope not loaded"
      Just preludeScope -> do
        assertBool "Prelude exposes Char" (Map.member "Char" (scopeTypes preludeScope))
        assertBool "Prelude exposes String" (Map.member "String" (scopeTypes preludeScope))
    result <- evaluateExpression session "\"hello\""
    assertEqual "result" (Right "\"hello\"") result
    _ <- handleReplInput session ":set +type"
    typedStringResult <- evaluateExpression session "\"hello\""
    case typedStringResult of
      Right output ->
        assertBool ("expected [Char] type, got:\n" <> T.unpack output) ("type:\n[Char]" `T.isInfixOf` output)
      Left err -> assertFailure ("expected typed string success, got " <> show err)

test_packageInterfaceRoundTrip :: Assertion
test_packageInterfaceRoundTrip = do
  let typeVariable = TyVarId "a" (Unique 11)
      identityType = TcForAllTy typeVariable (TcFunTy (TcTyVar typeVariable) (TcTyVar typeVariable))
      packageKey =
        PackageInterfacePackageKey
          { packageInterfaceKeyHash = "dephash",
            packageInterfaceKeyPackage = PackageInterfacePackageSpec "demo" "1.2.3",
            packageInterfaceKeyFlags = [PackageInterfaceFlag "debug" True],
            packageInterfaceKeyDependencies =
              [PackageInterfaceDependency (PackageInterfacePackageSpec "base" "4.21.2.0") "basehash"]
          }
      interface =
        PackageInterface
          { packageInterfacePackageKey = packageKey,
            packageInterfaceStatus = "complete",
            packageInterfaceContains = ["name-resolution", "types", "fixities"],
            packageInterfaceSourceFiles = ["src/Demo.hs"],
            packageInterfaceModuleCount = 1,
            packageInterfaceModules =
              [ PackageInterfaceModule
                  { packageInterfaceModuleName = "Demo",
                    packageInterfaceModuleTerms = ["identity"],
                    packageInterfaceModuleTypes = [],
                    packageInterfaceModuleConstructors = Map.empty,
                    packageInterfaceModuleRecordFields = Map.empty,
                    packageInterfaceModuleMethods = Map.empty
                  }
              ],
            packageInterfaceDiagnostics = PackageInterfaceDiagnostics [] [] [] [],
            packageInterfaceTypecheck =
              [PackageInterfaceTcModule "Demo" True [PackageInterfaceBinding "identity" identityType] []]
          }
      decoded = Aeson.eitherDecode (Aeson.encode interface)
  assertEqual "package interface" (Right interface) decoded
  assertBool
    "decoded package identity"
    (Map.member (ModuleKey (Package "demo" (PackageId "demo-1-2-3-dephash")) "Demo") (packageInterfaceExports interface))

test_stableStorePath :: Assertion
test_stableStorePath =
  withFixturePackage $ \sourceRoot storeRoot -> do
    plan1 <- buildPackagePlanFromSource storeRoot (PackageSpec "demo" "0.1.0.0") sourceRoot
    plan2 <- buildPackagePlanFromSource storeRoot (PackageSpec "demo" "0.1.0.0") sourceRoot
    assertEqual "store path" (planStorePath plan1) (planStorePath plan2)
    assertBool "store path includes package" ("demo-0_1_0_0" `isInfixOf` takeFileName (planStorePath plan1))
    assertBool "store path starts below root" (storeRoot `isPrefixOf` planStorePath plan1)

test_recursiveDependenciesAffectStorePaths :: Assertion
test_recursiveDependenciesAffectStorePaths =
  withDependencyFixture $ \sourceRoot depV1Root depV2Root storeRoot -> do
    plan1 <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depV1Root)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    plan2 <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "2.0.0", depV2Root)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    assertBool "dependency variant should change store path" (planStorePath plan1 /= planStorePath plan2)

test_plansLibraryComponentsWithoutExecutables :: Assertion
test_plansLibraryComponentsWithoutExecutables =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        depRoot = root </> "dep"
        storeRoot = root </> "store"
    createDirectoryIfMissing True (sourceRoot </> "src")
    createDirectoryIfMissing True (sourceRoot </> "src" </> "Demo")
    createDirectoryIfMissing True (sourceRoot </> "internal" </> "Demo")
    createDirectoryIfMissing True (sourceRoot </> "app")
    createDirectoryIfMissing True storeRoot
    writeFile (sourceRoot </> "demo.cabal") libraryOnlyInstallCabal
    writeFile (sourceRoot </> "src" </> "Demo.hs") "module Demo where\nimport Demo.Internal\nimport Demo.Private\nimport Dep\nx = depId (privateValue internalValue)\n"
    writeFile (sourceRoot </> "src" </> "Demo" </> "Private.hs") "module Demo.Private where\nprivateValue x = x\n"
    writeFile (sourceRoot </> "internal" </> "Demo" </> "Internal.hs") ("module Demo.Internal where\ninternalValue = ()\n" <> fixtureTupleDeclaration)
    writeFile (sourceRoot </> "app" </> "Main.hs") "module Main where\nthis executable is intentionally invalid\n"
    createFixturePackageWithSource depRoot "dep" "1.0.0" "Dep" [] "module Dep where\ndepId x = x\n"

    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")

    assertEqual "library source file count" 3 (planSourceFileCount plan)
    assertEqual "external library dependencies" ["dep"] [pkgName (packageKeySpec (planPackageKey dependencyPlan)) | dependencyPlan <- planDependencyPlans plan]
    _ <- expectInstallSuccess (checkPackagePlan plan)
    result <- expectInstallSuccess (writeInstallScaffold plan)
    interface <- BL8.readFile (resultInterfacePath result)
    case Aeson.decode interface of
      Just (Aeson.Object artifact) ->
        case KeyMap.lookup "modules" artifact of
          Just (Aeson.Array modules) -> do
            let moduleNames =
                  [ name
                  | Aeson.Object moduleEntry <- foldr (:) [] modules,
                    Just (Aeson.String name) <- [KeyMap.lookup "module" moduleEntry]
                  ]
            assertEqual "public interface modules" ["Demo", "Demo.Internal"] moduleNames
          other -> assertFailure ("expected interface modules, got " <> show other)
      other -> assertFailure ("expected interface artifact, got " <> show other)

test_writeInstallScaffold :: Assertion
test_writeInstallScaffold =
  withDependencyFixture $ \sourceRoot depRoot _ storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    result <- expectInstallSuccess (writeInstallScaffold plan)
    assertFileExists (resultManifestPath result)
    assertFileExists (resultInterfacePath result)
    assertFileExists (resultFcPath result)

    manifest <- BL8.readFile (resultManifestPath result)
    let renderedManifest = BL8.unpack manifest
    assertBool "manifest records setup phase" ("compile-setup" `isInfixOf` renderedManifest)
    assertBool "manifest omits GHC version" (not ("ghcVersion" `isInfixOf` renderedManifest))
    assertBool "manifest records package key" ("packageKey" `isInfixOf` renderedManifest)
    assertBool "manifest records dependencies" ("dependencies" `isInfixOf` renderedManifest)
    assertBool "manifest records dependency package" ("dep" `isInfixOf` renderedManifest)
    assertBool "manifest records source count" ("sourceFileCount" `isInfixOf` renderedManifest)
    assertBool "manifest records completed desugaring" ("desugar-system-fc" `isInfixOf` renderedManifest && "Generate desugared System-FC data files" `isInfixOf` renderedManifest)

    interfaceJson <- BL8.readFile (resultInterfacePath result)
    let renderedInterface = BL8.unpack interfaceJson
    assertBool "interface includes fixities" ("fixities" `isInfixOf` renderedInterface)
    assertBool "interface includes modules" ("modules" `isInfixOf` renderedInterface)
    assertBool "interface includes typecheck data" ("typecheck" `isInfixOf` renderedInterface)
    assertBool "interface is implemented" (not ("unimplemented" `isInfixOf` renderedInterface))
    decodedInterface <- readPackageInterface (resultInterfacePath result)
    case decodedInterface of
      Left err -> assertFailure ("expected valid package interface, got " <> err)
      Right interface -> assertEqual "decoded interface module count" 1 (packageInterfaceModuleCount interface)

    fcJson <- BL8.readFile (resultFcPath result)
    let renderedFc = BL8.unpack fcJson
    assertBool "fc artifact includes system-fc" ("system-fc" `isInfixOf` renderedFc)
    assertBool "fc artifact is complete" ("\"status\":\"complete\"" `isInfixOf` renderedFc)
    assertBool "fc artifact contains desugared modules" ("\"program\"" `isInfixOf` renderedFc)

test_writeInstallScaffoldWritesDependencies :: Assertion
test_writeInstallScaffoldWritesDependencies =
  withDependencyFixture $ \sourceRoot depRoot _ storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    _ <- expectInstallSuccess (writeInstallScaffold plan)
    dependencyPlan <- findPlanByName "dep" (planDependencyPlans plan)
    assertFileExists (planStorePath dependencyPlan </> "manifest.json")
    assertFileExists (planStorePath dependencyPlan </> "interfaces" </> "package-interface.json")
    assertFileExists (planStorePath dependencyPlan </> "fc" </> "package-fc.json")

test_keepsLibraryIntermediateFiles :: Assertion
test_keepsLibraryIntermediateFiles =
  withTempDir "aihc-library-intermediates" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
    createFixturePackage sourceRoot "demo" "0.1.0.0" "Demo" []
    createDirectoryIfMissing True storeRoot
    plan <- buildPackagePlanFromSource storeRoot (PackageSpec "demo" "0.1.0.0") sourceRoot
    initialResult <- installPackageLibraries [Llvm] False False plan
    artifactRoot <- either assertFailure pure initialResult
    let corePath = artifactRoot </> "core" </> "demo" </> "Demo.core"
        grinPath = artifactRoot </> "grin" </> "demo" </> "Demo.grin"
        cpsGrinPath = artifactRoot </> "grin" </> "demo" </> "Demo.cps.grin"
        gcGrinPath = artifactRoot </> "grin" </> "demo" </> "Demo.gc.grin"
    assertFileDoesNotExist corePath
    assertFileDoesNotExist grinPath
    coreResult <- installPackageLibraries [Llvm] True False plan
    either assertFailure (assertEqual "Core artifact root" artifactRoot) coreResult
    assertFileExists corePath
    assertFileDoesNotExist grinPath
    grinResult <- installPackageLibraries [Llvm] False True plan
    either assertFailure (assertEqual "GRIN artifact root" artifactRoot) grinResult
    assertFileExists grinPath
    assertFileExists cpsGrinPath
    assertFileExists gcGrinPath

test_reportsParseErrorsAndWritesNoArtifacts :: Assertion
test_reportsParseErrorsAndWritesNoArtifacts =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" [] "module Demo where\nx =\n"
    createDirectoryIfMissing True storeRoot
    plan <- buildPackagePlanFromSource storeRoot (PackageSpec "demo" "0.1.0.0") sourceRoot

    err <- renderInstallFailure <$> expectInstallFailure (writeInstallScaffold plan)
    assertBool "error reports parse phase" ("parse errors" `isInfixOf` err)
    assertFileDoesNotExist (planStorePath plan </> "manifest.json")
    assertFileDoesNotExist (planStorePath plan </> "interfaces" </> "package-interface.json")
    assertFileDoesNotExist (planStorePath plan </> "fc" </> "package-fc.json")

test_reportsRenameErrorsAndWritesNoArtifacts :: Assertion
test_reportsRenameErrorsAndWritesNoArtifacts =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" [] "module Demo where\nimport Missing.Module\nx = 1\n"
    createDirectoryIfMissing True storeRoot
    plan <- buildPackagePlanFromSource storeRoot (PackageSpec "demo" "0.1.0.0") sourceRoot

    err <- renderInstallFailure <$> expectInstallFailure (writeInstallScaffold plan)
    assertBool "error reports package" ("failed to install demo-0.1.0.0" `isInfixOf` err)
    assertBool "error reports rename phase" ("rename errors" `isInfixOf` err)
    assertBool "error includes missing import" ("Missing.Module" `isInfixOf` err)
    assertFileDoesNotExist (planStorePath plan </> "manifest.json")
    assertFileDoesNotExist (planStorePath plan </> "interfaces" </> "package-interface.json")
    assertFileDoesNotExist (planStorePath plan </> "fc" </> "package-fc.json")

test_reportsTypeCheckErrorsAndWritesNoArtifacts :: Assertion
test_reportsTypeCheckErrorsAndWritesNoArtifacts =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" [] "module Demo where\ndata List a = [] | a : [a]\nx = [(), 'a']\n"
    createDirectoryIfMissing True storeRoot
    plan <- buildPackagePlanFromSource storeRoot (PackageSpec "demo" "0.1.0.0") sourceRoot

    err <- renderInstallFailure <$> expectInstallFailure (writeInstallScaffold plan)
    assertBool "error reports package" ("failed to install demo-0.1.0.0" `isInfixOf` err)
    assertBool "error reports type-check phase" ("type-check errors" `isInfixOf` err)
    assertBool "error includes unification diagnostic" ("UnificationError" `isInfixOf` err)
    assertFileDoesNotExist (planStorePath plan </> "manifest.json")
    assertFileDoesNotExist (planStorePath plan </> "interfaces" </> "package-interface.json")
    assertFileDoesNotExist (planStorePath plan </> "fc" </> "package-fc.json")

test_reportsDesugarErrorsAndWritesNoArtifacts :: Assertion
test_reportsDesugarErrorsAndWritesNoArtifacts =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
        source = "{-# LANGUAGE MagicHash #-}\nmodule Demo where\nforeign import prim mystery# :: Int# -> Int#\n"
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" [] source
    createDirectoryIfMissing True storeRoot
    plan <- buildPackagePlanFromSource storeRoot (PackageSpec "demo" "0.1.0.0") sourceRoot

    err <- renderInstallFailure <$> expectInstallFailure (writeInstallScaffold plan)
    assertBool "error reports desugar phase" ("desugar errors" `isInfixOf` err)
    assertBool "error includes unknown primitive" ("unknown foreign import prim" `isInfixOf` err)
    assertFileDoesNotExist (planStorePath plan </> "manifest.json")
    assertFileDoesNotExist (planStorePath plan </> "interfaces" </> "package-interface.json")
    assertFileDoesNotExist (planStorePath plan </> "fc" </> "package-fc.json")

test_limitsRenderedInstallErrorsToFirstModule :: Assertion
test_limitsRenderedInstallErrorsToFirstModule = do
  let opts =
        defaultInstallOptionsForTest
          { installFirstErrorModule = True
          }
      err = renderInstallFailureWithOptions opts syntheticInstallFailure
  assertBool "error includes first module" ("selected module failed" `isInfixOf` err)
  assertBool "error omits other module" (not ("other module failed" `isInfixOf` err))

test_rendersHumanInstallErrors :: Assertion
test_rendersHumanInstallErrors = do
  let err = renderInstallFailure syntheticInstallFailure
  assertBool ("error includes human location, got:\n" <> err) ("src/Demo.hs:2:3: error: [Demo] selected module failed" `isInfixOf` err)
  assertBool ("error includes source line, got:\n" <> err) ("  2 | x = selected + problem" `isInfixOf` err)
  assertBool ("error includes caret range, got:\n" <> err) ("    |   ^^^^^^^^" `isInfixOf` err)
  assertBool "error omits JSON object syntax" (not ("{\"" `isInfixOf` err))

test_rendersJsonInstallErrors :: Assertion
test_rendersJsonInstallErrors = do
  let opts =
        defaultInstallOptionsForTest
          { installErrorFormat = InstallErrorsJson
          }
      err = renderInstallFailureWithOptions opts syntheticInstallFailure
  assertBool ("error includes JSON diagnostic, got:\n" <> err) ("{\"message\":\"selected module failed\"" `isInfixOf` err)

test_rendersPreprocessedSourceForRenameErrors :: Assertion
test_rendersPreprocessedSourceForRenameErrors =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
        source = "{-# LANGUAGE CPP #-}\nmodule Demo where\n#define MISSING missingAfterCpp\nx = MISSING\n"
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" [] source
    createDirectoryIfMissing True storeRoot
    plan <- buildPackagePlanFromSource storeRoot (PackageSpec "demo" "0.1.0.0") sourceRoot

    err <- renderInstallFailure <$> expectInstallFailure (writeInstallScaffold plan)
    assertBool ("error explains the unbound name, got:\n" <> err) ("unbound term name ‘missingAfterCpp’" `isInfixOf` err)
    assertBool ("error includes postprocessed source, got:\n" <> err) ("| x = missingAfterCpp" `isInfixOf` err)
    assertBool ("error includes a caret, got:\n" <> err) ("^^^^^^^^^^^^^^^" `isInfixOf` err)
    assertBool "error omits preprocessed macro use" (not ("| x = MISSING" `isInfixOf` err))

defaultInstallOptionsForTest :: InstallOptions
defaultInstallOptionsForTest =
  InstallOptions "demo" Nothing Nothing False False False False [] False InstallErrorsHuman

syntheticInstallFailure :: InstallFailure
syntheticInstallFailure =
  InstallInterfaceFailure
    (PackageSpec "demo" "0.1.0.0")
    [ ( "type-check",
        [ object
            [ "module" .= ("Demo" :: String),
              "span"
                .= object
                  [ "file" .= ("src/Demo.hs" :: String),
                    "startLine" .= (2 :: Int),
                    "startColumn" .= (3 :: Int),
                    "endLine" .= (2 :: Int),
                    "endColumn" .= (11 :: Int)
                  ],
              "severity" .= ("error" :: String),
              "message" .= ("selected module failed" :: String),
              "sourceLines"
                .= [ object
                       [ "line" .= (2 :: Int),
                         "text" .= ("x = selected + problem" :: String)
                       ]
                   ]
            ],
          object
            [ "module" .= ("Other" :: String),
              "span"
                .= object
                  [ "file" .= ("src/Other.hs" :: String),
                    "startLine" .= (4 :: Int),
                    "startColumn" .= (5 :: Int),
                    "endLine" .= (4 :: Int),
                    "endColumn" .= (9 :: Int)
                  ],
              "severity" .= ("error" :: String),
              "message" .= ("other module failed" :: String),
              "sourceLines"
                .= [ object
                       [ "line" .= (4 :: Int),
                         "text" .= ("y = other" :: String)
                       ]
                   ]
            ]
        ]
      )
    ]

test_failedRootDoesNotInstallDependencies :: Assertion
test_failedRootDoesNotInstallDependencies =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        depRoot = root </> "dep"
        storeRoot = root </> "store"
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" ["dep >=1 && <2"] "module Demo where\nimport Missing.Module\nx = 1\n"
    createFixturePackage depRoot "dep" "1.0.0" "Dep" []
    createDirectoryIfMissing True storeRoot
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    dependencyPlan <- findPlanByName "dep" (planDependencyPlans plan)

    _ <- expectInstallFailure (writeInstallScaffold plan)
    assertFileDoesNotExist (planStorePath plan </> "manifest.json")
    assertFileDoesNotExist (planStorePath dependencyPlan </> "manifest.json")

test_typeChecksReferencesToDependencyBindings :: Assertion
test_typeChecksReferencesToDependencyBindings =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        depRoot = root </> "dep"
        storeRoot = root </> "store"
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" ["dep >=1 && <2"] "module Demo where\nimport Dep\ny = depId ()\n"
    createFixturePackageWithSource depRoot "dep" "1.0.0" "Dep" [] "module Dep where\ndepId :: a -> a\ndepId x = x\n"
    createDirectoryIfMissing True storeRoot
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")

    _ <- expectInstallSuccess (writeInstallScaffold plan)
    assertFileExists (planStorePath plan </> "manifest.json")

test_typeChecksWithDependencyTypeEnvironments :: Assertion
test_typeChecksWithDependencyTypeEnvironments =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        depRoot = root </> "dep"
        storeRoot = root </> "store"
        source =
          unlines
            [ "module Demo where",
              "import Dep",
              "use :: Box a -> Box a",
              "use value = mark value"
            ]
        dependencySource =
          unlines
            [ "module Dep where",
              "data Box a = Box a",
              "class Marker a where",
              "  mark :: a -> a",
              "instance Marker (Box a) where",
              "  mark value = value"
            ]
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" ["dep >=1 && <2"] source
    createFixturePackageWithSource depRoot "dep" "1.0.0" "Dep" [] dependencySource
    createDirectoryIfMissing True storeRoot
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")

    _ <- expectInstallSuccess (writeInstallScaffold plan)
    assertFileExists (planStorePath plan </> "manifest.json")

test_checksCastStyleDependencyId :: Assertion
test_checksCastStyleDependencyId =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        depRoot = root </> "dep"
        storeRoot = root </> "store"
        source =
          unlines
            [ "{-# LANGUAGE FlexibleInstances #-}",
              "{-# LANGUAGE MultiParamTypeClasses #-}",
              "{-# LANGUAGE NoImplicitPrelude #-}",
              "module Demo where",
              "import Dep (id)",
              "class Cast a b where",
              "  cast :: a -> b",
              "instance Cast a a where",
              "  cast = id"
            ]
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" ["dep >=1 && <2"] source
    createFixturePackageWithSource depRoot "dep" "1.0.0" "Dep" [] "module Dep where\nid :: a -> a\nid x = x\n"
    createDirectoryIfMissing True storeRoot
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")

    result <- expectInstallSuccess (writeInstallScaffold plan)
    fcJson <- BL8.readFile (resultFcPath result)
    let renderedFc = BL8.unpack fcJson
    assertBool "FC artifact constructs an ordinary Cast dictionary" ("$Dict$Cast" `isInfixOf` renderedFc)
    assertBool
      ("FC artifact declares imported Dep.id once:\n" <> renderedFc)
      ( "external \\\"dep" `isInfixOf` renderedFc
          && length (filter (isPrefixOf "Dep.id :") (tails renderedFc)) == 1
      )

test_checksConstraintKindedMultiParameterClasses :: Assertion
test_checksConstraintKindedMultiParameterClasses =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
        source =
          unlines
            [ "{-# LANGUAGE ConstraintKinds #-}",
              "{-# LANGUAGE MultiParamTypeClasses #-}",
              "{-# LANGUAGE NoImplicitPrelude #-}",
              "{-# LANGUAGE UndecidableSuperClasses #-}",
              "module Demo where",
              "class Eq a",
              "class c t => Lawful c t",
              "data Bool = False | True",
              "instance Eq Bool",
              "instance Lawful Eq Bool"
            ]
    createFixturePackageWithSource sourceRoot "demo" "0.1.0.0" "Demo" [] source
    createDirectoryIfMissing True storeRoot
    plan <- buildPackagePlanWithResolver (fixtureDependencyResolver sourceRoot []) storeRoot (PackageSpec "demo" "0.1.0.0")

    _ <- expectInstallSuccess (writeInstallScaffold plan)
    pure ()

test_checkPackagePlanWritesNoArtifacts :: Assertion
test_checkPackagePlanWritesNoArtifacts =
  withDependencyFixture $ \sourceRoot depRoot _ storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    _ <- expectInstallSuccess (checkPackagePlan plan)
    mapM_ (assertFileDoesNotExist . (</> "manifest.json") . planStorePath) (flattenPackagePlans plan)

test_usesLocalProviderForGhcPrim :: Assertion
test_usesLocalProviderForGhcPrim =
  withCoreDependencyFixture "ghc-prim" $ \sourceRoot storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    assertDependencySpec plan (PackageSpec "aihc-prim" "0.13.0")

test_usesLocalProviderForBase :: Assertion
test_usesLocalProviderForBase =
  withCoreDependencyFixture "base" $ \sourceRoot storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    assertDependencySpec plan (PackageSpec "aihc-base" "4.21.2.0")

test_usesLocalProviderForGhcInternal :: Assertion
test_usesLocalProviderForGhcInternal =
  withCoreDependencyFixture "ghc-internal" $ \sourceRoot storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    assertDependencySpec plan (PackageSpec "aihc-internal" "9.1204.0")

test_usesVirtualProviderForSystemCxxStdLib :: Assertion
test_usesVirtualProviderForSystemCxxStdLib =
  withCoreDependencyFixture "system-cxx-std-lib" $ \sourceRoot storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    assertDependencySpec plan (PackageSpec "system-cxx-std-lib" "1.0")
    dependencyPlan <- findPlanByName "system-cxx-std-lib" (planDependencyPlans plan)
    assertEqual "virtual package source file count" 0 (planSourceFileCount dependencyPlan)
    _ <- expectInstallSuccess (checkPackagePlan plan)
    pure ()

test_manifestRecordsCoreProviderDependencyNames :: Assertion
test_manifestRecordsCoreProviderDependencyNames =
  withCoreDependencyFixture "ghc-prim" $ \sourceRoot storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    result <- expectInstallSuccess (writeInstallScaffold plan)
    manifest <- BL8.readFile (resultManifestPath result)
    let renderedManifest = BL8.unpack manifest
    assertBool "manifest records provider dependency name" ("aihc-prim" `isInfixOf` renderedManifest)
    assertBool "manifest does not retain upstream boot package name" (not ("ghc-prim" `isInfixOf` renderedManifest))

test_installsResolvedBaseDependencyClosure :: Assertion
test_installsResolvedBaseDependencyClosure =
  withCoreDependencyFixture "base" $ \sourceRoot storeRoot -> do
    plan <-
      buildPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    _ <- expectInstallSuccess (writeInstallScaffold plan)
    let plans = flattenPackagePlans plan
        installedNames = sort [pkgName (packageKeySpec (planPackageKey item)) | item <- plans]
    assertEqual "installed packages" ["aihc-base", "demo"] installedNames
    mapM_ (assertFileExists . (</> "manifest.json") . planStorePath) plans

test_dryRunWritesNoScaffoldArtifacts :: Assertion
test_dryRunWritesNoScaffoldArtifacts =
  withDependencyFixture $ \sourceRoot depRoot _ storeRoot -> do
    plan <-
      buildDryRunPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [("dep", "1.0.0", depRoot)])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")
    result <- dryRunInstallScaffold plan

    assertEqual "store path" (planStorePath plan) (resultStorePath result)
    assertFileDoesNotExist (resultManifestPath result)
    assertFileDoesNotExist (resultInterfacePath result)
    assertFileDoesNotExist (resultFcPath result)
    storePathExists <- doesDirectoryExist (resultStorePath result)
    assertBool ("expected dry-run store path not to exist: " <> resultStorePath result) (not storePathExists)

test_dryRunPlannerDoesNotGenerateSourceFiles :: Assertion
test_dryRunPlannerDoesNotGenerateSourceFiles =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
        autogenDir = sourceRoot </> ".aihc-autogen"
    createFixturePackageWithOtherModules sourceRoot "demo" "0.1.0.0" "Demo" ["Paths_demo"] []
    createDirectoryIfMissing True storeRoot

    _ <-
      buildDryRunPackagePlanWithResolver
        (fixtureDependencyResolver sourceRoot [])
        storeRoot
        (PackageSpec "demo" "0.1.0.0")

    autogenExists <- doesDirectoryExist autogenDir
    assertBool ("expected dry-run planner not to create " <> autogenDir) (not autogenExists)

test_compileDefaultEnvironment :: Assertion
test_compileDefaultEnvironment =
  withTempDir "aihc-compile-environment" $ \root -> do
    let workingDirectory = root </> "project"
        cacheHome = root </> "cache"
    createDirectoryIfMissing True workingDirectory
    bracket
      (lookupEnv "XDG_CACHE_HOME")
      restoreCacheHome
      ( \_ -> do
          setEnv "XDG_CACHE_HOME" cacheHome
          withCurrentDirectory workingDirectory $ do
            environment <- defaultCompileEnvironment
            assertEqual "installed toolchain store" (cacheHome </> "aihc" </> "store") (compileInstalledStoreRoot environment)
      )
  where
    restoreCacheHome Nothing = unsetEnv "XDG_CACHE_HOME"
    restoreCacheHome (Just value) = setEnv "XDG_CACHE_HOME" value

test_runtimePrimitiveValidation :: Assertion
test_runtimePrimitiveValidation = do
  let valueTy = TcTyCon (TyCon "Value" 0) []
      intTy = TcTyCon (TyCon "Int#" 0) []
      kept = Var "kept#" (Unique 1) (TcFunTy intTy valueTy)
      unsafeCoerce = Var "unsafeCoerce#" (Unique 2) (TcFunTy valueTy valueTy)
      mainVar = Var "main" (Unique 3) valueTy
      core =
        FcProgram
          (FcModuleId "test" "Test")
          [ FcPrimitive kept 1,
            FcPrimitive unsafeCoerce 1,
            FcTopBind (FcNonRec mainVar (FcApp (FcVar unsafeCoerce) (FcApp (FcVar kept) (FcLit (LitInt IntRep 1) intTy))))
          ]
      grin = Grin.lowerProgram core
      reachable = reachableRuntimePrimitiveNames "main" (extractReachabilityInterface core) [grin]
  assertEqual "runtime primitives" (Set.singleton "kept#") reachable

test_installedPackage :: Assertion
test_installedPackage =
  withTempDir "aihc-installed-package" $ \root -> do
    let sourceRoot = root </> "prim-fixture"
        primSource = sourceRoot </> "src" </> "GHC" </> "Prim.hs"
        internalSource = sourceRoot </> "src" </> "GHC" </> "Prim" </> "Internal.hs"
        topHandlerSource = sourceRoot </> "src" </> "GHC" </> "TopHandler.hs"
        cabalFile = sourceRoot </> "prim-fixture.cabal"
        storeRoot = root </> "store"
        environment = CompileEnvironment storeRoot
        mainSource =
          T.unlines
            [ "{-# LANGUAGE MagicHash #-}",
              "{-# LANGUAGE NoImplicitPrelude #-}",
              "module Main where",
              "import qualified GHC.Prim as Prim",
              "data Local = Local",
              "identity value = value",
              "main = Prim.IO (Prim.identity Local)"
            ]
        privateMainSource =
          T.unlines
            [ "{-# LANGUAGE NoImplicitPrelude #-}",
              "module Main where",
              "import GHC.Prim.Internal",
              "main = Unit"
            ]
    createDirectoryIfMissing True (takeDirectory primSource)
    writeFile
      cabalFile
      ( unlines
          [ "cabal-version: 3.8",
            "name: prim-fixture",
            "version: 0.1.0.0",
            "build-type: Simple",
            "library",
            "  exposed-modules: GHC.Prim, GHC.TopHandler",
            "  other-modules: GHC.Prim.Internal",
            "  hs-source-dirs: src",
            "  default-extensions: NoImplicitPrelude",
            "  default-language: GHC2021"
          ]
      )
    writeFile
      primSource
      ( unlines
          [ "{-# LANGUAGE MagicHash #-}",
            "{-# LANGUAGE NoImplicitPrelude #-}",
            "module GHC.Prim (Unit(..), IO(..), identity) where",
            "import GHC.Prim.Internal (Unit(..), IO(..))",
            "identity value = value"
          ]
      )
    createDirectoryIfMissing True (takeDirectory internalSource)
    writeFile
      internalSource
      ( unlines
          [ "{-# LANGUAGE NoImplicitPrelude #-}",
            "module GHC.Prim.Internal where",
            "data Unit = Unit",
            "data IO a = IO a"
          ]
      )
    writeFile
      topHandlerSource
      ( unlines
          [ "{-# LANGUAGE NoImplicitPrelude #-}",
            "module GHC.TopHandler (runMainIO) where",
            "import GHC.Prim.Internal (IO)",
            "runMainIO :: IO a -> IO a",
            "runMainIO action = action"
          ]
      )
    plan <- buildPackagePlanFromSource storeRoot (PackageSpec "prim-fixture" "0.1.0.0") sourceRoot
    installResult <- installPackageLibraries [Llvm] False False plan
    case installResult of
      Left err -> assertFailure err
      Right _ -> pure ()
    removeDirectoryRecursive sourceRoot
    compileResult <- compileSourceToAssemblyWithDependenciesFor Llvm environment "Main.hs" mainSource
    case compileResult of
      Left err -> assertFailure (show err)
      Right assembly -> do
        let identitySymbol = T.intercalate "_" (packageVariantLibraryId (planPackageKey plan) <> ["GHC", "Prim", "identity"])
        assertBool "expected installed dependency initializer" ("_aihc_init_" `T.isInfixOf` assembly)
        assertBool ("expected readable dependency symbol " <> T.unpack identitySymbol) (identitySymbol `T.isInfixOf` assembly)
    privateCompileResult <- compileSourceToAssemblyWithDependenciesFor Llvm environment "PrivateMain.hs" privateMainSource
    case privateCompileResult of
      Left err -> assertBool ("expected private module error, got: " <> show err) ("GHC.Prim.Internal" `isInfixOf` show err)
      Right _ -> assertFailure "expected private other-module import to be rejected"
    wholeResult <- compileSourceToWholeCoreWithDependencies environment "Main.hs" mainSource
    case wholeResult of
      Left err -> assertFailure (show err)
      Right core -> do
        assertBool "expected qualified generated entry point" ("$g$exe$Main$_24_aihc_2e_main" `T.isInfixOf` core)
        assertBool "expected executable source binding" ("$g$exe$Main$main" `T.isInfixOf` core)
        assertBool "expected no unnamed source binding" (not ("$g$main$Main$main" `T.isInfixOf` core))
        assertBool "expected top-level handler call" ("runMainIO" `T.isInfixOf` core)
        assertBool "expected installed dependency body" ("main" `T.isInfixOf` core)
        assertBool ("expected no external references in:\n" <> T.unpack core) (not ("external " `T.isInfixOf` core))

test_preparedRuntime :: Assertion
test_preparedRuntime =
  withTempDir "aihc-prepared-runtime" $ \root -> do
    archive <- prepareRuntimeArchive root Llvm GcCalloc
    assertFileExists archive
-}

assertFileExists :: FilePath -> Assertion
assertFileExists path = do
  exists <- doesFileExist path
  assertBool ("expected file to exist: " <> path) exists

{- Disabled with the related test groups in main.
assertFileDoesNotExist :: FilePath -> Assertion
assertFileDoesNotExist path = do
  exists <- doesFileExist path
  assertBool ("expected file not to exist: " <> path) (not exists)

expectInstallSuccess :: IO (Either InstallFailure a) -> IO a
expectInstallSuccess action = do
  result <- action
  case result of
    Right value -> pure value
    Left failure -> assertFailure ("expected install success, got: " <> renderInstallFailure failure)

expectInstallFailure :: IO (Either InstallFailure a) -> IO InstallFailure
expectInstallFailure action = do
  result <- action
  case result of
    Left failure -> pure failure
    Right _ -> assertFailure "expected install failure"

withFixturePackage :: (FilePath -> FilePath -> IO a) -> IO a
withFixturePackage action =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
        srcDir = sourceRoot </> "src"
    createDirectoryIfMissing True srcDir
    createDirectoryIfMissing True storeRoot
    writeFile (sourceRoot </> "demo.cabal") demoCabal
    writeFile (sourceRoot </> "Setup.hs") "import Distribution.Simple\nmain = defaultMain\n"
    writeFile (srcDir </> "Demo.hs") ("module Demo where\nx = ()\n" <> fixtureTupleDeclaration)
    action sourceRoot storeRoot

withDependencyFixture :: (FilePath -> FilePath -> FilePath -> FilePath -> IO a) -> IO a
withDependencyFixture action =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        depV1Root = root </> "dep-v1"
        depV2Root = root </> "dep-v2"
        storeRoot = root </> "store"
    createFixturePackage sourceRoot "demo" "0.1.0.0" "Demo" ["dep >=1 && <3"]
    createFixturePackage depV1Root "dep" "1.0.0" "Dep" []
    createFixturePackage depV2Root "dep" "2.0.0" "Dep" []
    createDirectoryIfMissing True storeRoot
    action sourceRoot depV1Root depV2Root storeRoot

withCoreDependencyFixture :: String -> (FilePath -> FilePath -> IO a) -> IO a
withCoreDependencyFixture dependencyName action =
  withTempDir "aihc-cli" $ \root -> do
    let sourceRoot = root </> "source"
        storeRoot = root </> "store"
        coreLibsRoot = root
    createFixturePackage sourceRoot "demo" "0.1.0.0" "Demo" [dependencyName]
    createCoreProviderFixtures coreLibsRoot
    createDirectoryIfMissing True storeRoot
    withCoreLibsRoot coreLibsRoot (action sourceRoot storeRoot)

withCoreLibsRoot :: FilePath -> IO a -> IO a
withCoreLibsRoot root action =
  bracket
    (lookupEnv "AIHC_CORE_LIBS_ROOT")
    restoreEnv
    (\_ -> setEnv "AIHC_CORE_LIBS_ROOT" root *> action)
  where
    restoreEnv Nothing = unsetEnv "AIHC_CORE_LIBS_ROOT"
    restoreEnv (Just value) = setEnv "AIHC_CORE_LIBS_ROOT" value

assertDependencySpec :: PackagePlan -> PackageSpec -> Assertion
assertDependencySpec plan expected =
  assertBool ("expected dependency " <> show expected <> " in " <> show dependencySpecs) (expected `elem` dependencySpecs)
  where
    dependencySpecs = map resolvedDependencySpec (packageKeyDependencies (planPackageKey plan))

findPlanByName :: String -> [PackagePlan] -> IO PackagePlan
findPlanByName name plans =
  case [plan | plan <- plans, pkgName (packageKeySpec (planPackageKey plan)) == name] of
    plan : _ -> pure plan
    [] -> assertFailure ("missing dependency plan for " <> name)

flattenPackagePlans :: PackagePlan -> [PackagePlan]
flattenPackagePlans plan =
  concatMap flattenPackagePlans (planDependencyPlans plan) <> [plan]

fixtureDependencyResolver :: FilePath -> [(String, String, FilePath)] -> DependencyResolver
fixtureDependencyResolver sourceRoot dependencies =
  DependencyResolver
    { resolverResolveVersion = \name ->
        case [version | (dependencyName, version, _) <- dependencies, dependencyName == name] of
          version : _ -> pure version
          [] -> fail ("missing fixture dependency version for " <> name),
      resolverSourcePath = \spec ->
        if pkgName spec == "demo"
          then pure sourceRoot
          else case [path | (dependencyName, version, path) <- dependencies, dependencyName == pkgName spec, version == pkgVersion spec] of
            path : _ -> pure path
            [] -> fail ("missing fixture source for " <> show spec)
    }

createFixturePackage :: FilePath -> String -> String -> String -> [String] -> IO ()
createFixturePackage sourceRoot name version moduleName dependencies = do
  createFixturePackageWithOtherModules sourceRoot name version moduleName [] dependencies

createFixturePackageWithOtherModules :: FilePath -> String -> String -> String -> [String] -> [String] -> IO ()
createFixturePackageWithOtherModules sourceRoot name version moduleName otherModules dependencies = do
  let srcDir = sourceRoot </> "src"
  createDirectoryIfMissing True srcDir
  writeFile (sourceRoot </> name <> ".cabal") (fixtureCabal name version moduleName otherModules dependencies)
  writeFile (sourceRoot </> "Setup.hs") "import Distribution.Simple\nmain = defaultMain\n"
  writeFile (srcDir </> moduleName <> ".hs") ("module " <> moduleName <> " where\nx = ()\n" <> fixtureTupleDeclaration)

createFixturePackageWithSource :: FilePath -> String -> String -> String -> [String] -> String -> IO ()
createFixturePackageWithSource sourceRoot name version moduleName dependencies source = do
  let srcDir = sourceRoot </> "src"
  createDirectoryIfMissing True srcDir
  writeFile (sourceRoot </> name <> ".cabal") (fixtureCabal name version moduleName [] dependencies)
  writeFile (sourceRoot </> "Setup.hs") "import Distribution.Simple\nmain = defaultMain\n"
  writeFile (srcDir </> moduleName <> ".hs") (source <> fixtureTupleDeclaration)

-- Installer fixtures intentionally have no dependency on the core libraries.
-- Supply the primitive nullary tuple declaration in source so they exercise
-- the same ordinary-constructor path as packages which depend on aihc-prim.
fixtureTupleDeclaration :: String
fixtureTupleDeclaration = "\ndata FixtureUnit = ()\n"

createCoreProviderFixtures :: FilePath -> IO ()
createCoreProviderFixtures root = do
  createCoreProviderPackage
    (root </> "core-libs" </> "aihc-base")
    "aihc-base"
    "4.21.2.0"
    "Prelude"
  createCoreProviderPackage
    (root </> "core-libs" </> "aihc-prim")
    "aihc-prim"
    "0.13.0"
    "GHC.Prim"
  createCoreProviderPackage
    (root </> "core-libs" </> "aihc-internal")
    "aihc-internal"
    "9.1204.0"
    "GHC.Internal.Base"
  createVirtualCoreProviderPackage
    (root </> "core-libs" </> "system-cxx-std-lib")
    "system-cxx-std-lib"
    "1.0"

createCoreProviderPackage :: FilePath -> String -> String -> String -> IO ()
createCoreProviderPackage sourceRoot name version moduleName = do
  let srcDir = sourceRoot </> "src"
      modulePath = srcDir </> map dotToSlash moduleName <> ".hs"
  createDirectoryIfMissing True (takeDirectory modulePath)
  writeFile (sourceRoot </> name <> ".cabal") (fixtureCabal name version moduleName [] [])
  writeFile modulePath ("{-# LANGUAGE NoImplicitPrelude #-}\nmodule " <> moduleName <> " () where\n")
  where
    dotToSlash '.' = '/'
    dotToSlash c = c

createVirtualCoreProviderPackage :: FilePath -> String -> String -> IO ()
createVirtualCoreProviderPackage sourceRoot name version = do
  createDirectoryIfMissing True sourceRoot
  writeFile
    (sourceRoot </> name <> ".cabal")
    ( unlines
        [ "cabal-version: 3.8",
          "name: " <> name,
          "version: " <> version,
          "build-type: Simple",
          "license: NONE",
          "",
          "library",
          "  default-language: GHC2021"
        ]
    )

fixtureCabal :: String -> String -> String -> [String] -> [String] -> String
fixtureCabal name version moduleName otherModules dependencies =
  unlines $
    [ "cabal-version: 3.0",
      "name: " <> name,
      "version: " <> version,
      "",
      "library",
      "  exposed-modules: " <> moduleName,
      "  hs-source-dirs: src"
    ]
      <> ["  other-modules: " <> unwords otherModules | not (null otherModules)]
      <> ["  build-depends: " <> dependency | dependency <- dependencies]
      <> ["  default-language: Haskell2010"]

demoCabal :: String
demoCabal =
  unlines
    [ "cabal-version: 3.0",
      "name: demo",
      "version: 0.1.0.0",
      "",
      "library",
      "  exposed-modules: Demo",
      "  hs-source-dirs: src",
      "  build-depends: base >=4 && <5",
      "  default-language: Haskell2010"
    ]

libraryOnlyInstallCabal :: String
libraryOnlyInstallCabal =
  unlines
    [ "cabal-version: 3.0",
      "name: demo",
      "version: 0.1.0.0",
      "",
      "library demo-internal",
      "  exposed-modules: Demo.Internal",
      "  hs-source-dirs: internal",
      "  default-language: Haskell2010",
      "",
      "library",
      "  exposed-modules: Demo",
      "  other-modules: Demo.Private",
      "  hs-source-dirs: src",
      "  build-depends: demo-internal, dep",
      "  default-language: Haskell2010",
      "",
      "executable demo-cli",
      "  main-is: Main.hs",
      "  hs-source-dirs: app",
      "  build-depends: demo, executable-only-dependency",
      "  default-language: Haskell2010"
    ]
-}

withTempDir :: String -> (FilePath -> IO a) -> IO a
withTempDir prefix action = do
  tempRoot <- getTemporaryDirectory
  (tempFile, tempHandle) <- openTempFile tempRoot (prefix <> "-XXXXXX")
  hClose tempHandle
  removeFile tempFile
  createDirectory tempFile
  bracket
    (pure tempFile)
    removeDirectoryRecursive
    action

{- Disabled with the related test groups in main.
test_missingWasm32ClangTarget :: Assertion
test_missingWasm32ClangTarget = do
  withFakeClang "  arm64 - AArch64\n  x86-64 - 64-bit X86\n" $ \clang -> do
    (exitCode, _stdout, stderr) <- readWasmClangProcessWithExitCode clang ["--target=wasm32-unknown-unknown"]
    assertEqual "Clang failure" (ExitFailure 1) exitCode
    assertBool "original Clang error" ("original Clang failure" `isInfixOf` stderr)
    assertBool "missing target notice" ("does not include the wasm32 target" `isInfixOf` stderr)
    assertBool "Homebrew advice" ("brew install llvm" `isInfixOf` stderr)
    assertBool "Nix advice" ("nix shell nixpkgs#llvmPackages.clang-unwrapped" `isInfixOf` stderr)
  withFakeClang "  wasm32 - WebAssembly 32-bit\n" $ \clang -> do
    (exitCode, _stdout, stderr) <- readWasmClangProcessWithExitCode clang ["--target=wasm32-unknown-unknown"]
    assertEqual "Clang failure" (ExitFailure 1) exitCode
    assertEqual "supported target error" "original Clang failure\n" stderr

withFakeClang :: String -> (FilePath -> IO a) -> IO a
withFakeClang targets action =
  withTempDir "aihc-fake-clang" $ \directory -> do
    let clang = directory </> "clang"
    writeFile
      clang
      ( unlines
          [ "#!/bin/sh",
            "if [ \"$1\" = \"-print-targets\" ]; then",
            "  printf '%s' '" <> targets <> "'",
            "  exit 0",
            "fi",
            "printf '%s\\n' 'original Clang failure' >&2",
            "exit 1"
          ]
      )
    permissions <- getPermissions clang
    setPermissions clang permissions {executable = True}
    action clang
-}
