def test_decrypt_contest_invalid_input_fails(
        self,
        contest_description: Tuple[str, ContestDescription],
        keypair: ElGamalKeyPair,
        nonce_seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = contest_description
        random = Random(random_seed)
        data = ballot_factory.get_random_contest_from(description, random)

        placeholders = generate_placeholder_selections_from(
            description, description.number_elected
        )
        description_with_placeholders = contest_description_with_placeholders_from(
            description, placeholders
        )

        self.assertTrue(description_with_placeholders.is_valid())

        # Act
        subject = encrypt_contest(
            data, description_with_placeholders, keypair.public_key, nonce_seed
        )
        self.assertIsNotNone(subject)

        # tamper with the nonce
        subject.nonce = int_to_q_unchecked(1)

        result_from_nonce = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            remove_placeholders=False,
        )
        result_from_nonce_seed = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            nonce_seed,
            remove_placeholders=False,
        )

        # Assert
        self.assertIsNone(result_from_nonce)
        self.assertIsNone(result_from_nonce_seed)

        # Tamper with the encryption
        subject.ballot_selections[0].message = ElGamalCiphertext(TWO_MOD_P, TWO_MOD_P)

        result_from_key_tampered = decrypt_contest_with_secret(
            subject,
            description_with_placeholders,
            keypair.public_key,
            keypair.secret_key,
            remove_placeholders=False,
        )
        result_from_nonce_tampered = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            remove_placeholders=False,
        )
        result_from_nonce_seed_tampered = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            nonce_seed,
            remove_placeholders=False,
        )

        # Assert
        self.assertIsNone(result_from_key_tampered)
        self.assertIsNone(result_from_nonce_tampered)
        self.assertIsNone(result_from_nonce_seed_tampered)
    def test_decrypt_contest_valid_input_succeeds(
        self,
        contest_description: Tuple[str, ContestDescription],
        keypair: ElGamalKeyPair,
        nonce_seed: ElementModQ,
        random_seed: int,
    ):

        # Arrange
        _, description = contest_description
        random = Random(random_seed)
        data = ballot_factory.get_random_contest_from(description, random)

        placeholders = generate_placeholder_selections_from(
            description, description.number_elected
        )
        description_with_placeholders = contest_description_with_placeholders_from(
            description, placeholders
        )

        self.assertTrue(description_with_placeholders.is_valid())

        # Act
        subject = encrypt_contest(
            data, description_with_placeholders, keypair.public_key, nonce_seed
        )
        self.assertIsNotNone(subject)

        # Decrypt the contest, but keep the placeholders
        # so we can verify the selection count matches as expected in the test
        result_from_key = decrypt_contest_with_secret(
            subject,
            description_with_placeholders,
            keypair.public_key,
            keypair.secret_key,
            remove_placeholders=False,
        )
        result_from_nonce = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            remove_placeholders=False,
        )
        result_from_nonce_seed = decrypt_contest_with_nonce(
            subject,
            description_with_placeholders,
            keypair.public_key,
            nonce_seed,
            remove_placeholders=False,
        )

        # Assert
        self.assertIsNotNone(result_from_key)
        self.assertIsNotNone(result_from_nonce)
        self.assertIsNotNone(result_from_nonce_seed)

        # The decrypted contest should include an entry for each possible selection
        # and placeholders for each seat
        expected_entries = (
            len(description.ballot_selections) + description.number_elected
        )
        self.assertTrue(
            result_from_key.is_valid(
                description.object_id,
                expected_entries,
                description.number_elected,
                description.votes_allowed,
            )
        )
        self.assertTrue(
            result_from_nonce.is_valid(
                description.object_id,
                expected_entries,
                description.number_elected,
                description.votes_allowed,
            )
        )
        self.assertTrue(
            result_from_nonce_seed.is_valid(
                description.object_id,
                expected_entries,
                description.number_elected,
                description.votes_allowed,
            )
        )

        # Assert the ballot selections sum to the expected number of selections
        key_selected = sum(
            [selection.to_int() for selection in result_from_key.ballot_selections]
        )
        nonce_selected = sum(
            [selection.to_int() for selection in result_from_nonce.ballot_selections]
        )
        seed_selected = sum(
            [
                selection.to_int()
                for selection in result_from_nonce_seed.ballot_selections
            ]
        )

        self.assertEqual(key_selected, nonce_selected)
        self.assertEqual(seed_selected, nonce_selected)
        self.assertEqual(description.number_elected, key_selected)

        # Assert each selection is valid
        for selection_description in description.ballot_selections:

            key_selection = [
                selection
                for selection in result_from_key.ballot_selections
                if selection.object_id == selection_description.object_id
            ][0]
            nonce_selection = [
                selection
                for selection in result_from_nonce.ballot_selections
                if selection.object_id == selection_description.object_id
            ][0]
            seed_selection = [
                selection
                for selection in result_from_nonce_seed.ballot_selections
                if selection.object_id == selection_description.object_id
            ][0]

            data_selections_exist = [
                selection
                for selection in data.ballot_selections
                if selection.object_id == selection_description.object_id
            ]

            # It's possible there are no selections in the original data collection
            # since it is valid to pass in a ballot that is not complete
            if any(data_selections_exist):
                self.assertTrue(
                    data_selections_exist[0].to_int() == key_selection.to_int()
                )
                self.assertTrue(
                    data_selections_exist[0].to_int() == nonce_selection.to_int()
                )
                self.assertTrue(
                    data_selections_exist[0].to_int() == seed_selection.to_int()
                )

            # TODO: also check edge cases such as:
            # - placeholder selections are true for under votes

            self.assertTrue(key_selection.is_valid(selection_description.object_id))
            self.assertTrue(nonce_selection.is_valid(selection_description.object_id))
            self.assertTrue(seed_selection.is_valid(selection_description.object_id))