{-# LANGUAGE OverloadedStrings #-}

module Ormolu.Fixity.ParserSpec (spec) where

import Data.List.NonEmpty (NonEmpty (..))
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Ormolu.Fixity
import Ormolu.Fixity.Parser
import Test.Hspec
import Test.Hspec.Megaparsec
import Text.Megaparsec.Error (ErrorFancy (..))

spec :: Spec
spec = do
  describe "parseDotOrmolu" $ do
    it "parses the empty input without choking" $
      parseDotOrmolu "" ""
        `shouldParse` (FixityOverrides Map.empty, ModuleReexports Map.empty)
    it "parses a collection of fixity declarations" $
      -- The example is taken from base.
      parseDotOrmolu
        ""
        ( T.unlines
            [ "infixr 9  .",
              "infixr 5  ++",
              "infixl 4  <$",
              "infixl 1  >>, >>=",
              "infixr 1  =<<",
              "infixr 0  $, $!",
              "infixl 4 <*>, <*, *>, <**>"
            ]
        )
        `shouldParse` ( exampleFixityOverrides,
                        ModuleReexports Map.empty
                      )
    it "accepts fractional operator precedences" $
      parseDotOrmolu
        ""
        ( T.unlines
            [ "infixr 3 >~<",
              "infixr 3.3 |~|",
              "infixr 3.7 <~>"
            ]
        )
        `shouldParse` ( fractionalFixityOverrides,
                        ModuleReexports Map.empty
                      )
    it "combines conflicting fixity declarations correctly" $
      parseDotOrmolu
        ""
        ( T.unlines
            [ "infixr 9 ., ^",
              "infixr 7 ., $",
              "infixr 9 ^ ",
              "infixl 7 $"
            ]
        )
        `shouldParse` ( FixityOverrides
                          ( Map.fromList
                              [ ("$", FixityInfo InfixL 7),
                                (".", FixityInfo InfixR 7),
                                ("^", FixityInfo InfixR 9)
                              ]
                          ),
                        ModuleReexports Map.empty
                      )
    it "handles CRLF line endings correctly" $
      parseDotOrmolu ""
        `shouldSucceedOn` unlinesCrlf
          [ "infixr 9  .",
            "infixr 5  ++"
          ]
    it "parses inputs without a trailing newline" $
      parseDotOrmolu ""
        `shouldSucceedOn` T.intercalate
          "\n"
          [ "infixr 9  .",
            "infixr 5  ++"
          ]
    it "fails with correct parse error (keyword wrong second line)" $
      parseDotOrmolu "" "infixr 5 .\nfoobar 5 $"
        `shouldFailWith` err
          11
          ( mconcat
              [ utok 'f',
                etoks "infix",
                etoks "infixl",
                etoks "infixr",
                etoks "module",
                eeof
              ]
          )
    it "parses module re-exports and combines them correctly" $
      parseDotOrmolu
        ""
        ( T.unlines
            [ "module Control.Lens exports Control.Lens.Lens",
              "module Control.Lens exports \"lens\" Control.Lens.At",
              "module Text.Megaparsec exports Control.Monad.Combinators"
            ]
        )
        `shouldParse` (FixityOverrides Map.empty, exampleModuleReexports)
    it "parses fixity declarations + module re-export declarations with blanks" $
      parseDotOrmolu
        ""
        ( T.unlines
            [ "module Control.Lens exports Control.Lens.Lens",
              "",
              "infixr 5  ++",
              "infixl 4  <$",
              "",
              "",
              "module Control.Lens exports \"lens\" Control.Lens.At",
              "infixr 9  .",
              "module Text.Megaparsec exports Control.Monad.Combinators",
              "infixl 1  >>, >>=",
              "infixr 1  =<<",
              "",
              "infixr 0  $, $!",
              "infixl 4 <*>, <*, *>, <**>"
            ]
        )
        `shouldParse` (exampleFixityOverrides, exampleModuleReexports)
  describe "parseFixtiyDeclaration" $ do
    it "parses a simple infixr declaration" $
      parseFixityDeclaration "infixr 5 $"
        `shouldParse` [("$", FixityInfo InfixR 5)]
    it "parses a simple infixl declaration" $
      parseFixityDeclaration "infixl 5 $"
        `shouldParse` [("$", FixityInfo InfixL 5)]
    it "parses a simple infix declaration" $
      parseFixityDeclaration "infix 5 $"
        `shouldParse` [("$", FixityInfo InfixN 5)]
    it "parses a declaration for a ticked identifier" $
      parseFixityDeclaration "infixl 5 `foo`"
        `shouldParse` [("foo", FixityInfo InfixL 5)]
    it "parses a declaration for a ticked identifier (constructor case)" $
      parseFixityDeclaration "infixl 5 `Foo`"
        `shouldParse` [("Foo", FixityInfo InfixL 5)]
    it "parses a multi-operator declaration" $
      parseFixityDeclaration "infixl 5 $, ., `Foo`, `bar`"
        `shouldParse` [ ("$", FixityInfo InfixL 5),
                        (".", FixityInfo InfixL 5),
                        ("Foo", FixityInfo InfixL 5),
                        ("bar", FixityInfo InfixL 5)
                      ]
    it "parses a declaration with a unicode operator" $
      parseFixityDeclaration "infixr 5 ×"
        `shouldParse` [("×", FixityInfo InfixR 5)]
    it "fails with correct parse error (keyword wrong)" $
      parseFixityDeclaration "foobar 5 $"
        `shouldFailWith` err
          0
          ( mconcat
              [ utoks "foobar",
                etoks "infix",
                etoks "infixl",
                etoks "infixr"
              ]
          )
    it "fails with correct parse error (missing operator)" $
      parseFixityDeclaration "infixr 5 "
        `shouldFailWith` err
          9
          ( mconcat
              [ ueof,
                etok '`',
                elabel "operator character"
              ]
          )
    it "fails with correct parse error (trailing comma)" $
      parseFixityDeclaration "infixr 5 ., "
        `shouldFailWith` err
          12
          ( mconcat
              [ ueof,
                etok '`',
                elabel "operator character"
              ]
          )
    it "fails with correct parse error (precedence greater than 9)" $
      parseFixityDeclaration "infixl 10 $"
        `shouldFailWith` errFancy
          7
          (fancy (ErrorFail "precedence should not be greater than 9"))
  describe "parseModuleReexportDeclaration" $ do
    it "parses a re-export declaration" $
      parseModuleReexportDeclaration "module Control.Lens exports Control.Lens.Lens"
        `shouldParse` ( "Control.Lens",
                        (Nothing, "Control.Lens.Lens") :| []
                      )
    it "parses a re-export declaration (explicit package)" $
      parseModuleReexportDeclaration "module Control.Lens exports \"lens\" Control.Lens.Lens"
        `shouldParse` ( "Control.Lens",
                        (Just "lens", "Control.Lens.Lens") :| []
                      )
    it "fails with correct parse error (keyword wrong)" $
      parseModuleReexportDeclaration "foo Control.Lens exports Control.Lens.Lens"
        `shouldFailWith` err
          0
          ( mconcat
              [ utoks "foo Co",
                etoks "module"
              ]
          )
    it "fails with correct parse error (module syntax)" $
      parseModuleReexportDeclaration "module control.Lens exports Control.Lens.Lens"
        `shouldFailWith` err
          7
          ( mconcat
              [ utok 'c',
                elabel "module name"
              ]
          )
    it "fails with correct parse error (typo: export intead exports)" $
      parseModuleReexportDeclaration "module Control.Lens export Control.Lens.Lens"
        `shouldFailWith` err
          20
          ( mconcat
              [ utoks "export ",
                etoks "exports"
              ]
          )

exampleFixityOverrides :: FixityOverrides
exampleFixityOverrides =
  FixityOverrides
    ( Map.fromList
        [ ("$", FixityInfo InfixR 0),
          ("$!", FixityInfo InfixR 0),
          ("*>", FixityInfo InfixL 4),
          ("++", FixityInfo InfixR 5),
          (".", FixityInfo InfixR 9),
          ("<$", FixityInfo InfixL 4),
          ("<*", FixityInfo InfixL 4),
          ("<**>", FixityInfo InfixL 4),
          ("<*>", FixityInfo InfixL 4),
          ("=<<", FixityInfo InfixR 1),
          (">>", FixityInfo InfixL 1),
          (">>=", FixityInfo InfixL 1)
        ]
    )

fractionalFixityOverrides :: FixityOverrides
fractionalFixityOverrides =
  FixityOverrides
    ( Map.fromList
        [ (">~<", FixityInfo InfixR 3),
          ("|~|", FixityInfo InfixR 3.3),
          ("<~>", FixityInfo InfixR 3.7)
        ]
    )

exampleModuleReexports :: ModuleReexports
exampleModuleReexports =
  ModuleReexports . Map.fromList $
    [ ( "Control.Lens",
        (Nothing, "Control.Lens.Lens") :| [(Just "lens", "Control.Lens.At")]
      ),
      ( "Text.Megaparsec",
        (Nothing, "Control.Monad.Combinators") :| []
      )
    ]

unlinesCrlf :: [Text] -> Text
unlinesCrlf = T.concat . fmap (<> "\r\n")
