{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-incomplete-patterns #-}

module Test.Miniscript.Witness (
    witnessTests,
) where

import Data.ByteString (ByteString)
import Data.Serialize (encode)
import Data.Text (Text)
import Haskoin.Crypto (
    ripemd160,
    sha256,
    signHash,
 )
import Haskoin.Keys (PubKeyI, secKeyData)
import Haskoin.Script (
    Script (..),
    ScriptOp (..),
    TxSignature (..),
    encodeTxSig,
    opPushData,
    sigHashAll,
 )
import Haskoin.Util.Arbitrary.Keys (arbitraryKeyPair)
import Haskoin.Util.Arbitrary.Util (arbitraryBSn)
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.QuickCheck (
    Gen,
    Property,
    Testable,
    forAll,
    (===),
 )

import Language.Bitcoin.Miniscript (Miniscript (..), let_)
import Language.Bitcoin.Miniscript.Witness (
    ChainState (..),
    SatisfactionError (..),
    Signature (..),
    emptyChainState,
    preimage,
    satisfactionContext,
    satisfy,
    signature,
 )
import Language.Bitcoin.Script.Descriptors (pubKey)
import Test.Example (
    Example (..),
    testExampleProperty,
 )
import qualified Test.Miniscript.Examples as E
import Test.Utils (forAllLabeled, pr23)

witnessTests :: TestTree
witnessTests = testGroup "witness" examples
  where
    examples =
        [ example1
        , example2
        , example3
        , example4
        , example5
        , example6
        , example7
        , example8
        , example9
        , example10
        ]

pushKey :: PubKeyI -> ScriptOp
pushKey = opPushData . encode

pushSig :: Signature -> ScriptOp
pushSig (Signature s sh) = opPushData . encodeTxSig $ TxSignature s sh

forKeys :: Testable p => [Text] -> Miniscript -> ([(PubKeyI, Signature)] -> Miniscript -> p) -> Property
forKeys ls scr k = forAllLabeled arbKeySig mkRow ls mkProp
  where
    mkRow label (pk, s) = (label, pk, s)
    mkProp xs = k (pr23 <$> xs) $ let_ (binding <$> xs) scr
    binding (l, pk, _) = (l, KeyDesc $ pubKey pk)

arbKeySig :: Gen (PubKeyI, Signature)
arbKeySig = repack <$> arbitraryKeyPair
  where
    repack (sk, pk) = (pk, mkSig $ secKeyData sk)

    mkSig s = Signature (signHash s $ sha256 msg) sigHashAll

    msg :: ByteString
    msg = "arbKeySig"

testExample ::
    Testable p =>
    Example Miniscript ->
    [Text] ->
    ([(PubKeyI, Signature)] -> Miniscript -> p) ->
    TestTree
testExample e ls = testExampleProperty e . forKeys ls (script e)

example1 :: TestTree
example1 = testExample E.example1 ["key_1"] test
  where
    test [(k, s)] scr = satisfy emptyChainState (signature k s) scr === Right (Script [pushSig s])

example2 :: TestTree
example2 = testExample E.example2 ["key_1", "key_2"] test
  where
    test xs scr = satisfy emptyChainState (context xs) scr === Right (expected xs)

    expected ((_, s) : _) = Script [OP_0, pushSig s]
    context (x : _) = uncurry signature x

example3 :: TestTree
example3 = testExample E.example3 ["key_likely", "key_unlikely"] test
  where
    test xs scr = satisfy emptyChainState (context xs) scr === Right (expected xs)

    expected (_ : (k, s) : _) = Script [pushSig s, pushKey k, OP_0]
    context (_ : x : _) = uncurry signature x

example4 :: TestTree
example4 = testExample E.example4 ["key_user", "key_service"] test
  where
    test xs scr = satisfy chainState (context xs) scr === Right (expected xs)

    expected ((_, s) : _) = Script [OP_0, pushSig s]
    context (x : _) = uncurry signature x

    chainState = ChainState{blockHeight = Nothing, utxoAge = Just 20000}

example5 :: TestTree
example5 = testExample E.example5 ["key_1", "key_2", "key_3"] test
  where
    test xs scr = result xs scr === Right (expected xs)

    expected [(_, s1), _, (_, s3)] = Script [OP_1, pushSig s3, OP_0, pushSig s1]
    result [x1, _, x3] scr = satisfy chainState (context x1 x3) scr
    context (k1, s1) (k3, s3) = signature k1 s1 <> signature k3 s3

    chainState = ChainState{blockHeight = Nothing, utxoAge = Just 13000}

example6 :: TestTree
example6 = testExample E.example6 ["key_local", "key_revocation"] test
  where
    test xs scr = result xs scr === Right (expected xs)

    expected [_, (_, s2)] = Script [pushSig s2, OP_0]
    result xs scr = satisfy chainState (context xs) scr
    context = foldMap (uncurry signature)

    chainState = ChainState{blockHeight = Nothing, utxoAge = Just 100}

hashBinding :: ByteString -> Miniscript -> Miniscript
hashBinding bs = let_ [("H", Bytes . encode $ ripemd160 bs)]

example7 :: TestTree
example7 = testExample E.example7 ["key_local", "key_remote", "key_revocation"] test
  where
    test xs scr = forAll (arbitraryBSn 32) $ \bs ->
        satisfy chainState (context xs bs) (hashBinding bs scr) === Right (expected xs bs)

    expected [_, (_, s), _] bs = Script [opPushData bs, OP_0, pushSig s, OP_0]
    context [_, x, _] bs = uncurry signature x <> preimage (encode $ ripemd160 bs) bs

    chainState = ChainState{blockHeight = Nothing, utxoAge = Just 2000}

example8 :: TestTree
example8 = testExample E.example8 ["key_local", "key_remote", "key_revocation"] test
  where
    test xs scr = forAll (arbitraryBSn 32) $ \bs ->
        satisfy chainState (context xs bs) (hashBinding bs scr) === Right (expected xs bs)

    expected [(kl, sl), (_, sr), _] bs = Script [opPushData bs, pushSig sl, pushKey kl, OP_1, pushSig sr]
    context [l, r, _] bs = satisfactionContext [(encode $ ripemd160 bs, bs)] [l, r]

    chainState = ChainState{blockHeight = Nothing, utxoAge = Just 6}

example9 :: TestTree
example9 = testExample E.example9 [] test
  where
    test _ scr = satisfy chainState mempty scr === Left Impossible
    chainState = ChainState{blockHeight = Nothing, utxoAge = Just 100}

example10 :: TestTree
example10 = testExample E.example10 ["A", "B", "C", "D", "E", "F", "G", "H"] test
  where
    test xs scr = satisfy chainState (context xs) scr === Right (expected xs)

    expected xs = Script $ OP_0 : drop 1 (foldMap pushPkhSig (reverse $ drop 5 xs)) <> replicate 5 OP_0
    context xs = foldMap (uncurry signature) . take 2 $ drop 5 xs

    chainState = ChainState{blockHeight = Nothing, utxoAge = Just 20000}

    pushPkhSig (k, s) = [pushSig s, pushKey k]
