content: {- |
  Module     : Sample.Proposal.Advance
  Maintainer : connor@mlabs.city
  Description: Generate sample data for testing the functionalities of advancing proposals
  
  Sample and utilities for testing the functionalities of advancing proposals.
  -}
  module Sample.Proposal.Advance (
    -- * Parameters
    ParameterBundle (..),
    GovernorParameters (..),
    AuthorityTokenParameters (..),
    ProposalParameters (..),
    StakeParameters (..),
    Winner (..),
  
    -- * Testing Utilities
    Validity (..),
    advance,
    mkTestTree,
    mkTestTree',
  
    -- * Parameter Bundles
    mkValidToNextStateBundle,
    mkValidToNextStateBundles,
    mkValidToFailedStateBundles,
    mkValidToFinishedInlineGATDatumBundles,
    mkInsufficientVotesBundle,
    mkAmbiguousWinnerBundle,
    mkFromFinishedBundles,
    mkInsufficientCosignsBundle,
    mkToNextStateTooLateBundles,
    mkMintGATsForWrongEffectsBundle,
    mkNoGATMintedBundle,
    mkGATsWithWrongDatumBundle,
    mkMintGATsWithoutTagBundle,
    mkBadGovernorOutputDatumBundle,
    mkUnexpectedOutputStakeBundles,
    mkFastforwardToFinishBundles,
    mkBadGovernorRedeemerBundle,
  ) where
  
  import Agora.Governor (
    Governor (..),
    GovernorDatum (..),
    GovernorRedeemer (CreateProposal, MintGATs),
   )
  import Agora.Proposal (
    ProposalDatum (..),
    ProposalEffectGroup,
    ProposalEffectMetadata (ProposalEffectMetadata),
    ProposalId (ProposalId),
    ProposalRedeemer (AdvanceProposal),
    ProposalStatus (..),
    ProposalThresholds (..),
    ProposalVotes (ProposalVotes),
    ResultTag (ResultTag),
    emptyVotesFor,
   )
  import Agora.Proposal.Time (
    ProposalStartingTime (ProposalStartingTime),
    ProposalTimingConfig (
      draftTime,
      executingTime,
      lockingTime,
      votingTime
    ),
   )
  import Agora.SafeMoney (AuthorityTokenTag, GTTag)
  import Agora.Stake (
    StakeDatum (..),
   )
  import Control.Applicative (liftA2)
  import Control.Monad.State (execState, modify, when)
  import Data.Default (def)
  import Data.List (singleton, sort)
  import Data.Map.Strict qualified as StrictMap
  import Data.Maybe (fromJust)
  import Data.Tagged (Tagged (Tagged), untag)
  import Plutarch.Context (
    input,
    mint,
    output,
    referenceInput,
    script,
    signedWith,
    timeRange,
    withDatum,
    withInlineDatum,
    withRedeemer,
    withRef,
    withValue,
   )
  import Plutarch.Extra.AssetClass (AssetClass (AssetClass), assetClassValue)
  import Plutarch.Extra.ScriptContext (scriptHashToTokenName)
  import Plutarch.Lift (PLifted, PUnsafeLiftDecl)
  import PlutusLedgerApi.V2 (
    Credential (PubKeyCredential),
    DatumHash,
    POSIXTime,
    POSIXTimeRange,
    PubKeyHash,
    ScriptHash,
    TxOutRef (TxOutRef),
   )
  import PlutusTx qualified
  import Sample.Proposal.Shared (
    governorTxRef,
    proposalTxRef,
    stakeTxRef,
   )
  import Sample.Shared (
    authorityTokenPolicy,
    authorityTokenSymbol,
    governor,
    governorAssetClass,
    governorScriptHash,
    governorValidator,
    minAda,
    proposalAssetClass,
    proposalScriptHash,
    proposalValidator,
    signer,
    stakeAssetClass,
    stakeScriptHash,
   )
  import Test.Specification (
    SpecificationTree,
    group,
    testPolicy,
    testValidator,
   )
  import Test.Util (
    CombinableBuilder,
    closedBoundedInterval,
    datumHash,
    groupsOfN,
    mkMinting,
    mkSpending,
    pubKeyHashes,
    scriptHashes,
    sortValue,
    toDatum,
    validatorHashes,
   )
  
  {- | A bunch of parameters that control the generation of the transaction
      context.
  -}
  data ParameterBundle = ParameterBundle
    { proposalParameters :: ProposalParameters
    -- ^ Parameters related to the the advancing proposal.
    , stakeParameters :: StakeParameters
    -- ^ Parameters related to stakes.
    , governorParameters :: Maybe GovernorParameters
    -- ^ Parameters related to GST moving. If set to 'Nothing', the GST won't
    --   be moved, thus the governor validator won't be run in 'mkTestTree'.
    , authorityTokenParameters :: [AuthorityTokenParameters]
    -- ^ Parameters related to GAT minting. If set to 'Nothing', no GAT will
    --     be minted, thus the GAT minting policy won't be run in 'mkTestTree'.
    , transactionTimeRange :: POSIXTimeRange
    -- ^ The value of 'TxInfo.txInfoValidRange', valid range of the generated
    --    transaction.
    , extraSignature :: Maybe PubKeyHash
    -- ^ An extra signator. Intended to be used when
    --    'StakeParametersstakeParameters.transactionSignedByOwners' is set to
    --    false.
    }
  
  -- | Everything about the generated governor stuff.
  data GovernorParameters = forall
      (redeemer :: Type)
      (predeemer :: PType).
    ( PUnsafeLiftDecl predeemer
    , PLifted predeemer ~ redeemer
    , PIsData predeemer
    , PlutusTx.ToData redeemer
    ) =>
    GovernorParameters
    { invalidGovernorOutputDatum :: Bool
    -- ^ The output governor datum will be changed.
    , governorRedeemer :: redeemer
    }
  
  -- | Everything about the generated authority token stuff.
  data AuthorityTokenParameters = forall
      (datum :: Type)
      (pdatum :: S -> Type).
    ( PUnsafeLiftDecl pdatum
    , PLifted pdatum ~ datum
    , PIsData pdatum
    ) =>
    AuthorityTokenParameters
    { mintGATsFor :: ScriptHash
    -- ^ GATs will be minted and sent to the given group of effects.
    , carryDatum :: Maybe datum
    -- ^ The datum that GAT UTxOs will be carrying.
    , carryAuthScript :: Maybe ScriptHash
    -- ^ The authentication script that GAT UTxOs link to through their token name.
    , invalidTokenName :: Bool
    -- ^ If set to true, GATs won't be tagged by their corresponding effect
    --    hashes.
    , shouldInlineDatum :: Bool
    -- ^ If set to true, the effect datum will be inlined.
    }
  
  -- | Represent the winning effect group(s).
  data Winner
    = -- | Only one effect at the given index has the highest votes.
      EffectAt Index
    | -- | All the effects have the same highest votes.
      All
  
  -- | Everything about the generated proposal stuff.
  data ProposalParameters = ProposalParameters
    { fromStatus :: ProposalStatus
    -- ^ What status is the proposal advancing from
    , toStatus :: ProposalStatus
    -- ^ What status is the proposal advancing to
    , effectList :: [ProposalEffectGroup]
    -- ^ The effect groups of the proposal. A neutral effect group is not
    --    required here.
    , winnerAndVotes :: Maybe (Winner, Integer)
    -- ^ Specify the effect group(s) that have the highest votes, and the value
    --    of the highest votes.
    , numCosigners :: NumStake
    -- ^ The number of cosigners.
    , invalidProposalOutputDatum :: Bool
    -- ^ Whether to make the proposal output datum invalid or not.
    }
  
  -- | Everything about the generated stake stuff.
  data StakeParameters = StakeParameters
    { numStake :: NumStake
    , perStakeGTs :: Tagged GTTag Integer
    , transactionSignedByOwners :: Bool
    }
  
  -- | Represent the number of stakes or the number of the cosigners.
  type NumStake = Int
  
  -- | Represent an index.
  type Index = Int
  
  {- | The validity of the generated transaction for variuos componets.
       'True' means valid, 'False' means invalid.
  -}
  data Validity = Validity
    { forProposalValidator :: Bool
    , forStakeValidator :: Bool
    , forGovernorValidator :: Maybe Bool
    , forAuthorityTokenPolicy :: Maybe Bool
    }
  
  --------------------------------------------------------------------------------
  
  -- * Proposal
  
  -- | Mock cosigners.
  mkCosigners :: NumStake -> [Credential]
  mkCosigners = sort . fmap PubKeyCredential . flip take pubKeyHashes
  
  -- | Allocate the result tag for the effect at the given index.
  outcomeIdxToResultTag :: Index -> ResultTag
  outcomeIdxToResultTag = ResultTag . fromIntegral
  
  -- | Add a neutral effect group and allocate result tags for the effect groups.
  mkEffects ::
    ProposalParameters ->
    StrictMap.Map ResultTag ProposalEffectGroup
  mkEffects ps =
    let resultTags = map ResultTag [0 ..]
        neutralEffect = StrictMap.empty
        finalEffects = ps.effectList <> [neutralEffect]
     in StrictMap.fromList $ zip resultTags finalEffects
  
  -- | Set the votes of the winning group(s).
  setWinner :: (Winner, Integer) -> ProposalVotes -> ProposalVotes
  setWinner (All, votes) (ProposalVotes m) =
    ProposalVotes $ StrictMap.mapMaybe (const $ Just votes) m
  setWinner (EffectAt winnerIdx, votes) (ProposalVotes m) =
    let winnerResultTag = outcomeIdxToResultTag winnerIdx
     in ProposalVotes $ StrictMap.adjust (const votes) winnerResultTag m
  
  -- | Mock votes for the proposal, given the parameters.
  mkVotes ::
    ProposalParameters ->
    ProposalVotes
  mkVotes ps =
    let effects = mkEffects ps
        emptyVotes = emptyVotesFor effects
     in maybe emptyVotes (`setWinner` emptyVotes) (ps.winnerAndVotes)
  
  -- | The starting time of every generated proposal.
  proposalStartingTime :: POSIXTime
  proposalStartingTime = 100
  
  -- | Create the input proposal datum given the parameters.
  mkProposalInputDatum :: ProposalParameters -> ProposalDatum
  mkProposalInputDatum ps =
    let effects = mkEffects ps
        votes = mkVotes ps
        st = ProposalStartingTime proposalStartingTime
     in ProposalDatum
          { proposalId = ProposalId 0
          , effects = effects
          , status = ps.fromStatus
          , cosigners = mkCosigners ps.numCosigners
          , thresholds = def
          , votes = votes
          , timingConfig = def
          , startingTime = st
          }
  
  -- | Create the output proposal datum given the parameters.
  mkProposalOutputDatum :: ProposalParameters -> ProposalDatum
  mkProposalOutputDatum ps =
    let inputDatum = mkProposalInputDatum ps
        outputCosigners =
          if ps.invalidProposalOutputDatum
            then []
            else inputDatum.cosigners
     in inputDatum
          { status = ps.toStatus
          , cosigners = outputCosigners
          }
  
  -- | Reference to the proposal UTXO.
  proposalRef :: TxOutRef
  proposalRef = TxOutRef proposalTxRef 1
  
  {- | Create a context builder that contains all the information about the
        input/output of the proposal validator, given the paramters.
  -}
  mkProposalBuilder :: forall b. CombinableBuilder b => ProposalParameters -> b
  mkProposalBuilder ps =
    let pst = assetClassValue proposalAssetClass 1
        value = sortValue $ minAda <> pst
     in mconcat
          [ input $
              mconcat
                [ script proposalScriptHash
                , withRef proposalRef
                , withDatum (mkProposalInputDatum ps)
                , withValue value
                ]
          , output $
              mconcat
                [ script proposalScriptHash
                , withDatum (mkProposalOutputDatum ps)
                , withValue value
                ]
          ]
  
  {- | The proposal redeemer used to spend the proposal UTXO, which is always
        'AdvanceProposal' in this case.
  -}
  proposalRedeemer :: ProposalRedeemer
  proposalRedeemer = AdvanceProposal
  
  --------------------------------------------------------------------------------
  
  -- * Stake
  
  -- Mock owners of the stakes.
  mkStakeOwners :: NumStake -> [Credential]
  mkStakeOwners = mkCosigners
  
  -- | Create the input stake datums given the parameters.
  mkStakeInputDatums :: StakeParameters -> [StakeDatum]
  mkStakeInputDatums ps =
    let template =
          StakeDatum
            { stakedAmount = ps.perStakeGTs
            , owner = PubKeyCredential ""
            , delegatedTo = Nothing
            , lockedBy = []
            }
     in (\owner -> template {owner = owner})
          <$> mkStakeOwners ps.numStake
  
  -- | Create the reference to a particular stake UTXO.
  mkStakeRef :: Index -> TxOutRef
  mkStakeRef = TxOutRef stakeTxRef . (+ 3) . fromIntegral
  
  {- | Create a context builder that contains all the inputs/outputs of the
        stake validator.
  -}
  mkStakeBuilder :: forall b. CombinableBuilder b => StakeParameters -> b
  mkStakeBuilder ps =
    let perStakeValue =
          sortValue $
            minAda
              <> assetClassValue stakeAssetClass 1
              <> assetClassValue
                governor.gtClassRef
                ps.perStakeGTs
        perStake idx i =
          let withSig =
                case (i.owner, ps.transactionSignedByOwners) of
                  (PubKeyCredential owner, True) -> signedWith owner
                  _ -> mempty
           in mconcat
                [ withSig
                , referenceInput $
                    mconcat
                      [ script stakeScriptHash
                      , withRef (mkStakeRef idx)
                      , withValue perStakeValue
                      , withInlineDatum i
                      ]
                ]
     in mconcat $
          zipWith
            perStake
            [0 :: Index ..]
            (mkStakeInputDatums ps)
  
  --------------------------------------------------------------------------------
  
  -- * Governor
  
  -- | The input governor datum.
  governorInputDatum :: GovernorDatum
  governorInputDatum =
    GovernorDatum
      { proposalThresholds = def
      , nextProposalId = ProposalId 42
      , proposalTimings = def
      , createProposalTimeRangeMaxWidth = def
      , maximumCreatedProposalsPerStake = 3
      }
  
  -- | Create the output governor datum given the parameters.
  mkGovernorOutputDatum :: GovernorParameters -> GovernorDatum
  mkGovernorOutputDatum ps =
    if ps.invalidGovernorOutputDatum
      then governorInputDatum {maximumCreatedProposalsPerStake = 15}
      else governorInputDatum
  
  -- | Reference to the governor UTXO.
  governorRef :: TxOutRef
  governorRef = TxOutRef governorTxRef 2
  
  {- | Create a context builder that contains the input and  the output of the
        governor validator.
  -}
  mkGovernorBuilder :: forall b. CombinableBuilder b => GovernorParameters -> b
  mkGovernorBuilder ps@(GovernorParameters _ redeemer) =
    let gst = assetClassValue governorAssetClass 1
        value = sortValue $ gst <> minAda
     in mconcat
          [ input $
              mconcat
                [ script governorScriptHash
                , withValue value
                , withRef governorRef
                , withDatum governorInputDatum
                , withRedeemer redeemer
                ]
          , output $
              mconcat
                [ script governorScriptHash
                , withValue value
                , withRef governorRef
                , withDatum (mkGovernorOutputDatum ps)
                ]
          ]
  
  --------------------------------------------------------------------------------
  
  -- * Authority Token
  
  {- | Create a context builder that contains the infomation about the minted
        authority tokens and where they're sent to.
  -}
  mkAuthorityTokenBuilder ::
    forall b.
    CombinableBuilder b =>
    AuthorityTokenParameters ->
    b
  mkAuthorityTokenBuilder ps@AuthorityTokenParameters {carryDatum, shouldInlineDatum} =
    let tn =
          case (ps.invalidTokenName, ps.carryAuthScript) of
            (True, Just _) -> "deadbeef"
            (True, Nothing) -> "deadbeef"
            (False, Just as) -> scriptHashToTokenName as
            (False, Nothing) -> ""
        ac = Tagged @AuthorityTokenTag $ AssetClass authorityTokenSymbol tn
        minted = assetClassValue ac 1
        value = sortValue $ minAda <> minted
        withDatum' = if shouldInlineDatum then withInlineDatum else withDatum
     in mconcat
          [ mint minted
          , output $
              mconcat
                [ script ps.mintGATsFor
                , maybe mempty withDatum' carryDatum
                , withValue value
                ]
          ]
  
  -- | The redeemer used while running the authority token policy.
  authorityTokenRedeemer :: ()
  authorityTokenRedeemer = ()
  
  --------------------------------------------------------------------------------
  
  -- | Create a 'TxInfo' that update the status of a proposal.
  advance ::
    forall b.
    CombinableBuilder b =>
    ParameterBundle ->
    b
  advance pb =
    let mkBuilderMaybe = maybe mempty
     in mconcat
          [ mkProposalBuilder pb.proposalParameters
          , mkStakeBuilder pb.stakeParameters
          , mkBuilderMaybe mkGovernorBuilder pb.governorParameters
          , foldMap mkAuthorityTokenBuilder pb.authorityTokenParameters
          , timeRange pb.transactionTimeRange
          , maybe mempty signedWith pb.extraSignature
          ]
  
  --------------------------------------------------------------------------------
  
  {- | Create a test tree that runs the relavant componets to test the advancing
        functionalities.
  -}
  mkTestTree ::
    String ->
    ParameterBundle ->
    Validity ->
    SpecificationTree
  mkTestTree name pb val =
    group name $ mconcat [proposal, governor, authority]
    where
      spend = mkSpending advance pb
  
      proposal =
        let proposalInputDatum = mkProposalInputDatum pb.proposalParameters
         in singleton $
              testValidator
                val.forProposalValidator
                "proposal"
                proposalValidator
                proposalInputDatum
                proposalRedeemer
                (spend proposalRef)
  
      governor =
        maybe
          []
          ( singleton
              . ( \(GovernorParameters _ governorRedeemer) ->
                    testValidator
                      (fromJust val.forGovernorValidator)
                      "governor"
                      governorValidator
                      governorInputDatum
                      governorRedeemer
                      (spend governorRef)
                )
          )
          (pb.governorParameters)
  
      authority = case pb.authorityTokenParameters of
        [] -> []
        _ ->
          singleton
            ( testPolicy
                (fromJust val.forAuthorityTokenPolicy)
                "authority"
                authorityTokenPolicy
                authorityTokenRedeemer
                (mkMinting advance pb authorityTokenSymbol)
            )
  
  {- | Create a test tree that runs a bunch of parameter bundles. These bundles
        should have the same validity.
  -}
  mkTestTree' ::
    String ->
    (ParameterBundle -> String) ->
    [ParameterBundle] ->
    Validity ->
    SpecificationTree
  mkTestTree' groupName mkCaseName bundles val =
    group groupName $
      (\b -> mkTestTree (mkCaseName b) b val)
        <$> bundles
  
  --------------------------------------------------------------------------------
  
  -- Utilities for creating parameter bundles
  
  {- | Given the proposal status, create a time range that is in time for
        advacing to the next state.
  -}
  mkInTimeTimeRange :: ProposalStatus -> POSIXTimeRange
  mkInTimeTimeRange advanceFrom =
    case advanceFrom of
      -- [S + 1, S + D - 1]
      Draft ->
        closedBoundedInterval
          (proposalStartingTime + 1)
          (proposalStartingTime + (def :: ProposalTimingConfig).draftTime - 1)
      -- [S + D + V + 1, S + D + V + L - 1]
      VotingReady ->
        closedBoundedInterval
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              + 1
          )
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              + (def :: ProposalTimingConfig).lockingTime
              - 1
          )
      -- [S + D + V + L + 1, S + + D + V + L + E - 1]
      Locked ->
        closedBoundedInterval
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              + (def :: ProposalTimingConfig).lockingTime
              + 1
          )
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              + (def :: ProposalTimingConfig).lockingTime
              + (def :: ProposalTimingConfig).executingTime
              - 1
          )
      Finished -> error "Cannot advance 'Finished' proposal"
  
  {- | Given the proposal status, create a time range that is too time for
        advacing to the next state.
  -}
  mkTooLateTimeRange :: ProposalStatus -> POSIXTimeRange
  mkTooLateTimeRange advanceFrom =
    case advanceFrom of
      -- [S + D + 1, S + D + V - 1]
      Draft ->
        closedBoundedInterval
          (proposalStartingTime + (def :: ProposalTimingConfig).draftTime + 1)
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              - 1
          )
      -- [S + D + V + L + 1, S + D + V + L + E -1]
      VotingReady ->
        closedBoundedInterval
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              + (def :: ProposalTimingConfig).lockingTime
              + 1
          )
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              + (def :: ProposalTimingConfig).lockingTime
              + (def :: ProposalTimingConfig).executingTime
              - 1
          )
      -- [S + D + V + L + E + 1, S + D + V + L + E + 100]
      Locked ->
        closedBoundedInterval
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              + (def :: ProposalTimingConfig).lockingTime
              + (def :: ProposalTimingConfig).executingTime
              + 1
          )
          ( proposalStartingTime
              + (def :: ProposalTimingConfig).draftTime
              + (def :: ProposalTimingConfig).votingTime
              + (def :: ProposalTimingConfig).lockingTime
              + (def :: ProposalTimingConfig).executingTime
              + 100
          )
      Finished -> error "Cannot advance 'Finished' proposal"
  
  -- | Next state of the given proposal status.
  getNextState :: ProposalStatus -> ProposalStatus
  getNextState = \case
    Draft -> VotingReady
    VotingReady -> Locked
    Locked -> Finished
    Finished -> error "Cannot advance 'Finished' proposal"
  
  -- | Calculate the number of GTs per stake in order to exceed the minimum limit.
  compPerStakeGTsForDraft :: NumStake -> Tagged GTTag Integer
  compPerStakeGTsForDraft nCosigners =
    Tagged $
      untag (def :: ProposalThresholds).toVoting
        `div` fromIntegral nCosigners
        + 1
  
  dummyDatum :: ()
  dummyDatum = ()
  
  dummyDatumHash :: DatumHash
  dummyDatumHash = datumHash $ toDatum dummyDatum
  
  -- | Create given number of effect groups. Each group will have 3 effects.
  mkMockEffects :: Bool -> Int -> [ProposalEffectGroup]
  mkMockEffects useAuthScript n = effects
    where
      effectsPerGroup = 3
  
      mkAuthScripts True = Just <$> scriptHashes
      mkAuthScripts False = repeat Nothing
      authScripts = mkAuthScripts useAuthScript
  
      datums = repeat dummyDatumHash
  
      effectMetadata = zipWith ProposalEffectMetadata datums authScripts
      effectScripts = validatorHashes
  
      effects =
        take n $
          StrictMap.fromList
            <$> groupsOfN
              effectsPerGroup
              (zip effectScripts effectMetadata)
  
  numberOfVotesThatJustMeetsTheMinimumRequirement :: Integer
  numberOfVotesThatJustMeetsTheMinimumRequirement =
    untag (def @ProposalThresholds).execute
  
  mkWinnerVotes :: Index -> (Winner, Integer)
  mkWinnerVotes idx =
    ( EffectAt idx
    , numberOfVotesThatJustMeetsTheMinimumRequirement
    )
  
  ambiguousWinnerVotes :: (Winner, Integer)
  ambiguousWinnerVotes =
    ( All
    , numberOfVotesThatJustMeetsTheMinimumRequirement
    )
  
  --------------------------------------------------------------------------------
  
  -- * Parameter Bundles
  
  ---
  
  -- * Legal
  
  defaultWinnerIdx :: Index
  defaultWinnerIdx = 0
  
  {- | Advance a proposal to the next state, perfectly valid for all the
      componets.
  -}
  mkValidToNextStateBundle ::
    -- | Number of cosigners.
    Word ->
    -- | Number of effects.
    Word ->
    -- | Toggle the referenc script in GAT UTXO.
    Bool ->
    -- | The initial proposal state, should not be 'Finished'.
    ProposalStatus ->
    ParameterBundle
  mkValidToNextStateBundle _ _ _ Finished =
    error "Cannot advance from Finished"
  mkValidToNextStateBundle nCosigners nEffects authScript from =
    let next = getNextState from
        effects = mkMockEffects authScript $ fromIntegral nEffects
        winner = defaultWinnerIdx
  
        template =
          ParameterBundle
            { proposalParameters =
                ProposalParameters
                  { fromStatus = from
                  , toStatus = next
                  , effectList = effects
                  , winnerAndVotes = Nothing
                  , numCosigners = fromIntegral nCosigners
                  , invalidProposalOutputDatum = False
                  }
            , stakeParameters =
                StakeParameters
                  { numStake = 0
                  , perStakeGTs =
                      compPerStakeGTsForDraft $
                        fromIntegral nCosigners
                  , transactionSignedByOwners = False
                  }
            , governorParameters = Nothing
            , authorityTokenParameters = []
            , transactionTimeRange = mkInTimeTimeRange from
            , extraSignature = Just signer
            }
  
        -- This is my favourite part of the test suite, lol.
        modifyTemplate = do
          when (from == Draft) $
            modify $ \b ->
              b
                { stakeParameters =
                    b.stakeParameters
                      { transactionSignedByOwners = True
                      , numStake = fromIntegral nCosigners
                      }
                , extraSignature = Nothing
                }
  
          when (from == VotingReady || from == Locked) $
            modify $ \b ->
              b
                { proposalParameters =
                    b.proposalParameters
                      { winnerAndVotes = Just $ mkWinnerVotes winner
                      }
                }
  
          when (from == Locked) $
            modify $ \b ->
              let aut =
                    StrictMap.elems $
                      StrictMap.mapWithKey
                        ( \vh (ProposalEffectMetadata _ authScript) ->
                            AuthorityTokenParameters
                              { mintGATsFor = vh
                              , carryDatum = Just dummyDatum
                              , carryAuthScript = authScript
                              , invalidTokenName = False
                              , shouldInlineDatum = False
                              }
                        )
                        (effects !! winner)
                  gov =
                    GovernorParameters
                      { invalidGovernorOutputDatum = False
                      , governorRedeemer = MintGATs
                      }
               in b
                    { governorParameters = Just gov
                    , authorityTokenParameters = aut
                    }
     in execState modifyTemplate template
  
  mkValidToNextStateBundles ::
    -- | Number of cosigners
    Word ->
    -- | Number of effects
    Word ->
    [ParameterBundle]
  mkValidToNextStateBundles nCosigners nEffects =
    liftA2
      (mkValidToNextStateBundle nCosigners nEffects)
      [True, False]
      [Draft, VotingReady, Locked]
  
  mkValidToFinishedInlineGATDatumBundles ::
    Word ->
    Word ->
    [ParameterBundle]
  mkValidToFinishedInlineGATDatumBundles nCosigners nEffects =
    let templates =
          liftA2
            (mkValidToNextStateBundle nCosigners nEffects)
            [True, False]
            [Locked]
  
        modifyTemplate template =
          template
            { authorityTokenParameters =
                modifyAuthorityParameters
                  <$> template.authorityTokenParameters
            }
  
        modifyAuthorityParameters params =
          params
            { shouldInlineDatum = True
            }
     in modifyTemplate <$> templates
  
  mkValidToFailedStateBundles ::
    -- | Number of cosigners
    Word ->
    -- | Number of effects
    Word ->
    [ParameterBundle]
  mkValidToFailedStateBundles nCosigners nEffects =
    liftA2
      mkBundle
      [True, False]
      [Draft, VotingReady, Locked]
    where
      mkBundle authScript from =
        let next = Finished
            effects = mkMockEffects authScript $ fromIntegral nEffects
         in ParameterBundle
              { proposalParameters =
                  ProposalParameters
                    { fromStatus = from
                    , toStatus = next
                    , effectList = effects
                    , winnerAndVotes = Nothing
                    , numCosigners = fromIntegral nCosigners
                    , invalidProposalOutputDatum = False
                    }
              , stakeParameters =
                  StakeParameters
                    { numStake = 0
                    , perStakeGTs =
                        compPerStakeGTsForDraft $
                          fromIntegral nCosigners
                    , transactionSignedByOwners = False
                    }
              , governorParameters = Nothing
              , authorityTokenParameters = []
              , transactionTimeRange = mkTooLateTimeRange from
              , extraSignature = Just signer
              }
  
  -- * Illegal
  
  mkFromFinishedBundles ::
    -- | Number of cosigners
    Word ->
    -- | Number of effects
    Word ->
    [ParameterBundle]
  mkFromFinishedBundles nCosigners nEffects =
    liftA2
      mkBundle
      [True, False]
      [Draft, VotingReady, Locked]
    where
      mkBundle authScript from =
        let template = mkValidToNextStateBundle nCosigners nEffects authScript from
         in template
              { proposalParameters =
                  template.proposalParameters
                    { fromStatus = Finished
                    , toStatus = Finished
                    }
              }
  
  mkToNextStateTooLateBundles :: Word -> Word -> [ParameterBundle]
  mkToNextStateTooLateBundles nCosigners nEffects =
    liftA2
      mkBundle
      [True, False]
      [Draft, VotingReady, Locked]
    where
      mkBundle authScript from =
        let template = mkValidToNextStateBundle nCosigners nEffects authScript from
         in template
              { transactionTimeRange = mkTooLateTimeRange from
              }
  
  mkUnexpectedOutputStakeBundles :: Word -> Word -> [ParameterBundle]
  mkUnexpectedOutputStakeBundles nCosigners nEffects =
    liftA2
      mkBundle
      [True, False]
      [VotingReady, Locked]
    where
      mkBundle authScript from =
        let template = mkValidToNextStateBundle nCosigners nEffects authScript from
         in template
              { stakeParameters =
                  template.stakeParameters
                    { numStake = 1
                    }
              }
  
  -- * From Draft
  
  mkInsufficientCosignsBundle :: Word -> Word -> ParameterBundle
  mkInsufficientCosignsBundle nCosigners nEffects =
    template
      { stakeParameters =
          template.stakeParameters
            { perStakeGTs = insuffcientPerStakeGTs
            }
      }
    where
      insuffcientPerStakeGTs =
        Tagged $
          untag (def :: ProposalThresholds).toVoting
            `div` fromIntegral nCosigners
            - 1
      template = mkValidToNextStateBundle nCosigners nEffects False Draft
  
  -- * From VotingReady
  
  setWinnerAndVotes ::
    ParameterBundle ->
    Maybe (Winner, Integer) ->
    ParameterBundle
  setWinnerAndVotes pb wv =
    pb
      { proposalParameters =
          pb.proposalParameters
            { winnerAndVotes = wv
            }
      }
  
  mkInsufficientVotesBundle ::
    Word ->
    Word ->
    ParameterBundle
  mkInsufficientVotesBundle nCosigners nEffects =
    mkValidToNextStateBundle nCosigners nEffects False VotingReady
      `setWinnerAndVotes` Nothing
  
  mkAmbiguousWinnerBundle ::
    Word ->
    Word ->
    ParameterBundle
  mkAmbiguousWinnerBundle nCosigners nEffects =
    mkValidToNextStateBundle nCosigners nEffects False VotingReady
      `setWinnerAndVotes` Just ambiguousWinnerVotes
  
  -- * From Locked
  
  mkValidFromLockedBundle :: Word -> Word -> ParameterBundle
  mkValidFromLockedBundle nCosigners nEffects =
    mkValidToNextStateBundle nCosigners nEffects False Locked
  
  mkMintGATsForWrongEffectsBundle ::
    Word ->
    Word ->
    ParameterBundle
  mkMintGATsForWrongEffectsBundle nCosigners nEffects =
    template
      { authorityTokenParameters =
          take 4 $
            zipWith
              (\a i -> a {mintGATsFor = validatorHashes !! i})
              template.authorityTokenParameters
              [1, 3 ..]
      }
    where
      template = mkValidFromLockedBundle nCosigners nEffects
  
  mkNoGATMintedBundle ::
    Word ->
    Word ->
    ParameterBundle
  mkNoGATMintedBundle nCosigners nEffects =
    template
      { authorityTokenParameters = []
      }
    where
      template = mkValidFromLockedBundle nCosigners nEffects
  
  mkMintGATsWithoutTagBundle ::
    Word ->
    Word ->
    ParameterBundle
  mkMintGATsWithoutTagBundle nCosigners nEffects =
    template
      { authorityTokenParameters =
          ( \aut ->
              aut
                { invalidTokenName = True
                }
          )
            <$> template.authorityTokenParameters
      }
    where
      template = mkValidFromLockedBundle nCosigners nEffects
  
  mkGATsWithWrongDatumBundle ::
    Word ->
    Word ->
    ParameterBundle
  mkGATsWithWrongDatumBundle nCosigners nEffects =
    template
      { authorityTokenParameters = newAut
      }
    where
      template = mkValidFromLockedBundle nCosigners nEffects
      newAut =
        ( \aut ->
            AuthorityTokenParameters
              aut.mintGATsFor
              (Just (1 :: Integer))
              aut.carryAuthScript
              False
              False
        )
          <$> template.authorityTokenParameters
  
  mkBadGovernorOutputDatumBundle ::
    Word ->
    Word ->
    ParameterBundle
  mkBadGovernorOutputDatumBundle nCosigners nEffects =
    template
      { governorParameters = Just gov
      }
    where
      template = mkValidFromLockedBundle nCosigners nEffects
      gov = GovernorParameters True MintGATs
  
  mkBadGovernorRedeemerBundle ::
    Word ->
    Word ->
    ParameterBundle
  mkBadGovernorRedeemerBundle nCosigners nEffects =
    template
      { governorParameters = Just gov
      }
    where
      template = mkValidFromLockedBundle nCosigners nEffects
      gov = GovernorParameters False CreateProposal
  
  mkFastforwardToFinishBundles ::
    Word ->
    Word ->
    [ParameterBundle]
  mkFastforwardToFinishBundles nCosigners nEffects = updateTemplate <$> templates
    where
      templates = mkValidToFailedStateBundles nCosigners nEffects
      mkMaliciousTimRange =
        let lb = proposalStartingTime - 1
            dub =
              1
                + proposalStartingTime
                + (def :: ProposalTimingConfig).draftTime
            vub =
              dub
                + (def :: ProposalTimingConfig).votingTime
                + (def :: ProposalTimingConfig).lockingTime
            lub =
              vub
                + (def :: ProposalTimingConfig).executingTime
            go Draft = (lb, dub)
            go VotingReady = (lb, vub)
            go Locked = (lb, lub)
            go Finished = error "cannot advance from Finished"
         in uncurry closedBoundedInterval . go
      updateTemplate template =
        template
          { transactionTimeRange =
              mkMaliciousTimRange template.proposalParameters.fromStatus
          }
  