{-# LANGUAGE NoImplicitPrelude   #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE FlexibleInstances   #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE QuasiQuotes         #-}
{-# LANGUAGE RecordWildCards     #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Pantry.TypesSpec
    ( spec
    ) where

import           Data.Aeson.WarningParser ( WithJSONWarnings (..) )
import qualified Data.ByteString.Char8 as S8
import qualified Data.Yaml as Yaml
import           Distribution.Types.PackageName ( mkPackageName )
import           Distribution.Types.Version ( mkVersion )
import           Hedgehog
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range
import           Pantry
import qualified Pantry.SHA256 as SHA256
import           Pantry.Types
                   ( Tree (..), TreeEntry (..), parseTree, renderTree )
import           RIO
import qualified RIO.Text as T
import           RIO.Time ( Day (..), fromGregorian )
import           Test.Hspec
import           Text.RawString.QQ

hh :: HasCallStack => String -> Property -> Spec
hh name p = it name $ do
  result <- check p
  unless result $ throwString "Hedgehog property failed" :: IO ()

genBlobKey :: Gen BlobKey
genBlobKey = BlobKey <$> genSha256 <*> (FileSize <$> Gen.word (Range.linear 1 10000))

genSha256 :: Gen SHA256
genSha256 = SHA256.hashBytes <$> Gen.bytes (Range.linear 1 500)

samplePLIRepo :: ByteString
samplePLIRepo =
    [r|
subdir: wai
cabal-file:
  # This is ignored, only included to make sure we get no warnings
  size: 1765
  sha256: eea52c4967d8609c2f79213d6dffe6d6601034f1471776208404781de7051410
name: wai
version: 3.2.1.2
git: https://github.com/yesodweb/wai.git
pantry-tree:
  size: 714
  sha256: ecfd0b4b75f435a3f362394807b35e5ef0647b1a25005d44a3632c49db4833d2
commit: d11d63f1a6a92db8c637a8d33e7953ce6194a3e0
|]

samplePLIRepo2 :: ByteString
samplePLIRepo2 =
    [r|
name: merkle-log
version: 0.1.0.0
git: https://github.com/kadena-io/merkle-log.git
pantry-tree:
  size: 615
  sha256: 5a99e5e41ccd675a7721a733714ba2096f4204d9010f867c5fb7095b78e2959d
commit: a7ae61d7082afe3aa1a0fd0546fc1351a2f7c376
|]

spec :: Spec
spec = do
  describe "WantedCompiler" $ do
    hh "parse/render works" $ property $ do
      wc <- forAll $
        let ghc = WCGhc <$> genVersion
            ghcjs = WCGhcjs <$> genVersion <*> genVersion
            genVersion = mkVersion <$> Gen.list (Range.linear 1 5) (Gen.int (Range.linear 0 100))
         in Gen.choice [ghc, ghcjs]
      let text = utf8BuilderToText $ display wc
      case parseWantedCompiler text of
        Left e -> throwIO e
        Right actual -> liftIO $ actual `shouldBe` wc

  describe "Tree" $ do
    hh "parse/render works" $ property $ do
      tree <- forAll $
        let sfp = do
              pieces <- Gen.list (Range.linear 1 10) sfpComponent
              let combined = T.intercalate "/" pieces
              case mkSafeFilePath combined of
                Nothing -> error $ "Incorrect SafeFilePath in test suite: " ++ show pieces
                Just sfp' -> pure sfp'
            sfpComponent = Gen.text (Range.linear 1 15) Gen.alphaNum
            entry = TreeEntry
              <$> genBlobKey
              <*> Gen.choice (map pure [minBound..maxBound])
         in TreeMap <$> Gen.map (Range.linear 1 20) ((,) <$> sfp <*> entry)
      let bs = renderTree tree
      liftIO $ parseTree bs `shouldBe` Just tree

  describe "(Raw)SnapshotLayer" $ do
    let parseSl :: String -> IO RawSnapshotLayer
        parseSl str = case Yaml.decodeThrow . S8.pack $ str of
          (Just (WithJSONWarnings x _)) -> resolvePaths Nothing x
          Nothing -> fail "Can't parse RawSnapshotLayer"

    it "parses snapshot using 'resolver'" $ do
      RawSnapshotLayer{..} <- parseSl $
        "name: 'test'\n" ++
        "resolver: lts-22.9\n"
      rslParent `shouldBe` RSLSynonym (LTS 22 9)

    it "parses snapshot using 'snapshot'" $ do
      RawSnapshotLayer{..} <- parseSl $
        "name: 'test'\n" ++
        "snapshot: lts-22.9\n"
      rslParent `shouldBe` RSLSynonym (LTS 22 9)

    it "throws if both 'resolver' and 'snapshot' are present" $ do
      let go = parseSl $
                "name: 'test'\n" ++
                "resolver: lts-22.9\n" ++
                "snapshot: lts-22.9\n"
      go `shouldThrow` anyException

    it "throws if both 'snapshot' and 'compiler' are not present" $ do
      let go = parseSl "name: 'test'\n"
      go `shouldThrow` anyException

    it "works if no 'snapshot' specified" $ do
      RawSnapshotLayer{..} <- parseSl $
        "name: 'test'\n" ++
        "compiler: ghc-9.6.4\n"
      rslParent `shouldBe` RSLCompiler (WCGhc (mkVersion [9, 6, 4]))

    hh "rendering the name of an LTS to JSON" $ property $ do
      (major, minor) <- forAll $ (,)
        <$> Gen.integral (Range.linear 1 10000)
        <*> Gen.integral (Range.linear 1 10000)
      liftIO $
        Yaml.toJSON (RSLSynonym $ LTS major minor) `shouldBe`
        Yaml.String (T.pack $ concat ["lts-", show major, ".", show minor])

    hh "rendering the name of a nightly to JSON" $ property $ do
      days <- forAll $ Gen.integral $ Range.linear 1 10000000
      let day = ModifiedJulianDay days
      liftIO $
        Yaml.toJSON (RSLSynonym $ Nightly day) `shouldBe`
        Yaml.String (T.pack $ "nightly-" ++ show day)
    it "FromJSON instance for PLIRepo" $ do
      WithJSONWarnings unresolvedPli warnings <- Yaml.decodeThrow samplePLIRepo
      warnings `shouldBe` []
      pli <- resolvePaths Nothing unresolvedPli
      let repoValue =
              Repo
                  { repoSubdir = "wai"
                  , repoType = RepoGit
                  , repoCommit =
                        "d11d63f1a6a92db8c637a8d33e7953ce6194a3e0"
                  , repoUrl = "https://github.com/yesodweb/wai.git"
                  }
          pantrySha =
              SHA256.fromHexBytes
                  "ecfd0b4b75f435a3f362394807b35e5ef0647b1a25005d44a3632c49db4833d2"
      psha <- case pantrySha of
        Right psha -> pure psha
        _ -> fail "Failed decoding sha256"
      let pkgValue =
              PackageMetadata
                  { pmIdent =
                        PackageIdentifier
                            (mkPackageName "wai")
                            (mkVersion [3, 2, 1, 2])
                  , pmTreeKey = TreeKey (BlobKey psha (FileSize 714))
                  }
      pli `shouldBe` PLIRepo repoValue pkgValue

      WithJSONWarnings reparsed warnings2 <- Yaml.decodeThrow $ Yaml.encode pli
      warnings2 `shouldBe` []
      reparsed' <- resolvePaths Nothing reparsed
      reparsed' `shouldBe` pli
    it "parseHackageText parses" $ do
      let txt =
              "persistent-2.8.2@sha256:df118e99f0c46715e932fe82d787fc09689d87898f3a8b13f5954d25af6b46a1,5058"
          hsha =
              SHA256.fromHexBytes
                  "df118e99f0c46715e932fe82d787fc09689d87898f3a8b13f5954d25af6b46a1"
      sha <- case hsha of
        Right sha' -> pure sha'
        _ -> fail "parseHackagetext: failed decoding the sha256"
      let Right (pkgIdentifier, blobKey) = parseHackageText txt
      blobKey `shouldBe` BlobKey sha (FileSize 5058)
      pkgIdentifier `shouldBe`
          PackageIdentifier
              (mkPackageName "persistent")
              (mkVersion [2, 8, 2])
    it "roundtripping a PLIRepo" $ do
      WithJSONWarnings unresolvedPli warnings <- Yaml.decodeThrow samplePLIRepo2
      warnings `shouldBe` []
      pli <- resolvePaths Nothing unresolvedPli
      WithJSONWarnings unresolvedPli2 warnings2 <- Yaml.decodeThrow $ Yaml.encode pli
      warnings2 `shouldBe` []
      pli2 <- resolvePaths Nothing unresolvedPli2
      pli2 `shouldBe` (pli :: PackageLocationImmutable)

  describe "completeSnapshotLocation" $ do
    let sameUrl (SLUrl txt _) (RSLUrl txt' _) txt'' =
          do
          txt `shouldBe` txt'
          txt `shouldBe` txt''
        sameUrl _ _ _ = liftIO $ error "Snapshot synonym did not complete as expected"

    it "default location for nightly-2024-02-04" $ do
      let sn = Nightly $ fromGregorian 2024 2 4
      loc <- runPantryAppClean $ completeSnapshotLocation $ RSLSynonym sn
      sameUrl loc (defaultSnapshotLocation sn)
        "https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2024/2/4.yaml"

    it "default location for lts-22.9" $ do
      let sn = LTS 22 9
      loc <- runPantryAppClean $ completeSnapshotLocation $ RSLSynonym sn
      sameUrl loc (defaultSnapshotLocation sn)
        "https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/9.yaml"
