"""Test OOB Manager."""

import base64
import json
from copy import deepcopy
from datetime import datetime, timedelta, timezone
from typing import List
from unittest import IsolatedAsyncioTestCase
from unittest.mock import ANY

from .....connections.models.conn_record import ConnRecord
from .....connections.models.connection_target import ConnectionTarget
from .....connections.models.diddoc import DIDDoc, PublicKey, PublicKeyType, Service
from .....core.event_bus import EventBus
from .....core.oob_processor import OobMessageProcessor
from .....did.did_key import DIDKey
from .....messaging.decorators.attach_decorator import AttachDecorator
from .....messaging.decorators.service_decorator import ServiceDecorator
from .....messaging.responder import BaseResponder, MockResponder
from .....messaging.util import datetime_now, datetime_to_str, str_to_epoch
from .....multitenant.base import BaseMultitenantManager
from .....multitenant.manager import MultitenantManager
from .....storage.error import StorageNotFoundError
from .....tests import mock
from .....transport.inbound.receipt import MessageReceipt
from .....utils.testing import create_test_profile
from .....wallet.askar import AskarWallet
from .....wallet.did_info import DIDInfo, KeyInfo
from .....wallet.did_method import SOV
from .....wallet.key_type import ED25519
from .....wallet.util import pad
from ....coordinate_mediation.v1_0.manager import MediationManager
from ....coordinate_mediation.v1_0.models.mediation_record import MediationRecord
from ....coordinate_mediation.v1_0.route_manager import RouteManager
from ....didcomm_prefix import DIDCommPrefix
from ....didexchange.v1_0.manager import DIDXManager
from ....issue_credential.v1_0.message_types import CREDENTIAL_OFFER
from ....issue_credential.v1_0.messages.credential_offer import (
    CredentialOffer as V10CredOffer,
)
from ....issue_credential.v1_0.messages.inner.credential_preview import (
    CredAttrSpec as V10CredAttrSpec,
)
from ....issue_credential.v1_0.messages.inner.credential_preview import (
    CredentialPreview as V10CredentialPreview,
)
from ....issue_credential.v1_0.models.credential_exchange import V10CredentialExchange
from ....issue_credential.v1_0.tests import INDY_OFFER
from ....issue_credential.v2_0.message_types import (
    ATTACHMENT_FORMAT as V20_CRED_ATTACH_FORMAT,
)
from ....issue_credential.v2_0.message_types import CRED_20_OFFER
from ....issue_credential.v2_0.messages.cred_format import V20CredFormat
from ....issue_credential.v2_0.messages.cred_offer import V20CredOffer
from ....issue_credential.v2_0.messages.inner.cred_preview import (
    V20CredAttrSpec,
    V20CredPreview,
)
from ....present_proof.v1_0.message_types import ATTACH_DECO_IDS as V10_PRES_ATTACH_FORMAT
from ....present_proof.v1_0.message_types import PRESENTATION_REQUEST
from ....present_proof.v1_0.messages.presentation_request import PresentationRequest
from ....present_proof.v2_0.message_types import (
    ATTACHMENT_FORMAT as V20_PRES_ATTACH_FORMAT,
)
from ....present_proof.v2_0.message_types import PRES_20_REQUEST
from ....present_proof.v2_0.messages.pres_format import V20PresFormat
from ....present_proof.v2_0.messages.pres_request import V20PresRequest
from .. import manager as test_module
from ..manager import (
    REUSE_ACCEPTED_WEBHOOK_TOPIC,
    REUSE_WEBHOOK_TOPIC,
    OutOfBandManager,
    OutOfBandManagerError,
)
from ..messages.invitation import HSProto, InvitationMessage
from ..messages.invitation import Service as OobService
from ..messages.problem_report import ProblemReport, ProblemReportReason
from ..messages.reuse import HandshakeReuse
from ..messages.reuse_accept import HandshakeReuseAccept
from ..models.invitation import InvitationRecord
from ..models.oob_record import OobRecord


class TestConfig:
    test_did = "55GkHamhTU1ZbTbV2ab9DE"
    test_verkey = "3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRx"
    test_endpoint = "http://localhost"
    test_target_did = "GbuDUYXaUZRfHD2jeDuQuP"
    their_public_did = "55GkHamhTU1ZbTbV2ab9DE"
    test_service = OobService(
        recipient_keys=[test_verkey],
        routing_keys=[],
        service_endpoint=test_endpoint,
    )
    NOW_8601 = datetime.now(tz=timezone.utc).isoformat(" ", "seconds")
    TEST_INVI_MESSAGE_TYPE = "out-of-band/1.1/invitation"
    NOW_EPOCH = str_to_epoch(NOW_8601)
    CD_ID = "GMm4vMw8LLrLJjp81kRRLp:3:CL:12:tag"
    INDY_PROOF_REQ = json.loads(
        f"""{{
        "name": "proof-req",
        "version": "1.0",
        "nonce": "12345",
        "requested_attributes": {{
            "0_player_uuid": {{
                "name": "player",
                "restrictions": [
                    {{
                        "cred_def_id": "{CD_ID}"
                    }}
                ],
                "non_revoked": {{
                    "from": {NOW_EPOCH},
                    "to": {NOW_EPOCH}
                }}
            }},
            "0_screencapture_uuid": {{
                "name": "screenCapture",
                "restrictions": [
                    {{
                        "cred_def_id": "{CD_ID}"
                    }}
                ],
                "non_revoked": {{
                    "from": {NOW_EPOCH},
                    "to": {NOW_EPOCH}
                }}
            }}
        }},
        "requested_predicates": {{
            "0_highscore_GE_uuid": {{
                "name": "highScore",
                "p_type": ">=",
                "p_value": 1000000,
                "restrictions": [
                    {{
                        "cred_def_id": "{CD_ID}"
                    }}
                ],
                "non_revoked": {{
                    "from": {NOW_EPOCH},
                    "to": {NOW_EPOCH}
                }}
            }}
        }}
    }}"""
    )
    DIF_PROOF_REQ = {
        "presentation_definition": {
            "id": "32f54163-7166-48f1-93d8-ff217bdb0654",
            "submission_requirements": [
                {
                    "name": "Citizenship Information",
                    "rule": "pick",
                    "min": 1,
                    "from": "A",
                }
            ],
            "input_descriptors": [
                {
                    "id": "citizenship_input_1",
                    "name": "EU Driver's License",
                    "group": ["A"],
                    "schema": [
                        {
                            "uri": "https://www.w3.org/2018/credentials#VerifiableCredential"
                        }
                    ],
                    "constraints": {
                        "limit_disclosure": "required",
                        "fields": [
                            {
                                "path": ["$.credentialSubject.givenName"],
                                "purpose": "The claim must be from one of the specified issuers",
                                "filter": {
                                    "type": "string",
                                    "enum": ["JOHN", "CAI"],
                                },
                            }
                        ],
                    },
                }
            ],
        },
    }

    PRES_REQ_V1 = PresentationRequest(
        comment="Test",
        request_presentations_attach=[
            AttachDecorator.data_base64(
                mapping=INDY_PROOF_REQ,
                ident=V10_PRES_ATTACH_FORMAT[PRESENTATION_REQUEST],
            )
        ],
    )
    pres_req_dict = PRES_REQ_V1.request_presentations_attach[0].serialize()
    req_attach_v1 = {
        "@id": "request-0",
        "mime-type": "application/json",
        "data": {
            "json": {
                "@type": DIDCommPrefix.qualify_current(PRESENTATION_REQUEST),
                "@id": "12345678-0123-4567-1234-567812345678",
                "comment": "some comment",
                "request_presentations~attach": [pres_req_dict],
            }
        },
    }

    PRES_REQ_V2 = V20PresRequest(
        comment="some comment",
        will_confirm=True,
        formats=[
            V20PresFormat(
                attach_id="indy",
                format_=V20_PRES_ATTACH_FORMAT[PRES_20_REQUEST][
                    V20PresFormat.Format.INDY.api
                ],
            )
        ],
        request_presentations_attach=[
            AttachDecorator.data_base64(mapping=INDY_PROOF_REQ, ident="indy")
        ],
    )

    DIF_PRES_REQ_V2 = V20PresRequest(
        comment="some comment",
        will_confirm=True,
        formats=[
            V20PresFormat(
                attach_id="dif",
                format_=V20_PRES_ATTACH_FORMAT[PRES_20_REQUEST][
                    V20PresFormat.Format.DIF.api
                ],
            )
        ],
        request_presentations_attach=[
            AttachDecorator.data_json(mapping=DIF_PROOF_REQ, ident="dif")
        ],
    )

    CRED_OFFER_V1 = V10CredOffer(
        credential_preview=V10CredentialPreview(
            attributes=(
                V10CredAttrSpec(name="legalName", value="value"),
                V10CredAttrSpec(name="jurisdictionId", value="value"),
                V10CredAttrSpec(name="incorporationDate", value="value"),
            )
        ),
        offers_attach=[V10CredOffer.wrap_indy_offer(INDY_OFFER)],
    )

    CRED_OFFER_V2 = V20CredOffer(
        credential_preview=V20CredPreview(
            attributes=V20CredAttrSpec.list_plain(
                {
                    "legalName": "value",
                    "jurisdictionId": "value",
                    "incorporationDate": "value",
                }
            ),
        ),
        formats=[
            V20CredFormat(
                attach_id="indy",
                format_=V20_CRED_ATTACH_FORMAT[CRED_20_OFFER][
                    V20CredFormat.Format.INDY.api
                ],
            )
        ],
        offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="indy")],
    )

    req_attach_v2 = AttachDecorator.data_json(
        mapping=PRES_REQ_V2.serialize(),
        ident="request-0",
    ).serialize()

    indy_cred_req = {
        "schema_id": f"{test_did}:2:bc-reg:1.0",
        "cred_def_id": f"{test_did}:3:CL:12:tag1",
    }
    cred_req_meta = {}

    def make_did_doc(self, did, verkey):
        doc = DIDDoc(did=did)
        controller = did
        ident = "1"
        pk_value = verkey
        pk = PublicKey(
            did, ident, pk_value, PublicKeyType.ED25519_SIG_2018, controller, False
        )
        doc.set(pk)
        recip_keys = [pk]
        router_keys = []
        service = Service(
            did, "indy", "IndyAgent", recip_keys, router_keys, TestConfig.test_endpoint
        )
        doc.set(service)
        return doc


