{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}

module CheckSkeletonParser
  ( checkSkeletonParser,
    main,
  )
where

import Core.System
import Core.Text
import Technique.Language
import Technique.Parser
import Technique.Quantity
import Test.Hspec
import Text.Megaparsec hiding (Label)

main :: IO ()
main = do
  finally (hspec checkSkeletonParser) (putStrLn ".")

checkSkeletonParser :: Spec
checkSkeletonParser = do
  describe "Parse procfile header" $ do
    it "correctly parses a complete magic line" $ do
      parseMaybe pMagicLine "% technique v2\n" `shouldBe` Just 2
    it "errors if magic line has incorrect syntax" $ do
      parseMaybe pMagicLine "%\n" `shouldBe` Nothing
      parseMaybe pMagicLine "%technique\n" `shouldBe` Nothing
      parseMaybe pMagicLine "% technique\n" `shouldBe` Nothing
      parseMaybe pMagicLine "% technique \n" `shouldBe` Nothing
      parseMaybe pMagicLine "% technique v\n" `shouldBe` Nothing
      parseMaybe pMagicLine "% technique  v2\n" `shouldBe` Nothing
      parseMaybe pMagicLine "% technique v2 asdf\n" `shouldBe` Nothing

    it "correctly parses an SPDX header line" $ do
      parseMaybe pSpdxLine "! BSD-3-Clause\n" `shouldBe` Just ("BSD-3-Clause", Nothing)
    it "correctly parses an SPDX header line with Copyright" $ do
      parseMaybe pSpdxLine "! BSD-3-Clause; (c) 2019 Kermit le Frog\n" `shouldBe` Just ("BSD-3-Clause", Just "2019 Kermit le Frog")
    it "errors if SPDX line has incorrect syntax" $ do
      parseMaybe pSpdxLine "!\n" `shouldBe` Nothing
      parseMaybe pSpdxLine "! Public-Domain;\n" `shouldBe` Nothing
      parseMaybe pSpdxLine "! Public-Domain; (\n" `shouldBe` Nothing
      parseMaybe pSpdxLine "! Public-Domain; (c)\n" `shouldBe` Nothing
      parseMaybe pSpdxLine "! Public-Domain; (c) \n" `shouldBe` Nothing

    it "correctly parses a complete technique program header" $ do
      parseMaybe
        pTechnique
        [quote|
% technique v0
! BSD-3-Clause
            |]
        `shouldBe` Just
          ( Technique
              { techniqueVersion = 0,
                techniqueLicense = "BSD-3-Clause",
                techniqueCopyright = Nothing,
                techniqueBody = []
              }
          )

  describe "Parses a proecdure declaration" $ do
    it "name parser handles valid identifiers" $ do
      parseMaybe pIdentifier "" `shouldBe` Nothing
      parseMaybe pIdentifier "i" `shouldBe` Just (Identifier "i")
      parseMaybe pIdentifier "ingredients" `shouldBe` Just (Identifier "ingredients")
      parseMaybe pIdentifier "roast_turkey" `shouldBe` Just (Identifier "roast_turkey")
      parseMaybe pIdentifier "1x" `shouldBe` Nothing
      parseMaybe pIdentifier "x1" `shouldBe` Just (Identifier "x1")

    it "type parser handles valid type names" $ do
      parseMaybe pType "" `shouldBe` Nothing
      parseMaybe pType "I" `shouldBe` Just (Type "I")
      parseMaybe pType "Ingredients" `shouldBe` Just (Type "Ingredients")
      parseMaybe pType "RoastTurkey" `shouldBe` Just (Type "RoastTurkey")
      parseMaybe pType "Roast_Turkey" `shouldBe` Nothing
      parseMaybe pType "2Dinner" `shouldBe` Nothing
      parseMaybe pType "Dinner3" `shouldBe` Just (Type "Dinner3")

    it "handles a name, parameters, and a type signature" $ do
      parseMaybe pProcedureDeclaration "roast_turkey i : Ingredients -> Turkey"
        `shouldBe` Just (Identifier "roast_turkey", [Identifier "i"], [Type "Ingredients"], [Type "Turkey"])
      parseMaybe pProcedureDeclaration "roast_turkey  i  :  Ingredients  ->  Turkey"
        `shouldBe` Just (Identifier "roast_turkey", [Identifier "i"], [Type "Ingredients"], [Type "Turkey"])
      parseMaybe pProcedureDeclaration "roast_turkey:Ingredients->Turkey"
        `shouldBe` Just (Identifier "roast_turkey", [], [Type "Ingredients"], [Type "Turkey"])

  describe "Literals" $ do
    it "quoted string is interpreted as text" $ do
      parseMaybe stringLiteral "\"Hello world\"" `shouldBe` Just "Hello world"
      parseMaybe stringLiteral "\"Hello \\\"world\"" `shouldBe` Just "Hello \"world"
      parseMaybe stringLiteral "\"\"" `shouldBe` Just ""

    it "positive integers" $ do
      parseMaybe numberLiteral "42" `shouldBe` Just 42
      parseMaybe numberLiteral "0" `shouldBe` Just 0
      parseMaybe numberLiteral "1a" `shouldBe` Nothing

  describe "Parses quantities" $ do
    it "a number is a Number" $ do
      parseMaybe pQuantity "42" `shouldBe` Just (Number 42)
      parseMaybe pQuantity "-42" `shouldBe` Just (Number (-42))

    it "a quantity with units is a Quantity" $ do
      parseMaybe pQuantity "149 kg" `shouldBe` Just (Quantity (Decimal 149 0) (Decimal 0 0) 0 "kg")

    it "a quantity with mantissa, magnitude, and units is a Quantity" $ do
      parseMaybe pQuantity "5.9722 × 10^24 kg" `shouldBe` Just (Quantity (Decimal 59722 4) (Decimal 0 0) 24 "kg")

    it "a quantity with mantissa, uncertainty, and units is a Quantity" $ do
      parseMaybe pQuantity "5.9722 ± 0.0006 kg" `shouldBe` Just (Quantity (Decimal 59722 4) (Decimal 6 4) 0 "kg")

    it "a quantity with mantissa, uncertainty, magnitude, and units is a Quantity" $ do
      parseMaybe pQuantity "5.9722 ± 0.0006 × 10^24 kg" `shouldBe` Just (Quantity (Decimal 59722 4) (Decimal 6 4) 24 "kg")

    it "same quantity, with superscripts, is a Quantity" $ do
      parseMaybe pQuantity "5.9722 ± 0.0006 × 10²⁴ kg" `shouldBe` Just (Quantity (Decimal 59722 4) (Decimal 6 4) 24 "kg")

    it "negative Quantities also parse" $ do
      parseMaybe pQuantity "1234567890" `shouldBe` Just (Number 1234567890)
      parseMaybe pQuantity "-1234567890" `shouldBe` Just (Number (-1234567890))
      parseMaybe pQuantity "3.14 ± 0.01 m" `shouldBe` Just (Quantity (Decimal 314 2) (Decimal 001 2) 0 "m")
      parseMaybe pQuantity "-3.14 ± 0.01 m" `shouldBe` Just (Quantity (Decimal (-314) 2) (Decimal 001 2) 0 "m")

  describe "Parses attributes" $ do
    it "recognizes a role marker" $ do
      parseMaybe pAttribute "@butler" `shouldBe` Just (Role (Identifier "butler"))
    it "recognizes a place marker" $ do
      parseMaybe pAttribute "#library" `shouldBe` Just (Place (Identifier "library"))
    it "recognizes any" $ do
      parseMaybe pAttribute "@*" `shouldBe` Just (Role (Identifier "*"))
      parseMaybe pAttribute "#*" `shouldBe` Just (Place (Identifier "*"))

  describe "Parses expressions" $ do
    it "an empty input an error" $ do
      parseMaybe pExpression "" `shouldBe` Nothing

    it "an pair of parentheses is None" $ do
      parseMaybe pExpression "()" `shouldBe` Just (None 0)

    it "a quoted string is a Text" $ do
      parseMaybe pExpression "\"Hello world\"" `shouldBe` Just (Text 0 "Hello world")
      parseMaybe pExpression "\"\"" `shouldBe` Just (Text 0 "")

    it "a bare identifier is a Variable" $ do
      parseMaybe pExpression "x" `shouldBe` Just (Variable 0 [Identifier "x"])

    it "an identifier, space, and then expression is an Application" $ do
      parseMaybe pExpression "a x"
        `shouldBe` Just (Application 0 (Identifier "a") (Variable 2 [Identifier "x"]))

    it "a quoted string is a Literal Text" $ do
      parseMaybe pExpression "\"Hello world\"" `shouldBe` Just (Text 0 "Hello world")

    it "a bare number is a Literal Number" $ do
      parseMaybe pExpression "42" `shouldBe` Just (Amount 0 (Number 42))

    it "a nested expression is parsed as Grouped" $ do
      parseMaybe pExpression "(42)" `shouldBe` Just (Grouping 0 (Amount 1 (Number 42)))

    it "an operator between two expressions is an Operation" $ do
      parseMaybe pExpression "x & y"
        `shouldBe` Just
          ( Operation
              0
              WaitBoth
              (Variable 0 [Identifier "x"])
              (Variable 4 [Identifier "y"])
          )

    it "handles tablet with one binding" $ do
      parseMaybe pExpression "[ \"King\" ~ george ]"
        `shouldBe` Just
          ( Object
              0
              ( Tablet
                  [ Binding (Label "King") (Variable 11 [Identifier "george"])
                  ]
              )
          )

    it "handles tablet with multiple bindings" $ do
      parseMaybe pExpression "[ \"first\" ~ \"George\" \n \"last\" ~ \"Windsor\" ]"
        `shouldBe` Just
          ( Object
              0
              ( Tablet
                  [ Binding (Label "first") (Text 12 "George"),
                    Binding (Label "last") (Text 32 "Windsor")
                  ]
              )
          )
  {-
    it "handles tablet with alternate single-line syntax" $
      let expected =
            Just
              ( Object
                  ( Tablet
                      [ Binding "name" (Variable [Identifier "n"]),
                        Binding "king" (Amount (Number 42))
                      ]
                  )
              )
       in do
            parseMaybe pExpression "[\"name\" ~ n,\"king\" ~ 42]" `shouldBe` expected
            parseMaybe pExpression "[\"name\" ~ n , \"king\" ~ 42]" `shouldBe` expected
  -}

  describe "Parses statements containing expressions" $ do
    it "a blank line is a Blank" $ do
      parseMaybe pStatement "\n" `shouldBe` Just (Blank 0)

    it "considers a single identifier an Execute" $ do
      parseMaybe pStatement "x"
        `shouldBe` Just (Execute 0 (Variable 0 [Identifier "x"]))

    it "considers a line with an '=' to be an Assignment" $ do
      parseMaybe pStatement "answer = 42"
        `shouldBe` Just (Assignment 0 [Identifier "answer"] (Amount 9 (Number 42)))

  describe "Parses blocks of statements" $ do
    it "an empty block is a [] (special case)" $ do
      parseMaybe pBlock "{}" `shouldBe` Just (Block [])

    it "a block with a newline (only) is []" $ do
      parseMaybe pBlock "{\n}" `shouldBe` Just (Block [])

    it "a block with single statement surrounded by a newlines" $ do
      parseMaybe pBlock "{\nx\n}"
        `shouldBe` Just
          ( Block
              [ Execute 2 (Variable 2 [Identifier "x"])
              ]
          )
      parseMaybe pBlock "{\nanswer = 42\n}"
        `shouldBe` Just
          ( Block
              [ (Assignment 2 [Identifier "answer"] (Amount 11 (Number 42)))
              ]
          )

    it "a block with a blank line contains a Blank" $ do
      parseMaybe pBlock "{\nx1\n\nx2\n}"
        `shouldBe` Just
          ( Block
              [ Execute 2 (Variable 2 [Identifier "x1"]),
                Blank 5,
                Execute 6 (Variable 6 [Identifier "x2"])
              ]
          )

    it "a block with multiple statements separated by newlines" $ do
      parseMaybe pBlock "{\nx\nanswer = 42\n}"
        `shouldBe` Just
          ( Block
              [ Execute 2 (Variable 2 [Identifier "x"]),
                Assignment 4 [Identifier "answer"] (Amount 13 (Number 42))
              ]
          )

    it "a block with multiple statements separated by semicolons" $ do
      parseMaybe pBlock "{x ; answer = 42}"
        `shouldBe` Just
          ( Block
              [ Execute 1 (Variable 1 [Identifier "x"]),
                Series 3,
                Assignment 5 [Identifier "answer"] (Amount 14 (Number 42))
              ]
          )

    it "consumes whitespace in inconvenient places" $ do
      parseMaybe pBlock "{ \n }"
        `shouldBe` Just (Block [])
      parseMaybe pBlock "{ \n x \n }"
        `shouldBe` Just
          ( Block
              [ Execute 4 (Variable 4 [Identifier "x"])
              ]
          )
      parseMaybe pBlock "{ \n (42)    \n}"
        `shouldBe` Just
          ( Block
              [ Execute 4 (Grouping 4 (Amount 5 (Number 42)))
              ]
          )
      parseMaybe pBlock "{ \n (42 )    \n}"
        `shouldBe` Just
          ( Block
              [ Execute 4 (Grouping 4 (Amount 5 (Number 42)))
              ]
          )
      parseMaybe pBlock "{ answer = 42 ; }"
        `shouldBe` Just
          ( Block
              [ Assignment 2 [Identifier "answer"] (Amount 11 (Number 42)),
                Series 14
              ]
          )

  describe "Parses a procedure declaration" $ do
    it "simple declaration " $ do
      parseMaybe pProcedureDeclaration "f x : X -> Y"
        `shouldBe` Just (Identifier "f", [Identifier "x"], [Type "X"], [Type "Y"])

    it "declaration with multiple variables and input types" $ do
      parseMaybe pProcedureDeclaration "after_dinner i,s,w : IceCream,Strawberries,Waffles -> Dessert"
        `shouldBe` Just
          ( Identifier "after_dinner",
            [Identifier "i", Identifier "s", Identifier "w"],
            [Type "IceCream", Type "Strawberries", Type "Waffles"],
            [Type "Dessert"]
          )

    it "handles spurious whitespace" $ do
      parseMaybe pProcedureDeclaration "after_dinner   i ,s ,w  :  IceCream ,Strawberries,  Waffles -> Dessert"
        `shouldBe` Just
          ( Identifier "after_dinner",
            [Identifier "i", Identifier "s", Identifier "w"],
            [Type "IceCream", Type "Strawberries", Type "Waffles"],
            [Type "Dessert"]
          )

  describe "Parses a the code for a complete procedure" $ do
    it "parses a declaration and block" $ do
      parseMaybe pProcedureCode "f : X -> Y\n{ x }\n"
        `shouldBe` Just
          ( emptyProcedure
              { procedureOffset = 0,
                procedureName = Identifier "f",
                procedureInput = [Type "X"],
                procedureOutput = [Type "Y"],
                procedureBlock = Block [Execute 13 (Variable 13 [Identifier "x"])]
              }
          )
