{-# LANGUAGE OverloadedStrings #-}

module StoreSpec (spec) where

import Data.Aeson (Value (..))
import Test.Hspec (Spec, describe, it, shouldBe)
import Test.Hspec.QuickCheck (prop)
import Test.QuickCheck.Instances ()

import qualified Data.HashMap.Strict as HashMap

import OrphanInstances ()
import Store (Modification (..))
import qualified Store

spec :: Spec
spec = do
  describe "Store.insert" $ do

    it "creates an object when putting 'x' into Null" $
      let
        before = Null
        after = Object $ HashMap.singleton "x" (String "Robert")
      in
        Store.insert ["x"] (String "Robert") before `shouldBe` after

    it "overwrites a key when putting 'x' into {'x': ...}" $
      let
        before = Object $ HashMap.singleton "x" (String "Arian")
        after = Object $ HashMap.singleton "x" (String "Robert")
      in
        Store.insert ["x"] (String "Robert") before `shouldBe` after

    it "adds a key when putting 'x' into {'y': ...}" $
      let
        before = Object $ HashMap.singleton "y" (String "Arian")
        after = Object $ HashMap.fromList [("x", String "Robert"), ("y", String "Arian")]
      in
        Store.insert ["x"] (String "Robert") before `shouldBe` after

    it "creates a nested object when putting 'x/y' into Null" $
      let
        before = Null
        after = Object $ HashMap.singleton "x" $ Object $ HashMap.singleton "y" "Stefan"
      in
        Store.insert ["x", "y"] (String "Stefan") before `shouldBe` after

    it "updates a nested object when putting 'x/y' into {'x': {'y': ...}}" $
      let
        before = Object $ HashMap.singleton "x" $ Object $ HashMap.singleton "y" "Radek"
        after = Object $ HashMap.singleton "x" $ Object $ HashMap.singleton "y" "Stefan"
      in
        Store.insert ["x", "y"] (String "Stefan") before `shouldBe` after

    it "adds a nested key when putting 'x/y' into {'x': {'y': ...}, 'z': ...}" $
      let
        before = Object $ HashMap.fromList [("x", Object $ HashMap.singleton "y" "Nuno"), ("z", Null)]
        after = Object $ HashMap.fromList [("x", Object $ HashMap.singleton "y" "Stefan"), ("z", Null)]
      in
        Store.insert ["x", "y"] (String "Stefan") before `shouldBe` after

  describe "Store" $ do

    prop "returns None after (lookup . delete . insert) in Null" $ \ path value ->
      let
        lkupDelIns = Store.lookup path . Store.delete path . Store.insert path value
      in
        if path == []
          then lkupDelIns Null `shouldBe` (Just Null)
          else lkupDelIns Null `shouldBe` Nothing

    prop "returns None after (lookup . delete . insert) in anything" $ \ path value before ->
      let
        lkupDelIns = Store.lookup path . Store.delete path . Store.insert path value
      in
        if path == []
          then lkupDelIns before `shouldBe` (Just Null)
          else lkupDelIns before `shouldBe` Nothing

  describe "Store.applyModification" $ do

    it "creates an object when putting 'x' into Null" $
      let
        put = Put ["x"] (String "Robert")
        before = Null
        after = Object $ HashMap.singleton "x" (String "Robert")
      in
        Store.applyModification put before `shouldBe` after

    it "overwrites a key when putting 'x' into {'x': ...}" $
      let
        put = Put ["x"] (String "Robert")
        before = Object $ HashMap.singleton "x" (String "Arian")
        after = Object $ HashMap.singleton "x" (String "Robert")
      in
        Store.applyModification put before `shouldBe` after

    it "adds a key when putting 'x' into {'y': ...}" $
      let
        put = Put ["x"] (String "Robert")
        before = Object $ HashMap.singleton "y" (String "Arian")
        after = Object $ HashMap.fromList [("x", String "Robert"), ("y", String "Arian")]
      in
        Store.applyModification put before `shouldBe` after

    it "creates a nested object when putting 'x/y' into Null" $
      let
        put = Put ["x", "y"] (String "Stefan")
        before = Null
        after = Object $ HashMap.singleton "x" $ Object $ HashMap.singleton "y" "Stefan"
      in
        Store.applyModification put before `shouldBe` after

    it "updates a nested object when putting 'x/y' into {'x': {'y': ...}}" $
      let
        put = Put ["x", "y"] (String "Stefan")
        before = Object $ HashMap.singleton "x" $ Object $ HashMap.singleton "y" "Radek"
        after = Object $ HashMap.singleton "x" $ Object $ HashMap.singleton "y" "Stefan"
      in
        Store.applyModification put before `shouldBe` after

    it "adds a nested key when putting 'x/y' into {'x': {'y': ...}, 'z': ...}" $
      let
        put = Put ["x", "y"] (String "Stefan")
        before = Object $ HashMap.fromList [("x", Object $ HashMap.singleton "y" "Nuno"), ("z", Null)]
        after = Object $ HashMap.fromList [("x", Object $ HashMap.singleton "y" "Stefan"), ("z", Null)]
      in
        Store.applyModification put before `shouldBe` after

    prop "is idempotent" $ \op value ->
      let apply = Store.applyModification op
      in apply value == apply (apply value)