{-# LANGUAGE OverloadedStrings #-}

module Text.Megaparsec.StreamSpec (spec) where

import Data.Char (isLetter, chr)
import Data.Proxy
import Data.Semigroup ((<>))
import Test.Hspec
import Test.Hspec.Megaparsec.AdHoc
import Test.QuickCheck
import Text.Megaparsec
import qualified Data.ByteString      as B
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text            as T
import qualified Data.Text.Lazy       as TL

spec :: Spec
spec = do

  describe "String instance of Stream" $ do
    describe "tokenToChunk" $
      it "produces the same result as singleton with tokensToChunk" $
        property $ \ch ->
          tokenToChunk sproxy ch === tokensToChunk sproxy [ch]
    describe "tokensToChunk" $
      it "list of tokens is isomorphic to chunk" $
        property $ \ts ->
          chunkToTokens sproxy (tokensToChunk sproxy ts) === ts
    describe "chunkToTokens" $
      it "chunk is isomorphic to list of tokens" $
        property $ \chunk ->
          tokensToChunk sproxy (chunkToTokens sproxy chunk) === chunk
    describe "chunkLength" $
      it "returns correct length of given chunk" $
        property $ \chunk ->
          chunkLength sproxy chunk === length chunk
    describe "chunkEmpty" $
      it "only true when chunkLength returns 0" $
        property $ \chunk ->
          chunkEmpty sproxy chunk === (chunkLength sproxy chunk <= 0)
    describe "positionAt1" $
      it "just returns the given position" $
        property $ \pos t ->
          positionAt1 sproxy pos t === pos
    describe "positionAtN" $
      it "just returns the given position" $
        property $ \pos chunk ->
          positionAtN sproxy pos chunk === pos
    describe "advance1" $ do
      context "when given newline" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l _) ->
            advance1 sproxy w pos '\n' === SourcePos n (l <> pos1) pos1
      context "when given tab" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l c) ->
            advance1 sproxy w pos '\t' === SourcePos n l (toNextTab w c)
      context "when given other character" $
        it "works correctly" $
          property $ \ch w pos@(SourcePos n l c) ->
            (ch /= '\n' && ch /= '\t') ==>
              advance1 sproxy w pos ch === SourcePos n l (c <> pos1)
    describe "advanceN" $
      it "works correctly" $
        advanceN sproxy defaultTabWidth (initialPos "") "something\n\tfoo"
          === SourcePos "" (mkPos 2) (mkPos 12)
    describe "take1_" $ do
      context "when input in empty" $
        it "returns Nothing" $
          take1_ ("" :: String) === Nothing
      context "when input is not empty" $
        it "unconses a token" $
          property $ \s -> not (null s) ==>
            take1_ (s :: String) === Just (head s, tail s)
    describe "takeN_" $ do
      context "requested length is 0" $
        it "returns Just empty chunk and original stream" $
          property $ \s ->
            takeN_ 0 (s :: String) === Just ("", s)
      context "requested length is greater than 0" $ do
        context "stream is empty" $
          it "returns Nothing" $
            property $ \(Positive n) ->
              takeN_ n ("" :: String) === Nothing
        context "stream is not empty" $
          it "returns a chunk of correct length and rest of the stream" $
            property $ \(Positive n) s -> not (null s) ==>
              takeN_ n (s :: String) === Just (splitAt n s)
    describe "takeWhile_" $
      it "extracts a chunk that is a prefix consisting of matching tokens" $
        property $ \s ->
          takeWhile_ isLetter s === span isLetter s

  describe "ByteString instance of Stream" $ do
    describe "tokenToChunk" $
      it "produces the same result as singleton with tokensToChunk" $
        property $ \ch ->
          tokenToChunk bproxy ch === tokensToChunk bproxy [ch]
    describe "tokensToChunk" $
      it "list of tokens is isomorphic to chunk" $
        property $ \ts ->
          chunkToTokens bproxy (tokensToChunk bproxy ts) === ts
    describe "chunkToTokens" $
      it "chunk is isomorphic to list of tokens" $
        property $ \chunk ->
          tokensToChunk bproxy (chunkToTokens bproxy chunk) === chunk
    describe "chunkLength" $
      it "returns correct length of given chunk" $
        property $ \chunk ->
          chunkLength bproxy chunk === B.length chunk
    describe "chunkEmpty" $
      it "only true when chunkLength returns 0" $
        property $ \chunk ->
          chunkEmpty bproxy chunk === (chunkLength bproxy chunk <= 0)
    describe "positionAt1" $
      it "just returns the given position" $
        property $ \pos t ->
          positionAt1 bproxy pos t === pos
    describe "positionAtN" $
      it "just returns the given position" $
        property $ \pos chunk ->
          positionAtN bproxy pos chunk === pos
    describe "advance1" $ do
      context "when given newline" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l _) ->
            advance1 bproxy w pos 10 === SourcePos n (l <> pos1) pos1
      context "when given tab" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l c) ->
            advance1 bproxy w pos 9 === SourcePos n l (toNextTab w c)
      context "when given other character" $
        it "works correctly" $
          property $ \ch w pos@(SourcePos n l c) ->
            (ch /= 10 && ch /= 9) ==>
              advance1 bproxy w pos ch === SourcePos n l (c <> pos1)
    describe "advanceN" $
      it "works correctly" $
        advanceN bproxy defaultTabWidth (initialPos "") "something\n\tfoo"
          === SourcePos "" (mkPos 2) (mkPos 12)
    describe "take1_" $ do
      context "when input in empty" $
        it "returns Nothing" $
          take1_ ("" :: B.ByteString) === Nothing
      context "when input is not empty" $
        it "unconses a token" $
          property $ \s -> not (B.null s) ==>
            take1_ (s :: B.ByteString) === B.uncons s
    describe "takeN_" $ do
      context "requested length is 0" $
        it "returns Just empty chunk and original stream" $
          property $ \s ->
            takeN_ 0 (s :: B.ByteString) === Just ("", s)
      context "requested length is greater than 0" $ do
        context "stream is empty" $
          it "returns Nothing" $
            property $ \(Positive n) ->
              takeN_ n ("" :: B.ByteString) === Nothing
        context "stream is not empty" $
          it "returns a chunk of correct length and rest of the stream" $
            property $ \(Positive n) s -> not (B.null s) ==>
              takeN_ n (s :: B.ByteString) === Just (B.splitAt n s)
    describe "takeWhile_" $
      it "extracts a chunk that is a prefix consisting of matching tokens" $
        property $ \s ->
          let f = isLetter . chr . fromIntegral
          in takeWhile_ f s === B.span f s

  describe "Lazy ByteString instance of Stream" $ do
    describe "tokenToChunk" $
      it "produces the same result as singleton with tokensToChunk" $
        property $ \ch ->
          tokenToChunk blproxy ch === tokensToChunk blproxy [ch]
    describe "tokensToChunk" $
      it "list of tokens is isomorphic to chunk" $
        property $ \ts ->
          chunkToTokens blproxy (tokensToChunk blproxy ts) === ts
    describe "chunkToTokens" $
      it "chunk is isomorphic to list of tokens" $
        property $ \chunk ->
          tokensToChunk blproxy (chunkToTokens blproxy chunk) === chunk
    describe "chunkLength" $
      it "returns correct length of given chunk" $
        property $ \chunk ->
          chunkLength blproxy chunk === fromIntegral (BL.length chunk)
    describe "chunkEmpty" $
      it "only true when chunkLength returns 0" $
        property $ \chunk ->
          chunkEmpty blproxy chunk === (chunkLength blproxy chunk <= 0)
    describe "positionAt1" $
      it "just returns the given position" $
        property $ \pos t ->
          positionAt1 blproxy pos t === pos
    describe "positionAtN" $
      it "just returns the given position" $
        property $ \pos chunk ->
          positionAtN blproxy pos chunk === pos
    describe "advance1" $ do
      context "when given newline" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l _) ->
            advance1 blproxy w pos 10 === SourcePos n (l <> pos1) pos1
      context "when given tab" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l c) ->
            advance1 blproxy w pos 9 === SourcePos n l (toNextTab w c)
      context "when given other character" $
        it "works correctly" $
          property $ \ch w pos@(SourcePos n l c) ->
            (ch /= 10 && ch /= 9) ==>
              advance1 blproxy w pos ch === SourcePos n l (c <> pos1)
    describe "advanceN" $
      it "works correctly" $
        advanceN blproxy defaultTabWidth (initialPos "") "something\n\tfoo"
          === SourcePos "" (mkPos 2) (mkPos 12)
    describe "take1_" $ do
      context "when input in empty" $
        it "returns Nothing" $
          take1_ ("" :: BL.ByteString) === Nothing
      context "when input is not empty" $
        it "unconses a token" $
          property $ \s -> not (BL.null s) ==>
            take1_ (s :: BL.ByteString) === BL.uncons s
    describe "takeN_" $ do
      context "requested length is 0" $
        it "returns Just empty chunk and original stream" $
          property $ \s ->
            takeN_ 0 (s :: BL.ByteString) === Just ("", s)
      context "requested length is greater than 0" $ do
        context "stream is empty" $
          it "returns Nothing" $
            property $ \(Positive n) ->
              takeN_ n ("" :: BL.ByteString) === Nothing
        context "stream is not empty" $
          it "returns a chunk of correct length and rest of the stream" $
            property $ \(Positive n) s -> not (BL.null s) ==>
              takeN_ n (s :: BL.ByteString) === Just (BL.splitAt (fromIntegral n) s)
    describe "takeWhile_" $
      it "extracts a chunk that is a prefix consisting of matching tokens" $
        property $ \s ->
          let f = isLetter . chr . fromIntegral
          in takeWhile_ f s === BL.span f s

  describe "Text instance of Stream" $ do
    describe "tokenToChunk" $
      it "produces the same result as singleton with tokensToChunk" $
        property $ \ch ->
          tokenToChunk tproxy ch === tokensToChunk tproxy [ch]
    describe "tokensToChunk" $
      it "list of tokens is isomorphic to chunk" $
        property $ \ts ->
          chunkToTokens tproxy (tokensToChunk tproxy ts) === ts
    describe "chunkToTokens" $
      it "chunk is isomorphic to list of tokens" $
        property $ \chunk ->
          tokensToChunk tproxy (chunkToTokens tproxy chunk) === chunk
    describe "chunkLength" $
      it "returns correct length of given chunk" $
        property $ \chunk ->
          chunkLength tproxy chunk === T.length chunk
    describe "chunkEmpty" $
      it "only true when chunkLength returns 0" $
        property $ \chunk ->
          chunkEmpty tproxy chunk === (chunkLength tproxy chunk <= 0)
    describe "positionAt1" $
      it "just returns the given position" $
        property $ \pos t ->
          positionAt1 tproxy pos t === pos
    describe "positionAtN" $
      it "just returns the given position" $
        property $ \pos chunk ->
          positionAtN tproxy pos chunk === pos
    describe "advance1" $ do
      context "when given newline" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l _) ->
            advance1 tproxy w pos '\n' === SourcePos n (l <> pos1) pos1
      context "when given tab" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l c) ->
            advance1 tproxy w pos '\t' === SourcePos n l (toNextTab w c)
      context "when given other character" $
        it "works correctly" $
          property $ \ch w pos@(SourcePos n l c) ->
            (ch /= '\n' && ch /= '\t') ==>
              advance1 tproxy w pos ch === SourcePos n l (c <> pos1)
    describe "advanceN" $
      it "works correctly" $
        advanceN tproxy defaultTabWidth (initialPos "") "something\n\tfoo"
          === SourcePos "" (mkPos 2) (mkPos 12)
    describe "take1_" $ do
      context "when input in empty" $
        it "returns Nothing" $
          take1_ ("" :: T.Text) === Nothing
      context "when input is not empty" $
        it "unconses a token" $
          property $ \s -> not (T.null s) ==>
            take1_ (s :: T.Text) === T.uncons s
    describe "takeN_" $ do
      context "requested length is 0" $
        it "returns Just empty chunk and original stream" $
          property $ \s ->
            takeN_ 0 (s :: T.Text) === Just ("", s)
      context "requested length is greater than 0" $ do
        context "stream is empty" $
          it "returns Nothing" $
            property $ \(Positive n) ->
              takeN_ n ("" :: T.Text) === Nothing
        context "stream is not empty" $
          it "returns a chunk of correct length and rest of the stream" $
            property $ \(Positive n) s -> not (T.null s) ==>
              takeN_ n (s :: T.Text) === Just (T.splitAt n s)
    describe "takeWhile_" $
      it "extracts a chunk that is a prefix consisting of matching tokens" $
        property $ \s ->
          takeWhile_ isLetter s === T.span isLetter s

  describe "Lazy Text instance of Stream" $ do
    describe "tokenToChunk" $
      it "produces the same result as singleton with tokensToChunk" $
        property $ \ch ->
          tokenToChunk tlproxy ch === tokensToChunk tlproxy [ch]
    describe "tokensToChunk" $
      it "list of tokens is isomorphic to chunk" $
        property $ \ts ->
          chunkToTokens tlproxy (tokensToChunk tlproxy ts) === ts
    describe "chunkToTokens" $
      it "chunk is isomorphic to list of tokens" $
        property $ \chunk ->
          tokensToChunk tlproxy (chunkToTokens tlproxy chunk) === chunk
    describe "chunkLength" $
      it "returns correct length of given chunk" $
        property $ \chunk ->
          chunkLength tlproxy chunk === fromIntegral (TL.length chunk)
    describe "chunkEmpty" $
      it "only true when chunkLength returns 0" $
        property $ \chunk ->
          chunkEmpty tlproxy chunk === (chunkLength tlproxy chunk <= 0)
    describe "positionAt1" $
      it "just returns the given position" $
        property $ \pos t ->
          positionAt1 tlproxy pos t === pos
    describe "positionAtN" $
      it "just returns the given position" $
        property $ \pos chunk ->
          positionAtN tlproxy pos chunk === pos
    describe "advance1" $ do
      context "when given newline" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l _) ->
            advance1 tlproxy w pos '\n' === SourcePos n (l <> pos1) pos1
      context "when given tab" $
        it "works correctly" $
          property $ \w pos@(SourcePos n l c) ->
            advance1 tlproxy w pos '\t' === SourcePos n l (toNextTab w c)
      context "when given other character" $
        it "works correctly" $
          property $ \ch w pos@(SourcePos n l c) ->
            (ch /= '\n' && ch /= '\t') ==>
              advance1 tlproxy w pos ch === SourcePos n l (c <> pos1)
    describe "advanceN" $
      it "works correctly" $
        advanceN tlproxy defaultTabWidth (initialPos "") "something\n\tfoo"
          === SourcePos "" (mkPos 2) (mkPos 12)
    describe "take1_" $ do
      context "when input in empty" $
        it "returns Nothing" $
          take1_ ("" :: TL.Text) === Nothing
      context "when input is not empty" $
        it "unconses a token" $
          property $ \s -> not (TL.null s) ==>
            take1_ (s :: TL.Text) === TL.uncons s
    describe "takeN_" $ do
      context "requested length is 0" $
        it "returns Just empty chunk and original stream" $
          property $ \s ->
            takeN_ 0 (s :: TL.Text) === Just ("", s)
      context "requested length is greater than 0" $ do
        context "stream is empty" $
          it "returns Nothing" $
            property $ \(Positive n) ->
              takeN_ n ("" :: TL.Text) === Nothing
        context "stream is not empty" $
          it "returns a chunk of correct length and rest of the stream" $
            property $ \(Positive n) s -> not (TL.null s) ==>
              takeN_ n (s :: TL.Text) === Just (TL.splitAt (fromIntegral n) s)
    describe "takeWhile_" $
      it "extracts a chunk that is a prefix consisting of matching tokens" $
        property $ \s ->
          takeWhile_ isLetter s === TL.span isLetter s

----------------------------------------------------------------------------
-- Helpers

toNextTab :: Pos -> Pos -> Pos
toNextTab w' c' = mkPos $ c + w - ((c - 1) `rem` w)
  where
     w = unPos w'
     c = unPos c'

sproxy :: Proxy String
sproxy = Proxy

bproxy :: Proxy B.ByteString
bproxy = Proxy

blproxy :: Proxy BL.ByteString
blproxy = Proxy

tproxy :: Proxy T.Text
tproxy = Proxy

tlproxy :: Proxy TL.Text
tlproxy = Proxy