class TestOOBManager(IsolatedAsyncioTestCase, TestConfig):
    async def asyncSetUp(self):
        self.responder = MockResponder()
        self.responder.send = mock.CoroutineMock()

        self.test_mediator_routing_keys = ["3Dn1SJNPaCXcvvJvSbsFWP2xaCjMom3can8CQNhWrTRR"]
        self.test_mediator_conn_id = "mediator-conn-id"
        self.test_mediator_endpoint = "http://mediator.example.com"

        self.route_manager = mock.MagicMock(RouteManager, autospec=True)
        self.route_manager.routing_info = mock.CoroutineMock(
            return_value=(self.test_mediator_routing_keys, self.test_mediator_endpoint)
        )

        self.profile = await create_test_profile(
            {
                "default_endpoint": TestConfig.test_endpoint,
                "default_label": "This guy",
                "additional_endpoints": ["http://aries.ca/another-endpoint"],
                "debug.auto_accept_invites": True,
                "debug.auto_accept_requests": True,
            }
        )

        self.profile.context.injector.bind_instance(RouteManager, self.route_manager)

        self.profile.context.injector.bind_instance(BaseResponder, self.responder)

        self.profile.context.injector.bind_instance(
            EventBus, mock.MagicMock(EventBus, autospec=True)
        )
        self.multitenant_mgr = mock.MagicMock(MultitenantManager, autospec=True)
        self.profile.context.injector.bind_instance(
            BaseMultitenantManager, self.multitenant_mgr
        )

        self.manager = OutOfBandManager(self.profile)
        assert self.manager.profile
        self.manager.resolve_invitation = mock.CoroutineMock()
        self.manager.resolve_invitation.return_value = (
            TestConfig.test_endpoint,
            [TestConfig.test_verkey],
            [],
        )

        self.test_conn_rec = mock.MagicMock(
            connection_id="dummy",
            my_did=TestConfig.test_did,
            their_did=TestConfig.test_target_did,
            their_role=ConnRecord.Role.REQUESTER,
            state=ConnRecord.State.COMPLETED,
            their_public_did=self.their_public_did,
            save=mock.CoroutineMock(),
        )

    async def test_create_invitation_handshake_succeeds(self):
        self.profile.context.update_settings({"public_invites": True})

        with mock.patch.object(
            AskarWallet, "get_public_did", autospec=True
        ) as mock_wallet_get_public_did:
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            invi_rec = await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                public=True,
                hs_protos=[HSProto.RFC23],
            )

            assert invi_rec.invitation._type == DIDCommPrefix.qualify_current(
                self.TEST_INVI_MESSAGE_TYPE
            )
            assert not invi_rec.invitation.requests_attach
            assert (
                DIDCommPrefix.qualify_current(HSProto.RFC23.name)
                in invi_rec.invitation.handshake_protocols
            )
            assert invi_rec.invitation.services == [f"did:sov:{TestConfig.test_did}"]

    async def test_create_invitation_multitenant_local(self):
        self.profile.context.update_settings(
            {
                "multitenant.enabled": True,
                "wallet.id": "test_wallet",
            }
        )

        with (
            mock.patch.object(
                AskarWallet, "create_signing_key", autospec=True
            ) as mock_wallet_create_signing_key,
            mock.patch.object(
                self.multitenant_mgr, "get_default_mediator"
            ) as mock_get_default_mediator,
        ):
            mock_wallet_create_signing_key.return_value = KeyInfo(
                TestConfig.test_verkey, None, ED25519
            )
            mock_get_default_mediator.return_value = MediationRecord()
            await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                hs_protos=[HSProto.RFC23],
                multi_use=False,
            )

            self.route_manager.route_invitation.assert_called_once()

    async def test_create_invitation_multitenant_public(self):
        self.profile.context.update_settings(
            {
                "multitenant.enabled": True,
                "wallet.id": "test_wallet",
                "public_invites": True,
            }
        )

        with mock.patch.object(
            AskarWallet, "get_public_did", autospec=True
        ) as mock_wallet_get_public_did:
            mock_wallet_get_public_did.return_value = DIDInfo(
                self.test_did,
                self.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            await self.manager.create_invitation(
                hs_protos=[HSProto.RFC23],
                public=True,
                multi_use=False,
            )

            self.route_manager.route_invitation.assert_called_once()

    async def test_create_invitation_mediation_overwrites_routing_and_endpoint(self):
        async with self.profile.session() as session:
            mock_conn_rec = mock.MagicMock()

            mediation_record = MediationRecord(
                role=MediationRecord.ROLE_CLIENT,
                state=MediationRecord.STATE_GRANTED,
                connection_id=self.test_mediator_conn_id,
                routing_keys=self.test_mediator_routing_keys,
                endpoint=self.test_mediator_endpoint,
            )
            await mediation_record.save(session)
            with (
                mock.patch.object(
                    MediationManager,
                    "get_default_mediator_id",
                ) as mock_get_default_mediator,
                mock.patch.object(mock_conn_rec, "metadata_set", mock.CoroutineMock()),
            ):
                invite = await self.manager.create_invitation(
                    my_endpoint=TestConfig.test_endpoint,
                    my_label="test123",
                    hs_protos=[HSProto.RFC23],
                    mediation_id=mediation_record.mediation_id,
                )
                assert isinstance(invite, InvitationRecord)
                assert invite.invitation._type == DIDCommPrefix.qualify_current(
                    self.TEST_INVI_MESSAGE_TYPE
                )
                assert invite.invitation.label == "test123"
                assert (
                    DIDKey.from_did(
                        invite.invitation.services[0].routing_keys[0]
                    ).public_key_b58
                    == self.test_mediator_routing_keys[0]
                )
                assert (
                    invite.invitation.services[0].service_endpoint
                    == self.test_mediator_endpoint
                )
                mock_get_default_mediator.assert_not_called()

    async def test_create_invitation_no_handshake_no_attachments_x(self):
        with self.assertRaises(OutOfBandManagerError) as context:
            await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                public=True,
                hs_protos=None,
                multi_use=False,
            )
        assert "Invitation must include" in str(context.exception)

    async def test_create_invitation_attachment_v1_0_cred_offer(self):
        self.profile.context.update_settings({"public_invites": True})
        with (
            mock.patch.object(
                AskarWallet, "get_public_did", autospec=True
            ) as mock_wallet_get_public_did,
            mock.patch.object(
                V10CredentialExchange,
                "retrieve_by_id",
                mock.CoroutineMock(),
            ) as mock_retrieve_cxid,
        ):
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            mock_retrieve_cxid.return_value = mock.MagicMock(
                credential_offer_dict=self.CRED_OFFER_V1
            )
            invi_rec = await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                public=True,
                hs_protos=[HSProto.RFC23],
                multi_use=False,
                attachments=[{"type": "credential-offer", "id": "dummy-id"}],
            )

            mock_retrieve_cxid.assert_called_once_with(ANY, "dummy-id")
            assert isinstance(invi_rec, InvitationRecord)
            assert invi_rec.invitation.handshake_protocols
            assert invi_rec.invitation.requests_attach[0].content[
                "@type"
            ] == DIDCommPrefix.qualify_current(CREDENTIAL_OFFER)

    async def test_create_invitation_attachment_v1_0_cred_offer_no_handshake(self):
        self.profile.context.update_settings({"public_invites": True})
        with (
            mock.patch.object(
                AskarWallet, "get_public_did", autospec=True
            ) as mock_wallet_get_public_did,
            mock.patch.object(
                V10CredentialExchange,
                "retrieve_by_id",
                mock.CoroutineMock(),
            ) as mock_retrieve_cxid,
        ):
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            mock_retrieve_cxid.return_value = mock.MagicMock(
                credential_offer_dict=self.CRED_OFFER_V1
            )
            invi_rec = await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                public=True,
                hs_protos=None,
                multi_use=False,
                attachments=[{"type": "credential-offer", "id": "dummy-id"}],
            )

            mock_retrieve_cxid.assert_called_once_with(ANY, "dummy-id")
            assert isinstance(invi_rec, InvitationRecord)
            assert not invi_rec.invitation.handshake_protocols
            assert invi_rec.invitation.requests_attach[0].content == {
                **self.CRED_OFFER_V1.serialize(),
                "~thread": {"pthid": invi_rec.invi_msg_id},
            }

    async def test_create_invitation_attachment_v2_0_cred_offer(self):
        with (
            mock.patch.object(
                AskarWallet, "get_public_did", autospec=True
            ) as mock_wallet_get_public_did,
            mock.patch.object(
                test_module.V10CredentialExchange,
                "retrieve_by_id",
                mock.CoroutineMock(),
            ) as mock_retrieve_cxid_v1,
            mock.patch.object(
                test_module.V20CredExRecord,
                "retrieve_by_id",
                mock.CoroutineMock(),
            ) as mock_retrieve_cxid_v2,
        ):
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            mock_retrieve_cxid_v1.side_effect = test_module.StorageNotFoundError()
            mock_retrieve_cxid_v2.return_value = mock.MagicMock(cred_offer=V20CredOffer())
            invi_rec = await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                public=False,
                hs_protos=None,
                multi_use=False,
                attachments=[{"type": "credential-offer", "id": "dummy-id"}],
            )

            mock_retrieve_cxid_v2.assert_called_once_with(ANY, "dummy-id")
            assert isinstance(invi_rec, InvitationRecord)
            assert not invi_rec.invitation.handshake_protocols
            attach = invi_rec.invitation.requests_attach[0].content
            assert isinstance(attach, dict)
            assert "~thread" in attach and "pthid" in attach["~thread"]
            assert attach["~thread"]["pthid"] == invi_rec.invi_msg_id

    async def test_create_invitation_attachment_present_proof_v1_0(self):
        self.profile.context.update_settings({"public_invites": True})
        with (
            mock.patch.object(
                AskarWallet, "get_public_did", autospec=True
            ) as mock_wallet_get_public_did,
            mock.patch.object(
                test_module.V10PresentationExchange,
                "retrieve_by_id",
                mock.CoroutineMock(),
            ) as mock_retrieve_pxid,
        ):
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            mock_retrieve_pxid.return_value = mock.MagicMock(
                presentation_request_dict=self.PRES_REQ_V1
            )
            invi_rec = await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                public=True,
                hs_protos=[test_module.HSProto.RFC23],
                multi_use=False,
                attachments=[{"type": "present-proof", "id": "dummy-id"}],
            )

            mock_retrieve_pxid.assert_called_once_with(ANY, "dummy-id")
            assert isinstance(invi_rec, InvitationRecord)
            assert invi_rec.invitation.handshake_protocols
            assert invi_rec.invitation.requests_attach[0].content == {
                **self.PRES_REQ_V1.serialize(),
                "~thread": {"pthid": invi_rec.invi_msg_id},
            }

    async def test_create_invitation_attachment_present_proof_v2_0(self):
        self.profile.context.update_settings({"public_invites": True})
        with (
            mock.patch.object(
                AskarWallet, "get_public_did", autospec=True
            ) as mock_wallet_get_public_did,
            mock.patch.object(
                test_module.V10PresentationExchange,
                "retrieve_by_id",
                mock.CoroutineMock(),
            ) as mock_retrieve_pxid_1,
            mock.patch.object(
                test_module.V20PresExRecord,
                "retrieve_by_id",
                mock.CoroutineMock(),
            ) as mock_retrieve_pxid_2,
        ):
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            mock_retrieve_pxid_1.side_effect = StorageNotFoundError()
            mock_retrieve_pxid_2.return_value = mock.MagicMock(
                pres_request=TestConfig.PRES_REQ_V2
            )
            invi_rec = await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                public=True,
                hs_protos=[test_module.HSProto.RFC23],
                multi_use=False,
                attachments=[{"type": "present-proof", "id": "dummy-id"}],
            )

            mock_retrieve_pxid_2.assert_called_once_with(ANY, "dummy-id")
            assert isinstance(invi_rec, InvitationRecord)
            assert invi_rec.invitation.handshake_protocols
            assert invi_rec.invitation.requests_attach[0].content == {
                **TestConfig.PRES_REQ_V2.serialize(),
                "~thread": {"pthid": invi_rec.invi_msg_id},
            }

    async def test_create_invitation_public_x_no_public_invites(self):
        self.profile.context.update_settings({"public_invites": False})

        with self.assertRaises(OutOfBandManagerError) as context:
            await self.manager.create_invitation(
                public=True,
                my_endpoint="testendpoint",
                hs_protos=[test_module.HSProto.RFC23],
                multi_use=False,
            )
        assert "Public invitations are not enabled" in str(context.exception)

    async def test_create_invitation_public_x_multi_use(self):
        self.profile.context.update_settings({"public_invites": True})

        with self.assertRaises(OutOfBandManagerError) as context:
            await self.manager.create_invitation(
                public=True,
                my_endpoint="testendpoint",
                hs_protos=[test_module.HSProto.RFC23],
                multi_use=True,
            )
        assert "Cannot create public invitation with" in str(context.exception)

    async def test_create_invitation_requests_attach_x_multi_use(self):
        with self.assertRaises(OutOfBandManagerError) as context:
            await self.manager.create_invitation(
                public=False,
                my_endpoint="testendpoint",
                hs_protos=[test_module.HSProto.RFC23],
                attachments=[{"some": "attachment"}],
                multi_use=True,
            )
        assert "Cannot create multi use invitation with attachments" in str(
            context.exception
        )

    async def test_create_invitation_public_x_no_public_did(self):
        self.profile.context.update_settings({"public_invites": True})

        with mock.patch.object(
            AskarWallet, "get_public_did", autospec=True
        ) as mock_wallet_get_public_did:
            mock_wallet_get_public_did.return_value = None
            with self.assertRaises(OutOfBandManagerError) as context:
                await self.manager.create_invitation(
                    public=True,
                    my_endpoint="testendpoint",
                    hs_protos=[test_module.HSProto.RFC23],
                    multi_use=False,
                )
            assert "Cannot create public invitation with no public DID" in str(
                context.exception
            )

    async def test_create_invitation_attachment_x(self):
        self.profile.context.update_settings({"public_invites": True})
        with mock.patch.object(
            AskarWallet, "get_public_did", autospec=True
        ) as mock_wallet_get_public_did:
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            with self.assertRaises(OutOfBandManagerError) as context:
                await self.manager.create_invitation(
                    my_endpoint=TestConfig.test_endpoint,
                    public=False,
                    hs_protos=[test_module.HSProto.RFC23],
                    multi_use=False,
                    attachments=[
                        {
                            "type": "asdf",
                            "id": "asdf",
                            "having": "attachment",
                            "is": "no",
                            "good": "here",
                        }
                    ],
                )
            assert "Unknown attachment type" in str(context.exception)

    async def test_create_invitation_peer_did(self):
        async with self.profile.session() as session:
            self.profile.context.update_settings(
                {
                    "multitenant.enabled": True,
                    "wallet.id": "my-wallet",
                }
            )
            mediation_record = MediationRecord(
                role=MediationRecord.ROLE_CLIENT,
                state=MediationRecord.STATE_GRANTED,
                connection_id=self.test_mediator_conn_id,
                routing_keys=self.test_mediator_routing_keys,
                endpoint=self.test_mediator_endpoint,
            )
            await mediation_record.save(session)
            with mock.patch.object(
                self.multitenant_mgr, "get_default_mediator"
            ) as mock_get_default_mediator:
                mock_get_default_mediator.return_value = mediation_record
                invi_rec = await self.manager.create_invitation(
                    my_label="That guy",
                    my_endpoint=None,
                    public=False,
                    hs_protos=[test_module.HSProto.RFC23],
                    multi_use=False,
                    service_accept=["didcomm/aip1", "didcomm/aip2;env=rfc19"],
                )

                assert invi_rec._invitation.ser["@type"] == DIDCommPrefix.qualify_current(
                    self.TEST_INVI_MESSAGE_TYPE
                )
                assert not invi_rec._invitation.ser.get("requests~attach")
                assert invi_rec.invitation.label == "That guy"
                assert (
                    DIDCommPrefix.qualify_current(HSProto.RFC23.name)
                    in invi_rec.invitation.handshake_protocols
                )
                service = invi_rec._invitation.ser["services"][0]
                assert service["id"] == "#inline"
                assert service["type"] == "did-communication"
                assert len(service["recipientKeys"]) == 1
                assert (
                    service["routingKeys"][0]
                    == DIDKey.from_public_key_b58(
                        self.test_mediator_routing_keys[0], ED25519
                    ).key_id
                )
                assert service["serviceEndpoint"] == self.test_mediator_endpoint

    async def test_create_invitation_metadata_assigned(self):
        async with self.profile.session() as session:
            invi_rec = await self.manager.create_invitation(
                hs_protos=[test_module.HSProto.RFC23],
                metadata={"hello": "world"},
            )
            service = invi_rec._invitation.ser["services"][0]
            invitation_key = DIDKey.from_did(service["recipientKeys"][0]).public_key_b58
            record = await ConnRecord.retrieve_by_invitation_key(session, invitation_key)
            assert await record.metadata_get_all(session) == {"hello": "world"}

    async def test_create_invitation_x_public_metadata(self):
        self.profile.context.update_settings({"public_invites": True})
        with mock.patch.object(
            AskarWallet, "get_public_did", autospec=True
        ) as mock_wallet_get_public_did:
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            with self.assertRaises(OutOfBandManagerError) as context:
                await self.manager.create_invitation(
                    public=False,
                    hs_protos=[],
                    attachments=[{"an": "attachment"}],
                    metadata={"hello": "world"},
                    multi_use=False,
                )
            assert "Cannot store metadata without handshake protocols" in str(
                context.exception
            )

    async def test_create_invitation_goal_code(self):
        self.profile.context.update_settings({"public_invites": True})
        with mock.patch.object(
            AskarWallet, "get_public_did", autospec=True
        ) as mock_wallet_get_public_did:
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            goal = "To issue a Faber College Graduate credential"
            goal_code = "issue-vc"
            my_label = "Invitation to Barry"

            invi_rec = await self.manager.create_invitation(
                public=False,
                my_label=my_label,
                protocol_version="1.1",
                alias="Test Alias",
                service_accept=["didcomm/aip1", "didcomm/aip2;env=rfc19"],
                hs_protos=[test_module.HSProto.RFC23],
                goal=goal,
                goal_code=goal_code,
            )
            assert isinstance(invi_rec, InvitationRecord)
            assert invi_rec.invitation.handshake_protocols
            assert invi_rec.invitation.label == my_label
            assert invi_rec.invitation.goal == goal
            assert invi_rec.invitation.goal_code == goal_code
            assert invi_rec.invitation_url

            base64_message = invi_rec.invitation_url.split("=", maxsplit=1)[1]
            base64_bytes = base64_message.encode("ascii")
            message_bytes = base64.b64decode(base64_bytes + b"==")
            base64.urlsafe_b64decode(pad(base64_message))
            data = message_bytes.decode("ascii")
            assert data
            invite_json = json.loads(data)
            assert invite_json
            assert invite_json["label"] == my_label
            assert invite_json["goal"] == goal
            assert invite_json["goal_code"] == goal_code

    async def test_wait_for_conn_rec_active_retrieve_by_id(self):
        with mock.patch.object(
            ConnRecord,
            "retrieve_by_id",
            mock.CoroutineMock(
                return_value=mock.MagicMock(connection_id="the-retrieved-connection-id")
            ),
        ):
            conn_rec = await self.manager._wait_for_conn_rec_active("a-connection-id")
            assert conn_rec.connection_id == "the-retrieved-connection-id"

    async def test_create_handshake_reuse_msg(self):
        self.profile.context.update_settings({"public_invites": True})

        with (
            mock.patch.object(
                OutOfBandManager,
                "fetch_connection_targets",
                autospec=True,
            ) as oob_mgr_fetch_conn,
            mock.patch.object(
                ConnRecord,
                "retrieve_by_id",
                mock.CoroutineMock(return_value=self.test_conn_rec),
            ),
        ):
            oob_mgr_fetch_conn.return_value = ConnectionTarget(
                did=TestConfig.test_did,
                endpoint=TestConfig.test_endpoint,
                recipient_keys=[TestConfig.test_verkey],
                sender_key=TestConfig.test_verkey,
            )

            invitation = InvitationMessage()
            oob_record = OobRecord(
                invitation=invitation,
                invi_msg_id=invitation._id,
                role=OobRecord.ROLE_RECEIVER,
                connection_id=self.test_conn_rec.connection_id,
                state=OobRecord.STATE_INITIAL,
            )

            oob_record = await self.manager._create_handshake_reuse_message(
                oob_record, self.test_conn_rec, invitation._version
            )

            _, kwargs = self.responder.send.call_args
            reuse_message: HandshakeReuse = kwargs.get("message")

            assert oob_record.state == OobRecord.STATE_AWAIT_RESPONSE

            # Assert responder has been called with the reuse message
            assert reuse_message._type == DIDCommPrefix.qualify_current(
                "out-of-band/1.1/handshake-reuse"
            )
            assert oob_record.reuse_msg_id == reuse_message._id

    async def test_create_handshake_reuse_msg_catch_exception(self):
        self.profile.context.update_settings({"public_invites": True})
        with mock.patch.object(
            OutOfBandManager,
            "fetch_connection_targets",
            autospec=True,
        ) as oob_mgr_fetch_conn:
            oob_mgr_fetch_conn.side_effect = StorageNotFoundError()
            with self.assertRaises(OutOfBandManagerError) as context:
                await self.manager._create_handshake_reuse_message(
                    mock.MagicMock(), self.test_conn_rec, "1.0"
                )
            assert "Error on creating and sending a handshake reuse message" in str(
                context.exception
            )

    async def test_receive_reuse_message_existing_found(self):
        self.profile.context.update_settings({"public_invites": True})

        receipt = MessageReceipt(
            recipient_did=TestConfig.test_did,
            recipient_did_public=False,
        )

        reuse_msg = HandshakeReuse()
        reuse_msg.assign_thread_id(thid="the-thread-id", pthid="the-pthid")

        self.test_conn_rec.invitation_msg_id = "test_123"
        self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160

        with (
            mock.patch.object(
                OutOfBandManager,
                "fetch_connection_targets",
                autospec=True,
            ) as oob_mgr_fetch_conn,
            mock.patch.object(
                OobRecord,
                "retrieve_by_tag_filter",
                autospec=True,
            ) as mock_retrieve_oob,
            mock.patch.object(self.profile, "notify", autospec=True) as mock_notify,
        ):
            mock_retrieve_oob.return_value = mock.MagicMock(
                emit_event=mock.CoroutineMock(),
                delete_record=mock.CoroutineMock(),
                multi_use=False,
            )

            await self.manager.receive_reuse_message(
                reuse_msg, receipt, self.test_conn_rec
            )
            mock_notify.assert_called_once_with(
                REUSE_WEBHOOK_TOPIC,
                {
                    "thread_id": "the-thread-id",
                    "connection_id": self.test_conn_rec.connection_id,
                    "comment": "Connection dummy is being reused for invitation the-pthid",
                },
            )

            # delete should be called if multi_use == False
            mock_retrieve_oob.return_value.delete_record.assert_called_once()
            mock_retrieve_oob.return_value.emit_event.assert_called_once()
            self.responder.send.assert_called_once_with(
                message=ANY, target_list=oob_mgr_fetch_conn.return_value
            )

            assert mock_retrieve_oob.return_value.state == OobRecord.STATE_DONE
            assert mock_retrieve_oob.return_value.reuse_msg_id == reuse_msg._thread_id
            assert (
                mock_retrieve_oob.return_value.connection_id
                == self.test_conn_rec.connection_id
            )

    async def test_receive_reuse_message_existing_found_multi_use(self):
        self.profile.context.update_settings({"public_invites": True})

        receipt = MessageReceipt(
            recipient_did=TestConfig.test_did,
            recipient_did_public=False,
        )

        reuse_msg = HandshakeReuse(version="1.0")
        reuse_msg.assign_thread_id(thid="the-thread-id", pthid="the-pthid")

        self.test_conn_rec.invitation_msg_id = "test_123"
        self.test_conn_rec.state = ConnRecord.State.COMPLETED.rfc160

        with (
            mock.patch.object(
                OutOfBandManager,
                "fetch_connection_targets",
                autospec=True,
            ) as oob_mgr_fetch_conn,
            mock.patch.object(
                OobRecord,
                "retrieve_by_tag_filter",
                autospec=True,
            ) as mock_retrieve_oob,
            mock.patch.object(self.profile, "notify", autospec=True) as mock_notify,
        ):
            mock_retrieve_oob.return_value = mock.MagicMock(
                emit_event=mock.CoroutineMock(),
                delete_record=mock.CoroutineMock(),
                multi_use=True,
            )

            await self.manager.receive_reuse_message(
                reuse_msg, receipt, self.test_conn_rec
            )
            mock_notify.assert_called_once_with(
                REUSE_WEBHOOK_TOPIC,
                {
                    "thread_id": "the-thread-id",
                    "connection_id": self.test_conn_rec.connection_id,
                    "comment": "Connection dummy is being reused for invitation the-pthid",
                },
            )

            # delete should be called if multi_use == False
            mock_retrieve_oob.return_value.delete_record.assert_not_called()
            mock_retrieve_oob.return_value.emit_event.assert_called_once()
            self.responder.send.assert_called_once_with(
                message=ANY, target_list=oob_mgr_fetch_conn.return_value
            )

            assert mock_retrieve_oob.return_value.state == OobRecord.STATE_DONE
            assert mock_retrieve_oob.return_value.reuse_msg_id == reuse_msg._thread_id
            assert (
                mock_retrieve_oob.return_value.connection_id
                == self.test_conn_rec.connection_id
            )

    async def test_receive_reuse_accepted(self):
        self.profile.context.update_settings({"public_invites": True})

        receipt = MessageReceipt(
            recipient_did=TestConfig.test_did,
            recipient_did_public=False,
            sender_did="test_did",
        )
        reuse_msg_accepted = HandshakeReuseAccept()
        reuse_msg_accepted.assign_thread_id(thid="the-thread-id", pthid="the-pthid")

        with (
            mock.patch.object(self.profile, "notify", autospec=True) as mock_notify,
            mock.patch.object(
                OobRecord, "retrieve_by_tag_filter", autospec=True
            ) as mock_retrieve_oob,
        ):
            mock_retrieve_oob.return_value = mock.MagicMock(
                emit_event=mock.CoroutineMock(),
                delete_record=mock.CoroutineMock(),
            )

            await self.manager.receive_reuse_accepted_message(
                reuse_msg_accepted, receipt, self.test_conn_rec
            )

            mock_retrieve_oob.return_value.emit_event.assert_called_once()
            mock_retrieve_oob.return_value.delete_record.assert_called_once()
            mock_notify.assert_called_once_with(
                REUSE_ACCEPTED_WEBHOOK_TOPIC,
                {
                    "thread_id": "the-thread-id",
                    "connection_id": self.test_conn_rec.connection_id,
                    "state": "accepted",
                    "comment": f"Connection {self.test_conn_rec.connection_id} is being reused for invitation the-pthid",
                },
            )

    async def test_receive_reuse_accepted_x(self):
        self.profile.context.update_settings({"public_invites": True})

        receipt = MessageReceipt(
            recipient_did=TestConfig.test_did,
            recipient_did_public=False,
            sender_did="test_did",
        )
        reuse_msg_accepted = HandshakeReuseAccept()
        reuse_msg_accepted.assign_thread_id(thid="the-thread-id", pthid="the-pthid")

        with (
            mock.patch.object(self.profile, "notify", autospec=True) as mock_notify,
            mock.patch.object(
                OobRecord, "retrieve_by_tag_filter", autospec=True
            ) as mock_retrieve_oob,
        ):
            mock_retrieve_oob.side_effect = (StorageNotFoundError,)

            with self.assertRaises(test_module.OutOfBandManagerError) as err:
                await self.manager.receive_reuse_accepted_message(
                    reuse_msg_accepted, receipt, self.test_conn_rec
                )
            assert "Error processing reuse accepted message " in err.exception.message

            mock_notify.assert_called_once_with(
                REUSE_ACCEPTED_WEBHOOK_TOPIC,
                {
                    "thread_id": "the-thread-id",
                    "state": "rejected",
                    "connection_id": self.test_conn_rec.connection_id,
                    "comment": f"Unable to process HandshakeReuseAccept message, connection {self.test_conn_rec.connection_id} and invitation the-pthid",
                },
            )

    async def test_receive_problem_report(self):
        self.profile.context.update_settings({"public_invites": True})

        receipt = MessageReceipt(
            recipient_did=TestConfig.test_did,
            recipient_did_public=False,
            sender_did="test_did",
        )
        problem_report = ProblemReport(
            description={
                "en": "test",
                "code": ProblemReportReason.EXISTING_CONNECTION_NOT_ACTIVE.value,
            }
        )
        problem_report.assign_thread_id(thid="the-thread-id", pthid="the-pthid")

        with mock.patch.object(
            OobRecord, "retrieve_by_tag_filter", autospec=True
        ) as mock_retrieve_oob:
            mock_retrieve_oob.return_value = mock.MagicMock(
                emit_event=mock.CoroutineMock(),
                delete_record=mock.CoroutineMock(),
                save=mock.CoroutineMock(),
            )

            await self.manager.receive_problem_report(
                problem_report, receipt, self.test_conn_rec
            )

            mock_retrieve_oob.assert_called_once_with(
                ANY, {"invi_msg_id": "the-pthid", "reuse_msg_id": "the-thread-id"}
            )
            assert mock_retrieve_oob.return_value.state == OobRecord.STATE_NOT_ACCEPTED

    async def test_receive_problem_report_x(self):
        self.profile.context.update_settings({"public_invites": True})

        receipt = MessageReceipt(
            recipient_did=TestConfig.test_did,
            recipient_did_public=False,
            sender_did="test_did",
        )
        problem_report = ProblemReport(
            description={
                "en": "test",
                "code": ProblemReportReason.EXISTING_CONNECTION_NOT_ACTIVE.value,
            }
        )
        problem_report.assign_thread_id(thid="the-thread-id", pthid="the-pthid")

        with mock.patch.object(
            OobRecord, "retrieve_by_tag_filter", autospec=True
        ) as mock_retrieve_oob:
            mock_retrieve_oob.side_effect = (StorageNotFoundError(),)

            with self.assertRaises(OutOfBandManagerError) as err:
                await self.manager.receive_problem_report(
                    problem_report, receipt, self.test_conn_rec
                )
            assert "Error processing problem report message " in err.exception.message

    async def test_receive_invitation_with_valid_mediation(self):
        mock_conn = mock.MagicMock(connection_id="dummy-connection")

        async with self.profile.session() as session:
            self.profile.context.update_settings({"public_invites": True})
            mediation_record = MediationRecord(
                role=MediationRecord.ROLE_CLIENT,
                state=MediationRecord.STATE_GRANTED,
                connection_id=self.test_mediator_conn_id,
                routing_keys=self.test_mediator_routing_keys,
                endpoint=self.test_mediator_endpoint,
            )
            await mediation_record.save(session)
            with (
                mock.patch.object(
                    DIDXManager, "receive_invitation", mock.CoroutineMock()
                ) as mock_didx_recv_invi,
                mock.patch.object(
                    ConnRecord, "retrieve_by_id", mock.CoroutineMock()
                ) as mock_retrieve_conn_by_id,
            ):
                invite = await self.manager.create_invitation(
                    my_endpoint=TestConfig.test_endpoint,
                    my_label="test123",
                    hs_protos=[HSProto.RFC23],
                )

                mock_retrieve_conn_by_id.return_value = mock_conn
                mock_didx_recv_invi.return_value = mock_conn
                invi_msg = invite.invitation
                await self.manager.receive_invitation(
                    invitation=invi_msg,
                    mediation_id=mediation_record._id,
                )
                mock_didx_recv_invi.assert_called_once_with(
                    invitation=invi_msg,
                    their_public_did=None,
                    auto_accept=None,
                    alias=None,
                    mediation_id=mediation_record._id,
                    protocol="didexchange/1.0",
                )

    async def test_receive_invitation_with_invalid_mediation(self):
        mock_conn = mock.MagicMock(connection_id="dummy-connection")

        with (
            mock.patch.object(
                DIDXManager,
                "receive_invitation",
                mock.CoroutineMock(),
            ) as mock_didx_recv_invi,
            mock.patch.object(
                ConnRecord,
                "retrieve_by_id",
                mock.CoroutineMock(),
            ) as mock_retrieve_conn_by_id,
        ):
            invite = await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                my_label="test123",
                hs_protos=[HSProto.RFC23],
            )
            mock_didx_recv_invi.return_value = mock_conn
            mock_retrieve_conn_by_id.return_value = mock_conn
            invi_msg = invite.invitation
            self.route_manager.mediation_record_if_id = mock.CoroutineMock(
                side_effect=StorageNotFoundError
            )
            await self.manager.receive_invitation(
                invi_msg,
                mediation_id="test-mediation-id",
            )
            mock_didx_recv_invi.assert_called_once_with(
                invitation=invi_msg,
                their_public_did=None,
                auto_accept=None,
                alias=None,
                mediation_id=None,
                protocol="didexchange/1.0",
            )

    async def test_receive_invitation_didx_services_with_service_block(self):
        self.profile.context.update_settings({"public_invites": True})

        mock_conn = mock.MagicMock(connection_id="dummy-connection")

        with (
            mock.patch.object(test_module, "DIDXManager", autospec=True) as didx_mgr_cls,
            mock.patch.object(
                ConnRecord, "retrieve_by_id", mock.CoroutineMock()
            ) as mock_retrieve_conn_by_id,
        ):
            didx_mgr_cls.return_value = mock.MagicMock(
                receive_invitation=mock.CoroutineMock(return_value=mock_conn)
            )
            mock_retrieve_conn_by_id.return_value = mock_conn
            oob_invitation = InvitationMessage(
                requests_attach=[],
                handshake_protocols=[
                    pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix
                ],
                services=[
                    OobService(
                        recipient_keys=["dummy"],
                        routing_keys=[],
                    )
                ],
            )

            await self.manager.receive_invitation(oob_invitation)

    async def test_receive_invitation_services_with_neither_service_blocks_nor_dids(
        self,
    ):
        self.profile.context.update_settings({"public_invites": True})
        oob_invitation = InvitationMessage(
            services=[],
        )
        with self.assertRaises(OutOfBandManagerError) as err:
            await self.manager.receive_invitation(oob_invitation)

        assert "service array must have exactly one element" in err.exception.message

    async def test_receive_invitation_no_hs_protos_no_attach(
        self,
    ):
        self.profile.context.update_settings({"public_invites": True})
        oob_invitation = InvitationMessage(
            services=["did:sov:something"],
        )
        with self.assertRaises(OutOfBandManagerError) as err:
            await self.manager.receive_invitation(oob_invitation)

        assert (
            "Invitation must specify handshake_protocols, requests_attach, or both"
            in err.exception.message
        )

    async def test_existing_conn_record_public_did(self):
        self.profile.context.update_settings({"public_invites": True})

        test_exist_conn = ConnRecord(
            connection_id="connection_id",
            my_did=TestConfig.test_did,
            their_did=TestConfig.test_target_did,
            their_public_did=TestConfig.test_target_did,
            invitation_msg_id="12345678-0123-4567-1234-567812345678",
            their_role=ConnRecord.Role.REQUESTER,
        )

        with (
            mock.patch.object(
                ConnRecord,
                "find_existing_connection",
                mock.CoroutineMock(),
            ) as oob_mgr_find_existing_conn,
            mock.patch.object(OobRecord, "save", mock.CoroutineMock()) as oob_record_save,
            mock.patch.object(
                OobRecord, "retrieve_by_id", mock.CoroutineMock()
            ) as oob_record_retrieve_by_id,
            mock.patch.object(
                OutOfBandManager, "fetch_connection_targets", autospec=True
            ) as oob_mgr_fetch_conn,
        ):
            oob_mgr_find_existing_conn.return_value = test_exist_conn
            oob_mgr_fetch_conn.return_value = []
            oob_invitation = InvitationMessage(
                handshake_protocols=[
                    pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix
                ],
                services=[TestConfig.test_target_did],
                requests_attach=[],
            )

            oob_record_retrieve_by_id.return_value = mock.MagicMock(
                state=OobRecord.STATE_ACCEPTED
            )

            result = await self.manager.receive_invitation(
                oob_invitation, use_existing_connection=True
            )

            oob_mgr_find_existing_conn.assert_called_once()
            assert result.state == OobRecord.STATE_ACCEPTED
            oob_record_save.assert_called_once_with(ANY, reason="Storing reuse msg data")

    async def test_receive_invitation_handshake_reuse(self):
        self.profile.context.update_settings({"public_invites": True})

        test_exist_conn = ConnRecord(
            connection_id="connection_id",
            my_did=TestConfig.test_did,
            their_did=TestConfig.test_target_did,
            their_public_did=TestConfig.test_target_did,
            invitation_msg_id="12345678-0123-4567-1234-567812345678",
            their_role=ConnRecord.Role.REQUESTER,
        )

        with (
            mock.patch.object(
                test_module.OutOfBandManager,
                "_handle_handshake_reuse",
                mock.CoroutineMock(),
            ) as handle_handshake_reuse,
            mock.patch.object(
                test_module.OutOfBandManager,
                "_perform_handshake",
                mock.CoroutineMock(),
            ) as perform_handshake,
            mock.patch.object(
                ConnRecord,
                "find_existing_connection",
                mock.CoroutineMock(return_value=test_exist_conn),
            ),
        ):
            oob_invitation = InvitationMessage(
                handshake_protocols=[
                    pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix
                ],
                services=[TestConfig.test_target_did],
                requests_attach=[],
            )

            handle_handshake_reuse.return_value = mock.MagicMock(
                state=OobRecord.STATE_ACCEPTED
            )

            result = await self.manager.receive_invitation(
                oob_invitation, use_existing_connection=True
            )

            perform_handshake.assert_not_called()
            handle_handshake_reuse.assert_called_once_with(
                ANY, test_exist_conn, oob_invitation._version
            )

            assert result.state == OobRecord.STATE_ACCEPTED

    async def test_receive_invitation_handshake_reuse_failed(self):
        self.profile.context.update_settings({"public_invites": True})

        test_exist_conn = ConnRecord(
            connection_id="connection_id",
            my_did=TestConfig.test_did,
            their_did=TestConfig.test_target_did,
            their_public_did=TestConfig.test_target_did,
            invitation_msg_id="12345678-0123-4567-1234-567812345678",
            their_role=ConnRecord.Role.REQUESTER,
        )

        with (
            mock.patch.object(
                test_module.OutOfBandManager,
                "_handle_handshake_reuse",
                mock.CoroutineMock(),
            ) as handle_handshake_reuse,
            mock.patch.object(
                test_module.OutOfBandManager,
                "_perform_handshake",
                mock.CoroutineMock(),
            ) as perform_handshake,
            mock.patch.object(
                ConnRecord,
                "find_existing_connection",
                mock.CoroutineMock(return_value=test_exist_conn),
            ),
            mock.patch.object(
                ConnRecord,
                "retrieve_by_id",
                mock.CoroutineMock(return_value=test_exist_conn),
            ),
        ):
            oob_invitation = InvitationMessage(
                handshake_protocols=[
                    pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix
                ],
                services=[TestConfig.test_target_did],
                requests_attach=[],
            )

            mock_oob = mock.MagicMock(
                delete_record=mock.CoroutineMock(),
                emit_event=mock.CoroutineMock(),
            )
            perform_handshake.return_value = mock_oob

            handle_handshake_reuse.return_value = mock.MagicMock(
                state=OobRecord.STATE_NOT_ACCEPTED
            )

            result = await self.manager.receive_invitation(
                oob_invitation,
                use_existing_connection=True,
                alias="alias",
                auto_accept=True,
                mediation_id="mediation_id",
            )

            handle_handshake_reuse.assert_called_once_with(
                ANY, test_exist_conn, oob_invitation._version
            )
            perform_handshake.assert_called_once_with(
                oob_record=ANY,
                alias="alias",
                auto_accept=True,
                mediation_id="mediation_id",
                service_accept=None,
            )

            assert mock_oob.state == OobRecord.STATE_DONE
            assert result is mock_oob
            mock_oob.emit_event.assert_called_once()
            mock_oob.delete_record.assert_called_once()

    async def test_receive_invitation_services_with_service_did(self):
        self.profile.context.update_settings({"public_invites": True})

        mock_conn = mock.MagicMock(connection_id="dummy")

        with (
            mock.patch.object(test_module, "DIDXManager", autospec=True) as didx_mgr_cls,
            mock.patch.object(
                ConnRecord,
                "retrieve_by_id",
                mock.CoroutineMock(return_value=mock_conn),
            ),
        ):
            didx_mgr_cls.return_value = mock.MagicMock(
                receive_invitation=mock.CoroutineMock(return_value=mock_conn)
            )
            oob_invitation = InvitationMessage(
                handshake_protocols=[
                    pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix
                ],
                services=[TestConfig.test_did],
                requests_attach=[],
            )

            invitation_record = await self.manager.receive_invitation(oob_invitation)
            assert invitation_record.invitation.services

    async def test_request_attach_oob_message_processor_connectionless(self):
        requests_attach: List[AttachDecorator] = [
            AttachDecorator.deserialize(deepcopy(TestConfig.req_attach_v1))
        ]

        mock_oob_processor = mock.MagicMock(OobMessageProcessor, autospec=True)
        mock_oob_processor.handle_message = mock.CoroutineMock()
        self.profile.context.injector.bind_instance(
            OobMessageProcessor, mock_oob_processor
        )

        mock_service_decorator = ServiceDecorator(
            endpoint=self.test_endpoint, recipient_keys=[self.test_verkey]
        )

        with (
            mock.patch.object(
                AskarWallet,
                "create_signing_key",
                mock.CoroutineMock(),
            ) as mock_create_signing_key,
            mock.patch.object(
                OutOfBandManager,
                "_service_decorator_from_service",
                mock.CoroutineMock(),
            ) as mock_service_decorator_from_service,
        ):
            mock_create_signing_key.return_value = KeyInfo(
                verkey="H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV",
                metadata={},
                key_type=ED25519,
            )
            mock_service_decorator_from_service.return_value = mock_service_decorator
            oob_invitation = InvitationMessage(
                handshake_protocols=[],
                services=[self.test_service],
                requests_attach=requests_attach,
            )

            oob_record = await self.manager.receive_invitation(
                oob_invitation, use_existing_connection=True
            )

            assert (
                oob_record.our_recipient_key
                == "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV"
            )
            assert oob_record.our_service
            assert oob_record.state == OobRecord.STATE_PREPARE_RESPONSE

            mock_create_signing_key.assert_called_once_with(ED25519)
            mock_oob_processor.handle_message.assert_called_once_with(
                self.profile,
                [attachment.content for attachment in requests_attach],
                oob_record=oob_record,
                their_service=mock_service_decorator,
            )

    async def test_request_attach_oob_message_processor_connection(self):
        self.profile.context.update_settings({"public_invites": True})
        test_exist_conn = ConnRecord(
            connection_id="a-connection-id",
            my_did=TestConfig.test_did,
            their_did=TestConfig.test_target_did,
            their_public_did=TestConfig.test_target_did,
            invitation_msg_id="12345678-0123-4567-1234-567812345678",
            their_role=ConnRecord.Role.REQUESTER,
            state=ConnRecord.State.COMPLETED.rfc160,
        )

        requests_attach: List[AttachDecorator] = [
            AttachDecorator.deserialize(deepcopy(TestConfig.req_attach_v1))
        ]

        mock_oob_processor = mock.MagicMock(OobMessageProcessor, autospec=True)
        mock_oob_processor.handle_message = mock.CoroutineMock()
        self.profile.context.injector.bind_instance(
            OobMessageProcessor, mock_oob_processor
        )

        with mock.patch.object(
            ConnRecord,
            "find_existing_connection",
            mock.CoroutineMock(),
        ) as oob_mgr_find_existing_conn:
            oob_mgr_find_existing_conn.return_value = test_exist_conn
            oob_invitation = InvitationMessage(
                handshake_protocols=[
                    pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix
                ],
                services=[TestConfig.test_target_did],
                requests_attach=requests_attach,
            )

            oob_record = await self.manager.receive_invitation(
                oob_invitation, use_existing_connection=True
            )

            mock_oob_processor.handle_message.assert_called_once_with(
                self.profile,
                [attachment.content for attachment in requests_attach],
                oob_record=oob_record,
                their_service=None,
            )

    async def test_request_attach_wait_for_conn_rec_active(self):
        async with self.profile.session() as session:
            self.profile.context.update_settings({"public_invites": True})
            test_exist_conn = ConnRecord(
                my_did=TestConfig.test_did,
                their_did=TestConfig.test_target_did,
                their_public_did=TestConfig.test_target_did,
                invitation_msg_id="12345678-0123-4567-1234-567812345678",
                their_role=ConnRecord.Role.REQUESTER,
            )

            with (
                mock.patch.object(
                    OutOfBandManager, "_wait_for_conn_rec_active"
                ) as mock_wait_for_conn_rec_active,
                mock.patch.object(
                    ConnRecord,
                    "find_existing_connection",
                    mock.CoroutineMock(),
                ) as oob_mgr_find_existing_conn,
            ):
                oob_mgr_find_existing_conn.return_value = test_exist_conn
                mock_wait_for_conn_rec_active.return_value = None
                oob_invitation = InvitationMessage(
                    handshake_protocols=[
                        pfx.qualify(HSProto.RFC23.name) for pfx in DIDCommPrefix
                    ],
                    services=[TestConfig.test_target_did],
                    requests_attach=[
                        AttachDecorator.deserialize(deepcopy(TestConfig.req_attach_v1))
                    ],
                )

                with self.assertRaises(test_module.OutOfBandManagerError) as err:
                    oob_record = await self.manager.receive_invitation(
                        oob_invitation, use_existing_connection=True
                    )
                assert (
                    "Connection not ready to process attach message for connection_id:"
                    in err.exception.message
                )

    async def test_service_decorator_from_service_did(self):
        did = "did:sov:something"

        self.manager.resolve_invitation = mock.CoroutineMock()
        self.manager.resolve_invitation.return_value = (
            TestConfig.test_endpoint,
            [TestConfig.test_verkey],
            self.test_mediator_routing_keys,
        )

        service = await self.manager._service_decorator_from_service(did)

        assert service.endpoint == TestConfig.test_endpoint
        assert service.recipient_keys == [TestConfig.test_verkey]
        assert service.routing_keys == self.test_mediator_routing_keys

    async def test_service_decorator_from_service_object(self):
        oob_service = OobService(
            service_endpoint=TestConfig.test_endpoint,
            recipient_keys=[
                DIDKey.from_public_key_b58(TestConfig.test_verkey, ED25519).did
            ],
            routing_keys=[
                DIDKey.from_public_key_b58(verkey, ED25519).did
                for verkey in self.test_mediator_routing_keys
            ],
        )
        service = await self.manager._service_decorator_from_service(oob_service)

        assert service.endpoint == TestConfig.test_endpoint
        assert service.recipient_keys == [TestConfig.test_verkey]
        assert service.routing_keys == self.test_mediator_routing_keys

    async def test_service_decorator_from_service_str_empty_endpoint(self):
        did = "did:sov:something"

        self.manager.resolve_invitation = mock.CoroutineMock()
        self.manager.resolve_invitation.return_value = (
            "",  # empty endpoint
            [TestConfig.test_verkey],
            self.test_mediator_routing_keys,
        )

        service = await self.manager._service_decorator_from_service(did)

        assert service is None

    async def test_service_decorator_from_service_str_none_endpoint(self):
        did = "did:sov:something"

        self.manager.resolve_invitation = mock.CoroutineMock()
        self.manager.resolve_invitation.return_value = (
            None,  # None endpoint
            [TestConfig.test_verkey],
            self.test_mediator_routing_keys,
        )

        service = await self.manager._service_decorator_from_service(did)

        assert service is None

    async def test_service_decorator_from_service_object_empty_endpoint(self):
        oob_service = OobService(
            service_endpoint="",  # empty endpoint
            recipient_keys=[
                DIDKey.from_public_key_b58(TestConfig.test_verkey, ED25519).did
            ],
            routing_keys=[
                DIDKey.from_public_key_b58(verkey, ED25519).did
                for verkey in self.test_mediator_routing_keys
            ],
        )
        service = await self.manager._service_decorator_from_service(oob_service)

        assert service is None

    async def test_service_decorator_from_service_object_none_endpoint(self):
        oob_service = OobService(
            service_endpoint=None,  # None endpoint
            recipient_keys=[
                DIDKey.from_public_key_b58(TestConfig.test_verkey, ED25519).did
            ],
            routing_keys=[
                DIDKey.from_public_key_b58(verkey, ED25519).did
                for verkey in self.test_mediator_routing_keys
            ],
        )
        service = await self.manager._service_decorator_from_service(oob_service)

        assert service is None

    async def test_service_decorator_from_service_invalid_type(self):
        invalid_type = 123  # integer, for example

        service = await self.manager._service_decorator_from_service(invalid_type)

        assert service is None

    async def test_delete_stale_connection_by_invitation(self):
        current_datetime = datetime_now()
        older_datetime = current_datetime - timedelta(hours=4)
        records = [
            ConnRecord(
                my_did=self.test_did,
                their_did="FBmi5JLf5g58kDnNXMy4QM",
                their_role=ConnRecord.Role.RESPONDER.rfc160,
                state=ConnRecord.State.INVITATION.rfc160,
                invitation_key="dummy2",
                invitation_mode="once",
                invitation_msg_id="test123",
                updated_at=datetime_to_str(older_datetime),
            )
        ]
        with (
            mock.patch.object(
                ConnRecord, "query", mock.CoroutineMock()
            ) as mock_connrecord_query,
            mock.patch.object(
                ConnRecord, "delete_record", mock.CoroutineMock()
            ) as mock_connrecord_delete,
        ):
            mock_connrecord_query.return_value = records
            await self.manager.delete_stale_connection_by_invitation("test123")
            mock_connrecord_delete.assert_called_once()

    async def test_delete_conn_and_oob_record_invitation(self):
        invitation = InvitationMessage()
        oob_records = [
            OobRecord(
                invitation=invitation,
                invi_msg_id=invitation._id,
                role=OobRecord.ROLE_RECEIVER,
                connection_id=self.test_conn_rec.connection_id,
                state=OobRecord.STATE_INITIAL,
            )
        ]
        conn_records = [
            ConnRecord(
                my_did=self.test_did,
                their_did="FBmi5JLf5g58kDnNXMy4QM",
                their_role=ConnRecord.Role.RESPONDER.rfc160,
                state=ConnRecord.State.INVITATION.rfc160,
                invitation_key="dummy2",
                invitation_mode="once",
                invitation_msg_id="test123",
            )
        ]
        with (
            mock.patch.object(
                ConnRecord, "query", mock.CoroutineMock()
            ) as mock_connrecord_query,
            mock.patch.object(
                ConnRecord, "delete_record", mock.CoroutineMock()
            ) as mock_connrecord_delete,
            mock.patch.object(
                OobRecord, "query", mock.CoroutineMock()
            ) as mock_oobrecord_query,
            mock.patch.object(
                OobRecord, "delete_record", mock.CoroutineMock()
            ) as mock_oobrecord_delete,
        ):
            mock_connrecord_query.return_value = conn_records
            mock_oobrecord_query.return_value = oob_records
            await self.manager.delete_conn_and_oob_record_invitation("test123")
            mock_connrecord_delete.assert_called_once()
            mock_oobrecord_delete.assert_called_once()

    async def test_fetch_invitation_succeeds(self):
        self.profile.context.update_settings({"public_invites": True})

        with mock.patch.object(
            AskarWallet, "get_public_did", autospec=True
        ) as mock_wallet_get_public_did:
            mock_wallet_get_public_did.return_value = DIDInfo(
                TestConfig.test_did,
                TestConfig.test_verkey,
                None,
                method=SOV,
                key_type=ED25519,
            )
            invi_rec = await self.manager.create_invitation(
                my_endpoint=TestConfig.test_endpoint,
                public=True,
                hs_protos=[HSProto.RFC23],
            )

            invi_rec = await self.manager.fetch_oob_invitation_record_by_id(
                oob_id=invi_rec.oob_id
            )

            assert invi_rec.invitation._type == DIDCommPrefix.qualify_current(
                self.TEST_INVI_MESSAGE_TYPE
            )
            assert not invi_rec.invitation.requests_attach
            assert (
                DIDCommPrefix.qualify_current(HSProto.RFC23.name)
                in invi_rec.invitation.handshake_protocols
            )
            assert invi_rec.invitation.services == [f"did:sov:{TestConfig.test_did}"]
