packages feed

seihou-core-0.4.0.0: test/Seihou/Core/ExprSpec.hs

module Seihou.Core.ExprSpec (tests) where

import Data.Map.Strict qualified as Map
import Seihou.Core.Expr (evalExpr, exprRefs, parseExpr)
import Seihou.Core.Types
import Test.Hspec
import Test.Tasty
import Test.Tasty.Hspec (testSpec)

tests :: IO TestTree
tests = testSpec "Seihou.Core.Expr" spec

spec :: Spec
spec = do
  describe "parseExpr" $ do
    it "parses true literal" $ do
      parseExpr "true" `shouldBe` Right (ExprLit True)

    it "parses false literal" $ do
      parseExpr "false" `shouldBe` Right (ExprLit False)

    it "parses IsSet atom" $ do
      parseExpr "IsSet license" `shouldBe` Right (ExprIsSet "license")

    it "parses IsSet with dotted var name" $ do
      parseExpr "IsSet project.name" `shouldBe` Right (ExprIsSet "project.name")

    it "parses Eq with quoted value" $ do
      parseExpr "Eq license \"MIT\""
        `shouldBe` Right (ExprEq "license" (VText "MIT"))

    it "parses Eq with bare word value" $ do
      parseExpr "Eq license MIT"
        `shouldBe` Right (ExprEq "license" (VText "MIT"))

    it "parses Eq with bare word true as VBool" $ do
      parseExpr "Eq enabled true"
        `shouldBe` Right (ExprEq "enabled" (VBool True))

    it "parses Eq with bare word false as VBool" $ do
      parseExpr "Eq enabled false"
        `shouldBe` Right (ExprEq "enabled" (VBool False))

    it "parses Eq with all-digit bare word as VInt" $ do
      parseExpr "Eq count 3"
        `shouldBe` Right (ExprEq "count" (VInt 3))

    it "parses Eq with negative integer bare word as VInt" $ do
      parseExpr "Eq offset -5"
        `shouldBe` Right (ExprEq "offset" (VInt (-5)))

    it "parses Eq with quoted digits as VText (not VInt)" $ do
      parseExpr "Eq count \"3\""
        `shouldBe` Right (ExprEq "count" (VText "3"))

    it "parses && expression" $ do
      parseExpr "IsSet a && IsSet b"
        `shouldBe` Right (ExprAnd (ExprIsSet "a") (ExprIsSet "b"))

    it "parses || expression" $ do
      parseExpr "IsSet a || IsSet b"
        `shouldBe` Right (ExprOr (ExprIsSet "a") (ExprIsSet "b"))

    it "parses ! negation" $ do
      parseExpr "!true"
        `shouldBe` Right (ExprNot (ExprLit True))

    it "parses negation with space" $ do
      parseExpr "! false"
        `shouldBe` Right (ExprNot (ExprLit False))

    it "parses parenthesized expression" $ do
      parseExpr "(true)"
        `shouldBe` Right (ExprLit True)

    it "respects operator precedence: && binds tighter than ||" $ do
      parseExpr "IsSet a && IsSet b || IsSet c"
        `shouldBe` Right (ExprOr (ExprAnd (ExprIsSet "a") (ExprIsSet "b")) (ExprIsSet "c"))

    it "parentheses override precedence" $ do
      parseExpr "IsSet a && (IsSet b || IsSet c)"
        `shouldBe` Right (ExprAnd (ExprIsSet "a") (ExprOr (ExprIsSet "b") (ExprIsSet "c")))

    it "parses compound expression from design doc" $ do
      parseExpr "IsSet license && Eq license \"MIT\""
        `shouldBe` Right
          ( ExprAnd
              (ExprIsSet "license")
              (ExprEq "license" (VText "MIT"))
          )

    it "rejects empty expression" $ do
      parseExpr "" `shouldBe` Left "empty expression"

    it "rejects malformed expression" $ do
      case parseExpr "@#$" of
        Left _ -> pure ()
        Right r -> expectationFailure ("Expected parse error, got: " <> show r)

    it "handles whitespace" $ do
      parseExpr "  true  " `shouldBe` Right (ExprLit True)

  describe "evalExpr" $ do
    let vars =
          Map.fromList
            [ (VarName "license", VText "MIT"),
              (VarName "project.name", VText "my-app"),
              (VarName "enabled", VBool True)
            ]

    it "evaluates ExprLit True" $ do
      evalExpr vars (ExprLit True) `shouldBe` True

    it "evaluates ExprLit False" $ do
      evalExpr vars (ExprLit False) `shouldBe` False

    it "evaluates ExprIsSet for present variable" $ do
      evalExpr vars (ExprIsSet "license") `shouldBe` True

    it "evaluates ExprIsSet for absent variable" $ do
      evalExpr vars (ExprIsSet "missing") `shouldBe` False

    it "evaluates ExprEq when values match" $ do
      evalExpr vars (ExprEq "license" (VText "MIT")) `shouldBe` True

    it "evaluates ExprEq when values differ" $ do
      evalExpr vars (ExprEq "license" (VText "BSD")) `shouldBe` False

    it "evaluates ExprEq with VBool True" $ do
      evalExpr vars (ExprEq "enabled" (VBool True)) `shouldBe` True

    it "evaluates ExprEq with VBool False against VBool True" $ do
      evalExpr vars (ExprEq "enabled" (VBool False)) `shouldBe` False

    it "evaluates a parsed int Eq against a VInt binding" $ do
      let intVars = Map.fromList [(VarName "count", VInt 3)]
      case parseExpr "Eq count 3" of
        Right expr -> evalExpr intVars expr `shouldBe` True
        Left err -> expectationFailure ("Expected parse, got: " <> show err)

    it "evaluates ExprNot" $ do
      evalExpr vars (ExprNot (ExprLit True)) `shouldBe` False
      evalExpr vars (ExprNot (ExprLit False)) `shouldBe` True

    it "evaluates ExprAnd" $ do
      evalExpr vars (ExprAnd (ExprLit True) (ExprLit True)) `shouldBe` True
      evalExpr vars (ExprAnd (ExprLit True) (ExprLit False)) `shouldBe` False

    it "evaluates ExprOr" $ do
      evalExpr vars (ExprOr (ExprLit True) (ExprLit False)) `shouldBe` True
      evalExpr vars (ExprOr (ExprLit False) (ExprLit False)) `shouldBe` False

    it "evaluates compound expression from design doc" $ do
      let expr = ExprAnd (ExprIsSet "license") (ExprEq "license" (VText "MIT"))
      evalExpr vars expr `shouldBe` True
      evalExpr Map.empty expr `shouldBe` False

  describe "exprRefs" $ do
    it "returns the compared literal for Eq with a bareword bool" $ do
      exprRefs (ExprEq "x" (VBool True)) `shouldBe` [("x", Just (VBool True))]

    it "returns the compared literal for Eq with a quoted string" $ do
      exprRefs (ExprEq "x" (VText "true")) `shouldBe` [("x", Just (VText "true"))]

    it "returns a Nothing literal for IsSet" $ do
      exprRefs (ExprIsSet "y") `shouldBe` [("y", Nothing)]

    it "collects refs across && with mixed atoms" $ do
      exprRefs (ExprAnd (ExprIsSet "y") (ExprEq "z" (VInt 1)))
        `shouldBe` [("y", Nothing), ("z", Just (VInt 1))]

    it "recurses through Or and Not" $ do
      exprRefs (ExprOr (ExprNot (ExprIsSet "a")) (ExprEq "b" (VBool False)))
        `shouldBe` [("a", Nothing), ("b", Just (VBool False))]

    it "returns nothing for a literal" $ do
      exprRefs (ExprLit True) `shouldBe` []