{-# OPTIONS_GHC -Wno-orphans #-}

import           Control.Monad ( liftM4, replicateM )
import           Data.ASN1.BinaryEncoding ( DER (..) )
import           Data.ASN1.BinaryEncoding.Parse ( parseLBS )
import           Data.ASN1.BinaryEncoding.Writer ( toLazyByteString )
import           Data.ASN1.BitArray ( BitArray, toBitArray )
import           Data.ASN1.Encoding ( ASN1Decoding (..), ASN1Encoding (..) )
import           Data.ASN1.Get ( Result (..), runGet )
import           Data.ASN1.Prim ( mkSmallestLength )
import           Data.ASN1.Serialize ( getHeader, putHeader )
import           Data.ASN1.Types
                   ( ASN1 (..), ASN1CharacterString (..)
                   , ASN1ConstructionType (..), ASN1StringEncoding (..)
                   , ASN1TimeType (..), OID, asn1CharacterString
                   )
import           Data.ASN1.Types.Lowlevel
                   ( ASN1Class (..), ASN1Event (..), ASN1Header (..)
                   , ASN1Length (..), ASN1Tag
                   )
import qualified Data.ByteString as B
import           Data.Hourglass
                   ( Date (..), DateTime (..), Elapsed (..), Hours (..)
                   , Minutes (..), Month (..), Seconds (..), TimeOfDay (..)
                   , TimezoneOffset (..), timeConvert, timezone_UTC
                   )
import           Test.Tasty ( TestTree, defaultMain, testGroup )
import           Test.Tasty.QuickCheck
                   ( Arbitrary (..), Gen, choose, elements, listOf1, oneof
                   , resize, suchThat, testProperty
                   )

instance Arbitrary ASN1Class where
  arbitrary = elements [ Universal, Application, Context, Private ]

instance Arbitrary ASN1Length where
  arbitrary = do
    c <- choose (0, 2) :: Gen Int
    case c of
      0 -> fmap LenShort (choose (0, 0x79))
      1 -> do
        nb <- choose (0x80, 0x1000)
        pure $ mkSmallestLength nb
      _ -> pure LenIndefinite

arbitraryDefiniteLength :: Gen ASN1Length
arbitraryDefiniteLength = arbitrary `suchThat` (/= LenIndefinite)

arbitraryTag :: Gen ASN1Tag
arbitraryTag = choose (1, 10000)

instance Arbitrary ASN1Header where
  arbitrary = liftM4 ASN1Header arbitrary arbitraryTag arbitrary arbitrary

arbitraryEvents :: Gen ASN1Events
arbitraryEvents = do
  hdr@(ASN1Header _ _ _ len) <-
    liftM4 ASN1Header arbitrary arbitraryTag (pure False) arbitraryDefiniteLength
  let blen = case len of
        LenLong _ x -> x
        LenShort x  -> x
        _           -> 0
  pr <- fmap Primitive (arbitraryBSsized blen)
  pure (ASN1Events [Header hdr, pr])

newtype ASN1Events = ASN1Events [ASN1Event]

instance Show ASN1Events where
  show (ASN1Events x) = show x

instance Arbitrary ASN1Events where
  arbitrary = arbitraryEvents

arbitraryOID :: Gen OID
arbitraryOID = do
  i1  <- choose (0, 2) :: Gen Integer
  i2  <- choose (0, 39) :: Gen Integer
  ran <- choose (0, 30) :: Gen Int
  l   <- replicateM ran (suchThat arbitrary (> 0))
  pure (i1:i2:l)

arbitraryBSsized :: Int -> Gen B.ByteString
arbitraryBSsized len = do
  ws <- replicateM len (choose (0, 255) :: Gen Int)
  pure $ B.pack $ map fromIntegral ws

instance Arbitrary B.ByteString where
  arbitrary = do
    len <- choose (0, 529) :: Gen Int
    arbitraryBSsized len

instance Arbitrary BitArray where
  arbitrary = do
    bs <- arbitrary
    w  <- choose (0, 7) :: Gen Int
    pure $ toBitArray bs w

instance Arbitrary Date where
  arbitrary = do
    y <- choose (1951, 2050)
    m <- elements [ January .. December]
    d <- choose (1, 30)
    pure $ normalizeDate $ Date y m d

normalizeDate :: Date -> Date
normalizeDate origDate
  | y < 1951  = normalizeDate (Date (y + 50) m d)
  | otherwise = normalizedDate
 where
  normalizedDate@(Date y m d) = timeConvert (timeConvert origDate :: Elapsed)

instance Arbitrary TimeOfDay where
  arbitrary = do
    h    <- choose (0, 23)
    mi   <- choose (0, 59)
    se   <- choose (0, 59)
    let nsec = 0
    pure $ TimeOfDay (Hours h) (Minutes mi) (Seconds se) nsec

instance Arbitrary DateTime where
  arbitrary = DateTime <$> arbitrary <*> arbitrary

instance Arbitrary TimezoneOffset where
  arbitrary = elements
    [ timezone_UTC
    , TimezoneOffset 60
    , TimezoneOffset 120
    , TimezoneOffset (-360)
    ]

instance Arbitrary Elapsed where
  arbitrary = Elapsed . Seconds <$> arbitrary

instance Arbitrary ASN1TimeType where
  arbitrary = elements [TimeUTC, TimeGeneralized]

instance Arbitrary ASN1StringEncoding where
  arbitrary = elements
    [ UTF8
    , Numeric
    , Printable
    , T61
    , VideoTex
    , IA5
    , Graphic
    , Visible
    , General
    , UTF32
    , BMP
    ]

arbitraryPrintString :: ASN1StringEncoding -> Gen ASN1CharacterString
arbitraryPrintString encoding = do
  let printableString = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ " ()+,-./:=?"
  asn1CharacterString encoding <$> replicateM 21 (elements printableString)

arbitraryBS :: ASN1StringEncoding -> Gen ASN1CharacterString
arbitraryBS encoding =
  ASN1CharacterString encoding . B.pack <$> replicateM 7 (choose (0, 0xff))

arbitraryIA5String :: Gen ASN1CharacterString
arbitraryIA5String =
  asn1CharacterString IA5 <$> replicateM 21 (choose (toEnum 0, toEnum 127))

arbitraryUCS2 :: Gen ASN1CharacterString
arbitraryUCS2 =
  asn1CharacterString BMP <$> replicateM 12 (choose (toEnum 0, toEnum 0xffff))

arbitraryUnicode :: ASN1StringEncoding -> Gen ASN1CharacterString
arbitraryUnicode e =
  asn1CharacterString e <$> replicateM 35 (choose (toEnum 0, toEnum 0x10ffff))

instance Arbitrary ASN1CharacterString where
  arbitrary = oneof
    [ arbitraryUnicode UTF8
    , arbitraryUnicode UTF32
    , arbitraryUCS2
    , arbitraryPrintString Numeric
    , arbitraryPrintString Printable
    , arbitraryBS T61
    , arbitraryBS VideoTex
    , arbitraryIA5String
    , arbitraryPrintString Graphic
    , arbitraryPrintString Visible
    , arbitraryPrintString General
    ]

instance Arbitrary ASN1 where
  arbitrary = oneof
    [ fmap Boolean arbitrary
    , fmap IntVal arbitrary
    , fmap BitString arbitrary
    , fmap OctetString arbitrary
    , pure Null
    , fmap OID arbitraryOID
    , fmap Real arbitrary
      -- , pure Enumerated
    , ASN1String <$> arbitrary
    , ASN1Time <$> arbitrary <*> arbitrary <*> arbitrary
    ]

newtype ASN1s = ASN1s [ASN1]

instance Show ASN1s where
  show (ASN1s x) = show x

instance Arbitrary ASN1s where
  arbitrary = do
    x <- choose (0,5) :: Gen Int
    z <- case x of
      4 -> makeList Sequence
      3 -> makeList Set
      _ -> resize 2 $ listOf1 arbitrary
    pure $ ASN1s z
   where
    makeList str = do
      (ASN1s l) <- arbitrary
      pure ([Start str] ++ l ++ [End str])

prop_header_marshalling_id :: ASN1Header -> Bool
prop_header_marshalling_id v =
  ofDone ( runGet getHeader $ putHeader v) == Right v
 where
  ofDone (Done r _ _) = Right r
  ofDone _            = Left "not done"

prop_event_marshalling_id :: ASN1Events -> Bool
prop_event_marshalling_id (ASN1Events e) =
  parseLBS (toLazyByteString e) == Right e

prop_asn1_der_marshalling_id :: [ASN1] -> Bool
prop_asn1_der_marshalling_id v =
  (decodeASN1 DER . encodeASN1 DER) v `assertEq` Right v
 where
  assertEq got expected
    | got /= expected =
        error ("got: " ++ show got ++ " expected: " ++ show expected)
    | otherwise       = True

prop_real_der_marshalling_id :: Double -> Bool
prop_real_der_marshalling_id v =
  (decodeASN1 DER . encodeASN1 DER) [Real v] `assertEq` Right [Real v]
 where
  assertEq got expected
    | got /= expected =
        error ("got: " ++ show got ++ " expected: " ++ show expected)
    | otherwise       = True

prop_integral_real_der_marshalling_id :: Integer -> Bool
prop_integral_real_der_marshalling_id v =
  (decodeASN1 DER . encodeASN1 DER) [Real (fromInteger v)]
    `assertEq` Right [Real (fromInteger v)]
 where
  assertEq got expected
    | got /= expected =
        error ("got: " ++ show got ++ " expected: " ++ show expected)
    | otherwise       = True

marshallingTests :: TestTree
marshallingTests = testGroup "Marshalling"
  [ testProperty "Header" prop_header_marshalling_id
  , testProperty "Event"  prop_event_marshalling_id
  , testProperty "DER"    prop_asn1_der_marshalling_id
  , testProperty "Real"   prop_real_der_marshalling_id
  , testProperty "Integral Real"   prop_integral_real_der_marshalling_id
  ]

main :: IO ()
main = defaultMain $ testGroup "asn1-encoding" [marshallingTests]
