import copy
import os
import socket
import sys
import pickle

import should_be.all  # noqa
import six
from parameterized import parameterized

from gssapi import creds as gsscreds
from gssapi import mechs as gssmechs
from gssapi import names as gssnames
from gssapi import sec_contexts as gssctx
from gssapi import raw as gb
from gssapi import _utils as gssutils
from gssapi import exceptions as excs
import k5test.unit as ktu
import k5test as kt


TARGET_SERVICE_NAME = b'host'
FQDN = socket.getfqdn().encode('utf-8')
SERVICE_PRINCIPAL = TARGET_SERVICE_NAME + b'/' + FQDN

# disable error deferring to catch errors immediately
gssctx.SecurityContext.__DEFER_STEP_ERRORS__ = False


class _GSSAPIKerberosTestCase(kt.KerberosTestCase):
    @classmethod
    def setUpClass(cls):
        super(_GSSAPIKerberosTestCase, cls).setUpClass()
        svc_princ = SERVICE_PRINCIPAL.decode("UTF-8")

        cls.realm.kinit(svc_princ, flags=['-k'])

        cls._init_env()

        cls.USER_PRINC = cls.realm.user_princ.split('@')[0].encode("UTF-8")
        cls.ADMIN_PRINC = cls.realm.admin_princ.split('@')[0].encode("UTF-8")

    @classmethod
    def _init_env(cls):
        cls._saved_env = copy.deepcopy(os.environ)
        for k, v in cls.realm.env.items():
            os.environ[k] = v

    @classmethod
    def _restore_env(cls):
        for k in copy.deepcopy(os.environ):
            if k in cls._saved_env:
                os.environ[k] = cls._saved_env[k]
            else:
                del os.environ[k]

        cls._saved_env = None

    @classmethod
    def tearDownClass(cls):
        super(_GSSAPIKerberosTestCase, cls).tearDownClass()
        cls._restore_env()


def _perms_cycle(elem, rest, old_d):
    if elem is None:
        name_str = "with_params_"
        true_keys = [k for (k, v) in old_d.items() if v]
        if not true_keys:
            name_str += 'none'
        else:
            name_str += '_'.join(true_keys)

        return [(name_str, old_d)]
    else:
        if len(rest) > 0:
            next_elem = rest.pop()
        else:
            next_elem = None

        res = []
        for v in (True, False):
            new_d = copy.deepcopy(old_d)
            new_d[elem] = v
            res.extend(_perms_cycle(next_elem, copy.deepcopy(rest), new_d))

        return res


def exist_perms(**kwargs):
    all_elems = list(kwargs.keys())
    curr_elems = copy.deepcopy(all_elems)

    perms = _perms_cycle(curr_elems.pop(), curr_elems, {})
    res = []
    for name_str, perm in perms:
        args = dict([(k, v) for (k, v) in kwargs.items() if perm[k]])
        res.append((name_str, args))

    return parameterized.expand(res)


def true_false_perms(*all_elems_tuple):
    all_elems = list(all_elems_tuple)
    curr_elems = copy.deepcopy(all_elems)

    perms = _perms_cycle(curr_elems.pop(), curr_elems, {})
    return parameterized.expand(perms)


# NB(directxman12): MIT Kerberos completely ignores input TTLs for
#                   credentials.  I suspect this is because the TTL
#                   is actually set when kinit is called.
# NB(directxman12): the above note used to be wonderfully sarcastic
class CredsTestCase(_GSSAPIKerberosTestCase):
    def setUp(self):
        super(CredsTestCase, self).setUp()

        svc_princ = SERVICE_PRINCIPAL.decode("UTF-8")
        self.realm.kinit(svc_princ, flags=['-k'])

        self.name = gssnames.Name(SERVICE_PRINCIPAL,
                                  gb.NameType.kerberos_principal)

    @exist_perms(lifetime=30, mechs=[gb.MechType.kerberos],
                 usage='both')
    def test_acquire_by_init(self, str_name, kwargs):
        creds = gsscreds.Credentials(name=self.name, **kwargs)

        creds.lifetime.should_be_an_integer()

        del creds

    @exist_perms(lifetime=30, mechs=[gb.MechType.kerberos],
                 usage='both')
    def test_acquire_by_method(self, str_name, kwargs):
        cred_resp = gsscreds.Credentials.acquire(name=self.name,
                                                 **kwargs)

        cred_resp.shouldnt_be_none()

        (creds, actual_mechs, ttl) = cred_resp

        creds.shouldnt_be_none()
        creds.should_be_a(gsscreds.Credentials)

        actual_mechs.shouldnt_be_empty()
        actual_mechs.should_include(gb.MechType.kerberos)

        ttl.should_be_an_integer()

        del creds

    @ktu.gssapi_extension_test('rfc5588', 'RFC 5588')
    def test_store_acquire(self):
        # we need to acquire a forwardable ticket
        svc_princ = SERVICE_PRINCIPAL.decode("UTF-8")
        self.realm.kinit(svc_princ, flags=['-k', '-f'])

        target_name = gssnames.Name(TARGET_SERVICE_NAME,
                                    gb.NameType.hostbased_service)

        client_creds = gsscreds.Credentials(usage='initiate')
        client_ctx = gssctx.SecurityContext(
            name=target_name, creds=client_creds,
            flags=gb.RequirementFlag.delegate_to_peer)

        client_token = client_ctx.step()

        server_creds = gsscreds.Credentials(usage='accept')
        server_ctx = gssctx.SecurityContext(creds=server_creds)
        server_ctx.step(client_token)

        deleg_creds = server_ctx.delegated_creds
        deleg_creds.shouldnt_be_none()

        store_res = deleg_creds.store(usage='initiate', set_default=True,
                                      overwrite=True)
        store_res.usage.should_be('initiate')
        store_res.mechs.should_include(gb.MechType.kerberos)

        reacquired_creds = gsscreds.Credentials(name=deleg_creds.name,
                                                usage='initiate')
        reacquired_creds.shouldnt_be_none()

    @ktu.gssapi_extension_test('cred_store', 'credentials store')
    def test_store_into_acquire_from(self):
        CCACHE = 'FILE:{tmpdir}/other_ccache'.format(tmpdir=self.realm.tmpdir)
        KT = '{tmpdir}/other_keytab'.format(tmpdir=self.realm.tmpdir)
        store = {'ccache': CCACHE, 'keytab': KT}

        princ_name = 'service/cs@' + self.realm.realm
        self.realm.addprinc(princ_name)
        self.realm.extract_keytab(princ_name, KT)
        self.realm.kinit(princ_name, None, ['-k', '-t', KT])

        initial_creds = gsscreds.Credentials(name=None,
                                             usage='initiate')

        store_res = initial_creds.store(store, overwrite=True)

        store_res.mechs.shouldnt_be_none()
        store_res.mechs.shouldnt_be_empty()
        store_res.usage.should_be('initiate')

        name = gssnames.Name(princ_name)
        retrieved_creds = gsscreds.Credentials(name=name, store=store)

        retrieved_creds.shouldnt_be_none()

    def test_create_from_other(self):
        raw_creds = gb.acquire_cred(None, usage='accept').creds

        high_level_creds = gsscreds.Credentials(raw_creds)
        high_level_creds.usage.should_be('accept')

    @true_false_perms('name', 'lifetime', 'usage', 'mechs')
    def test_inquire(self, str_name, kwargs):
        creds = gsscreds.Credentials(name=self.name)
        resp = creds.inquire(**kwargs)

        if kwargs['name']:
            resp.name.should_be(self.name)
        else:
            resp.name.should_be_none()

        if kwargs['lifetime']:
            resp.lifetime.should_be_an_integer()
        else:
            resp.lifetime.should_be_none()

        if kwargs['usage']:
            resp.usage.should_be('both')
        else:
            resp.usage.should_be_none()

        if kwargs['mechs']:
            resp.mechs.shouldnt_be_empty()
            resp.mechs.should_include(gb.MechType.kerberos)
        else:
            resp.mechs.should_be_none()

    @true_false_perms('name', 'init_lifetime', 'accept_lifetime', 'usage')
    def test_inquire_by_mech(self, str_name, kwargs):
        creds = gsscreds.Credentials(name=self.name)
        resp = creds.inquire_by_mech(mech=gb.MechType.kerberos, **kwargs)

        if kwargs['name']:
            resp.name.should_be(self.name)
        else:
            resp.name.should_be_none()

        if kwargs['init_lifetime']:
            resp.init_lifetime.should_be_an_integer()
        else:
            resp.init_lifetime.should_be_none()

        if kwargs['accept_lifetime']:
            resp.accept_lifetime.should_be_an_integer()
        else:
            resp.accept_lifetime.should_be_none()

        if kwargs['usage']:
            resp.usage.should_be('both')
        else:
            resp.usage.should_be_none()

    def test_add(self):
        input_creds = gsscreds.Credentials(gb.Creds())
        name = gssnames.Name(SERVICE_PRINCIPAL)
        new_creds = input_creds.add(name, gb.MechType.kerberos,
                                    usage='initiate')

        new_creds.shouldnt_be_none()
        new_creds.should_be_a(gsscreds.Credentials)

    @ktu.gssapi_extension_test('cred_store', 'credentials store')
    def test_store_into_add_from(self):
        CCACHE = 'FILE:{tmpdir}/other_ccache'.format(tmpdir=self.realm.tmpdir)
        KT = '{tmpdir}/other_keytab'.format(tmpdir=self.realm.tmpdir)
        store = {'ccache': CCACHE, 'keytab': KT}

        princ_name = 'service/cs@' + self.realm.realm
        self.realm.addprinc(princ_name)
        self.realm.extract_keytab(princ_name, KT)
        self.realm.kinit(princ_name, None, ['-k', '-t', KT])

        initial_creds = gsscreds.Credentials(name=None,
                                             usage='initiate')

        store_res = initial_creds.store(store, overwrite=True)

        store_res.mechs.shouldnt_be_none()
        store_res.mechs.shouldnt_be_empty()
        store_res.usage.should_be('initiate')

        name = gssnames.Name(princ_name)
        input_creds = gsscreds.Credentials(gb.Creds())
        retrieved_creds = input_creds.add(name, gb.MechType.kerberos,
                                          store=store)

        retrieved_creds.shouldnt_be_none()
        retrieved_creds.should_be_a(gsscreds.Credentials)

    @ktu.gssapi_extension_test('cred_imp_exp', 'credentials import-export')
    def test_export(self):
        creds = gsscreds.Credentials(name=self.name)
        token = creds.export()
        token.should_be_a(bytes)

    @ktu.gssapi_extension_test('cred_imp_exp', 'credentials import-export')
    def test_import_by_init(self):
        creds = gsscreds.Credentials(name=self.name)
        token = creds.export()
        imported_creds = gsscreds.Credentials(token=token)

        imported_creds.lifetime.should_be(creds.lifetime)
        imported_creds.name.should_be(creds.name)

    @ktu.gssapi_extension_test('cred_imp_exp', 'credentials import-export')
    def test_pickle_unpickle(self):
        creds = gsscreds.Credentials(name=self.name)
        pickled_creds = pickle.dumps(creds)
        unpickled_creds = pickle.loads(pickled_creds)

        unpickled_creds.lifetime.should_be(creds.lifetime)
        unpickled_creds.name.should_be(creds.name)

    @exist_perms(lifetime=30, mechs=[gb.MechType.kerberos],
                 usage='initiate')
    @ktu.gssapi_extension_test('s4u', 'S4U')
    def test_impersonate(self, str_name, kwargs):
        target_name = gssnames.Name(TARGET_SERVICE_NAME,
                                    gb.NameType.hostbased_service)
        # TODO(directxman12): make this use the high-level SecurityContext
        client_ctx_resp = gb.init_sec_context(target_name)
        client_token = client_ctx_resp[3]
        del client_ctx_resp  # free everything but the token

        server_name = self.name
        server_creds = gsscreds.Credentials(name=server_name,
                                            usage='both')
        server_ctx_resp = gb.accept_sec_context(client_token,
                                                acceptor_creds=server_creds)

        imp_creds = server_creds.impersonate(server_ctx_resp[1], **kwargs)

        imp_creds.shouldnt_be_none()
        imp_creds.should_be_a(gsscreds.Credentials)

    @ktu.gssapi_extension_test('s4u', 'S4U')
    def test_add_with_impersonate(self):
        target_name = gssnames.Name(TARGET_SERVICE_NAME,
                                    gb.NameType.hostbased_service)
        client_ctx = gssctx.SecurityContext(name=target_name)
        client_token = client_ctx.step()

        server_creds = gsscreds.Credentials(usage='both')
        server_ctx = gssctx.SecurityContext(creds=server_creds, usage='accept')
        server_ctx.step(client_token)

        # use empty creds to test here
        input_creds = gsscreds.Credentials(gb.Creds())
        new_creds = input_creds.add(server_ctx.initiator_name,
                                    gb.MechType.kerberos,
                                    impersonator=server_creds,
                                    usage='initiate')

        new_creds.shouldnt_be(None)
        new_creds.should_be_a(gsscreds.Credentials)


class MechsTestCase(_GSSAPIKerberosTestCase):
    def test_indicate_mechs(self):
        mechs = gssmechs.Mechanism.all_mechs()
        for mech in mechs:
            s = str(mech)
            s.shouldnt_be_empty()

    @ktu.gssapi_extension_test('rfc5801', 'RFC 5801: SASL Names')
    def test_sasl_properties(self):
        mechs = gssmechs.Mechanism.all_mechs()
        for mech in mechs:
            s = str(mech)
            s.shouldnt_be_empty()
            s.should_be_a(str)

            # Note that some mechanisms don't have SASL names or SASL
            # descriptions; in this case, GSSAPI returns empty strings.
            if mech.sasl_name:
                mech.sasl_name.should_be_a(six.text_type)

            if mech.description:
                mech.description.should_be_a(six.text_type)

            cmp_mech = gssmechs.Mechanism.from_sasl_name(mech.sasl_name)
            str(cmp_mech).should_be(str(mech))

    @ktu.gssapi_extension_test('rfc5587', 'RFC 5587: Mech Inquiry')
    def test_mech_inquiry(self):
        mechs = list(gssmechs.Mechanism.all_mechs())
        c = len(mechs)

        g_M_from_attrs = gssmechs.Mechanism.from_attrs

        for mech in mechs:
            attrs = mech.attrs
            known_attrs = mech.known_attrs

            for attr in attrs:
                from_desired = g_M_from_attrs(desired_attrs=[attr])
                from_except = g_M_from_attrs(except_attrs=[attr])

                from_desired = list(from_desired)
                from_except = list(from_except)

                (len(from_desired) + len(from_except)).should_be(c)
                from_desired.should_include(mech)
                from_except.shouldnt_include(mech)

            for attr in known_attrs:
                from_desired = g_M_from_attrs(desired_attrs=[attr])
                from_except = g_M_from_attrs(except_attrs=[attr])

                from_desired = list(from_desired)
                from_except = list(from_except)

                (len(from_desired) + len(from_except)).should_be(c)


class NamesTestCase(_GSSAPIKerberosTestCase):
    def test_create_from_other(self):
        raw_name = gb.import_name(SERVICE_PRINCIPAL)
        high_level_name = gssnames.Name(raw_name)

        bytes(high_level_name).should_be(SERVICE_PRINCIPAL)

    def test_create_from_name_no_type(self):
        name = gssnames.Name(SERVICE_PRINCIPAL)

        name.shouldnt_be_none()

    def test_create_from_name_and_type(self):
        name = gssnames.Name(SERVICE_PRINCIPAL, gb.NameType.kerberos_principal)

        name.shouldnt_be_none()
        name.name_type.should_be(gb.NameType.kerberos_principal)

    def test_create_from_token(self):
        name1 = gssnames.Name(TARGET_SERVICE_NAME,
                              gb.NameType.hostbased_service)
        exported_name = name1.canonicalize(gb.MechType.kerberos).export()
        name2 = gssnames.Name(token=exported_name)

        name2.shouldnt_be_none()
        name2.name_type.should_be(gb.NameType.kerberos_principal)

    @ktu.gssapi_extension_test('rfc6680', 'RFC 6680')
    def test_display_as(self):
        name = gssnames.Name(TARGET_SERVICE_NAME,
                             gb.NameType.hostbased_service)
        canonical_name = name.canonicalize(gb.MechType.kerberos)

        # NB(directxman12): krb5 doesn't implement display_name_ext, so just
        # check to make sure we return the right types and a reasonable value
        krb_name = canonical_name.display_as(
            gb.NameType.hostbased_service)

        princ_str = SERVICE_PRINCIPAL.decode('utf-8') + '@'
        six.text_type(canonical_name).should_be(princ_str)
        krb_name.should_be_a(six.text_type)
        krb_name.should_be(princ_str)

    @ktu.gssapi_extension_test('rfc6680', 'RFC 6680')
    def test_create_from_composite_token_no_attrs(self):
        name1 = gssnames.Name(TARGET_SERVICE_NAME,
                              gb.NameType.hostbased_service)
        exported_name = name1.canonicalize(
            gb.MechType.kerberos).export(composite=True)
        name2 = gssnames.Name(token=exported_name, composite=True)

        name2.shouldnt_be_none()

    @ktu.gssapi_extension_test('rfc6680', 'RFC 6680')
    @ktu.krb_plugin_test('authdata', 'greet_client')
    def test_create_from_composite_token_with_attrs(self):
        name1 = gssnames.Name(TARGET_SERVICE_NAME,
                              gb.NameType.hostbased_service)

        canon_name = name1.canonicalize(gb.MechType.kerberos)
        canon_name.attributes['urn:greet:greeting'] = b'some val'

        exported_name = canon_name.export(composite=True)

        # TODO(directxman12): when you just import a token as composite,
        # appears as this name whose text is all garbled, since it contains
        # all of the attributes, etc, but doesn't properly have the attributes.
        # Once it's canonicalized, the attributes reappear.  However, if you
        # just import it as normal export, the attributes appear directly.
        # It is thus unclear as to what is going on
        # name2_raw = gssnames.Name(token=exported_name, composite=True)
        # name2 = name2_raw.canonicalize(gb.MechType.kerberos)

        name2 = gssnames.Name(token=exported_name)

        name2.shouldnt_be_none()

        name2.attributes['urn:greet:greeting'].values.should_be(
            set([b'some val']))
        name2.attributes['urn:greet:greeting'].complete.should_be_true()
        name2.attributes['urn:greet:greeting'].authenticated.should_be_false()

    def test_to_str(self):
        name = gssnames.Name(SERVICE_PRINCIPAL, gb.NameType.kerberos_principal)

        name_str = str(name)

        name_str.should_be_a(str)
        if sys.version_info[0] == 2:
            target_val = SERVICE_PRINCIPAL
        else:
            target_val = SERVICE_PRINCIPAL.decode(gssutils._get_encoding())

        name_str.should_be(target_val)

    def test_to_unicode(self):
        name = gssnames.Name(SERVICE_PRINCIPAL, gb.NameType.kerberos_principal)

        name_str = six.text_type(name)

        name_str.should_be_a(six.text_type)
        name_str.should_be(SERVICE_PRINCIPAL.decode(gssutils._get_encoding()))

    def test_to_bytes(self):
        name = gssnames.Name(SERVICE_PRINCIPAL, gb.NameType.kerberos_principal)

        # NB(directxman12): bytes only calles __bytes__ on Python 3+
        name_bytes = name.__bytes__()

        name_bytes.should_be_a(bytes)
        name_bytes.should_be(SERVICE_PRINCIPAL)

    def test_compare(self):
        name1 = gssnames.Name(SERVICE_PRINCIPAL)
        name2 = gssnames.Name(SERVICE_PRINCIPAL)
        name3 = gssnames.Name(TARGET_SERVICE_NAME,
                              gb.NameType.hostbased_service)

        name1.should_be(name2)
        name1.shouldnt_be(name3)

    def test_canoncialize_and_export(self):
        name = gssnames.Name(SERVICE_PRINCIPAL, gb.NameType.kerberos_principal)
        canonical_name = name.canonicalize(gb.MechType.kerberos)
        exported_name = canonical_name.export()

        exported_name.should_be_a(bytes)

    def test_canonicalize(self):
        name = gssnames.Name(TARGET_SERVICE_NAME,
                             gb.NameType.hostbased_service)

        canonicalized_name = name.canonicalize(gb.MechType.kerberos)
        canonicalized_name.should_be_a(gssnames.Name)
        bytes(canonicalized_name).should_be(SERVICE_PRINCIPAL + b'@')

    def test_copy(self):
        name1 = gssnames.Name(SERVICE_PRINCIPAL)
        name2 = copy.copy(name1)

        name1.should_be(name2)

    # NB(directxman12): we don't test display_name_ext because the krb5 mech
    # doesn't actually implement it

    @ktu.gssapi_extension_test('rfc6680', 'RFC 6680')
    def test_is_mech_name(self):
        name = gssnames.Name(TARGET_SERVICE_NAME,
                             gb.NameType.hostbased_service)

        name.is_mech_name.should_be_false()

        canon_name = name.canonicalize(gb.MechType.kerberos)

        canon_name.is_mech_name.should_be_true()
        canon_name.mech.should_be_a(gb.OID)
        canon_name.mech.should_be(gb.MechType.kerberos)

    @ktu.gssapi_extension_test('rfc6680', 'RFC 6680')
    def test_export_name_composite_no_attrs(self):
        name = gssnames.Name(TARGET_SERVICE_NAME,
                             gb.NameType.hostbased_service)
        canon_name = name.canonicalize(gb.MechType.kerberos)
        exported_name = canon_name.export(composite=True)

        exported_name.should_be_a(bytes)

    @ktu.gssapi_extension_test('rfc6680', 'RFC 6680')
    @ktu.krb_plugin_test('authdata', 'greet_client')
    def test_export_name_composite_with_attrs(self):
        name = gssnames.Name(TARGET_SERVICE_NAME,
                             gb.NameType.hostbased_service)
        canon_name = name.canonicalize(gb.MechType.kerberos)
        canon_name.attributes['urn:greet:greeting'] = b'some val'
        exported_name = canon_name.export(composite=True)

        exported_name.should_be_a(bytes)

    @ktu.gssapi_extension_test('rfc6680', 'RFC 6680')
    @ktu.krb_plugin_test('authdata', 'greet_client')
    def test_basic_get_set_del_name_attribute_no_auth(self):
        name = gssnames.Name(TARGET_SERVICE_NAME,
                             gb.NameType.hostbased_service)
        canon_name = name.canonicalize(gb.MechType.kerberos)

        canon_name.attributes['urn:greet:greeting'] = (b'some val', True)
        canon_name.attributes['urn:greet:greeting'].values.should_be(
            set([b'some val']))
        canon_name.attributes['urn:greet:greeting'].complete.should_be_true()
        (canon_name.attributes['urn:greet:greeting'].authenticated
            .should_be_false())

        del canon_name.attributes['urn:greet:greeting']

        # NB(directxman12): for some reason, the greet:greeting handler plugin
        # doesn't properly delete itself -- it just clears the value
        # If we try to get its value now, we segfault (due to an issue with
        # greet:greeting's delete).  Instead, just try setting the value again
        # canon_name.attributes.should_be_empty(), which would normally give
        # an error.
        canon_name.attributes['urn:greet:greeting'] = b'some other val'


class SecurityContextTestCase(_GSSAPIKerberosTestCase):
    def setUp(self):
        super(SecurityContextTestCase, self).setUp()
        gssctx.SecurityContext.__DEFER_STEP_ERRORS__ = False
        self.client_name = gssnames.Name(self.USER_PRINC)
        self.client_creds = gsscreds.Credentials(name=None,
                                                 usage='initiate')

        self.target_name = gssnames.Name(TARGET_SERVICE_NAME,
                                         gb.NameType.hostbased_service)

        self.server_name = gssnames.Name(SERVICE_PRINCIPAL)
        self.server_creds = gsscreds.Credentials(name=self.server_name,
                                                 usage='accept')

    def _create_client_ctx(self, **kwargs):
        return gssctx.SecurityContext(name=self.target_name, **kwargs)

    # NB(directxman12): we skip testing process_context_token, because there is
    #                   no concrete, non-deprecated was to obtain an "async"
    #                   token

    def test_create_from_other(self):
        raw_client_ctx, raw_server_ctx = self._create_completed_contexts()
        high_level_ctx = gssctx.SecurityContext(raw_client_ctx)
        high_level_ctx.target_name.should_be(self.target_name)

    @exist_perms(lifetime=30, flags=[],
                 mech=gb.MechType.kerberos,
                 channel_bindings=None)
    def test_create_new_init(self, str_name, kwargs):
        client_ctx = gssctx.SecurityContext(name=self.target_name,
                                            creds=self.client_creds,
                                            **kwargs)
        client_ctx.usage.should_be('initiate')

        client_ctx = self._create_client_ctx(**kwargs)
        client_ctx.usage.should_be('initiate')

    def test_create_new_accept(self):
        server_ctx = gssctx.SecurityContext(creds=self.server_creds)
        server_ctx.usage.should_be('accept')

    def test_init_throws_error_on_invalid_args(self):
        def create_sec_context():
            gssctx.SecurityContext(usage='accept', name=self.target_name)

        create_sec_context.should_raise(TypeError)

    def _create_completed_contexts(self):
        client_ctx = self._create_client_ctx(lifetime=400)

        client_token = client_ctx.step()
        client_token.should_be_a(bytes)

        server_ctx = gssctx.SecurityContext(creds=self.server_creds)
        server_token = server_ctx.step(client_token)
        server_token.should_be_a(bytes)

        client_ctx.step(server_token)

        return (client_ctx, server_ctx)

    def test_complete_on_partially_completed(self):
        client_ctx = self._create_client_ctx()
        client_tok = client_ctx.step()
        client_ctx.complete.should_be_false()

        server_ctx = gssctx.SecurityContext(creds=self.server_creds)
        server_tok = server_ctx.step(client_tok)

        client_ctx.step(server_tok)

        client_ctx.complete.should_be_true()
        server_ctx.complete.should_be_true()

    def test_initiate_accept_steps(self):
        client_ctx, server_ctx = self._create_completed_contexts()

        # KDC may allow for clockskew by increasing acceptor context lifetime
        server_ctx.lifetime.should_be_at_most(400 + 300)
        server_ctx.initiator_name.should_be(client_ctx.initiator_name)
        server_ctx.mech.should_be_a(gb.OID)
        server_ctx.actual_flags.should_be_a(gb.IntEnumFlagSet)
        server_ctx.locally_initiated.should_be_false()
        server_ctx.complete.should_be_true()

        client_ctx.lifetime.should_be_at_most(400)
        client_ctx.target_name.should_be(self.target_name)
        client_ctx.mech.should_be_a(gb.OID)
        client_ctx.actual_flags.should_be_a(gb.IntEnumFlagSet)
        client_ctx.locally_initiated.should_be_true()
        client_ctx.complete.should_be_true()

    def test_channel_bindings(self):
        bdgs = gb.ChannelBindings(application_data=b'abcxyz',
                                  initiator_address_type=gb.AddressType.ip,
                                  initiator_address=b'127.0.0.1',
                                  acceptor_address_type=gb.AddressType.ip,
                                  acceptor_address=b'127.0.0.1')
        client_ctx = self._create_client_ctx(lifetime=400,
                                             channel_bindings=bdgs)

        client_token = client_ctx.step()
        client_token.should_be_a(bytes)

        server_ctx = gssctx.SecurityContext(creds=self.server_creds,
                                            channel_bindings=bdgs)
        server_token = server_ctx.step(client_token)
        server_token.should_be_a(bytes)

        client_ctx.step(server_token)

    def test_bad_channel_bindings_raises_error(self):
        bdgs = gb.ChannelBindings(application_data=b'abcxyz',
                                  initiator_address_type=gb.AddressType.ip,
                                  initiator_address=b'127.0.0.1',
                                  acceptor_address_type=gb.AddressType.ip,
                                  acceptor_address=b'127.0.0.1')
        client_ctx = self._create_client_ctx(lifetime=400,
                                             channel_bindings=bdgs)

        client_token = client_ctx.step()
        client_token.should_be_a(bytes)

        bdgs.acceptor_address = b'127.0.1.0'
        server_ctx = gssctx.SecurityContext(creds=self.server_creds,
                                            channel_bindings=bdgs)
        server_ctx.step.should_raise(gb.BadChannelBindingsError, client_token)

    def test_export_create_from_token(self):
        client_ctx, server_ctx = self._create_completed_contexts()
        token = client_ctx.export()

        token.should_be_a(bytes)

        imported_ctx = gssctx.SecurityContext(token=token)

        imported_ctx.usage.should_be('initiate')
        imported_ctx.target_name.should_be(self.target_name)

    def test_pickle_unpickle(self):
        client_ctx, server_ctx = self._create_completed_contexts()
        pickled_ctx = pickle.dumps(client_ctx)

        unpickled_ctx = pickle.loads(pickled_ctx)

        unpickled_ctx.should_be_a(gssctx.SecurityContext)
        unpickled_ctx.usage.should_be('initiate')
        unpickled_ctx.target_name.should_be(self.target_name)

    def test_encrypt_decrypt(self):
        client_ctx, server_ctx = self._create_completed_contexts()

        encrypted_msg = client_ctx.encrypt(b'test message')
        encrypted_msg.should_be_a(bytes)

        decrypted_msg = server_ctx.decrypt(encrypted_msg)
        decrypted_msg.should_be_a(bytes)
        decrypted_msg.should_be(b'test message')

    def test_encrypt_decrypt_throws_error_on_no_encryption(self):
        client_ctx, server_ctx = self._create_completed_contexts()

        wrap_res = client_ctx.wrap(b'test message', False)
        wrap_res.should_be_a(gb.WrapResult)
        wrap_res.encrypted.should_be_false()
        wrap_res.message.should_be_a(bytes)

        server_ctx.decrypt.should_raise(excs.EncryptionNotUsed,
                                        wrap_res.message)

    def test_wrap_unwrap(self):
        client_ctx, server_ctx = self._create_completed_contexts()

        wrap_res = client_ctx.wrap(b'test message', True)
        wrap_res.should_be_a(gb.WrapResult)
        wrap_res.encrypted.should_be_true()
        wrap_res.message.should_be_a(bytes)

        unwrap_res = server_ctx.unwrap(wrap_res.message)
        unwrap_res.should_be_a(gb.UnwrapResult)
        unwrap_res.message.should_be_a(bytes)
        unwrap_res.message.should_be(b'test message')
        unwrap_res.encrypted.should_be_true()

    def test_get_wrap_size_limit(self):
        client_ctx, server_ctx = self._create_completed_contexts()

        with_conf = client_ctx.get_wrap_size_limit(100)
        without_conf = client_ctx.get_wrap_size_limit(100, encrypted=True)

        with_conf.should_be_an_integer()
        without_conf.should_be_an_integer()

        with_conf.should_be_at_most(100)
        without_conf.should_be_at_most(100)

    def test_get_signature(self):
        client_ctx, server_ctx = self._create_completed_contexts()
        mic_token = client_ctx.get_signature(b'some message')

        mic_token.should_be_a(bytes)
        mic_token.shouldnt_be_empty()

    def test_verify_signature_raise(self):
        client_ctx, server_ctx = self._create_completed_contexts()
        mic_token = client_ctx.get_signature(b'some message')

        server_ctx.verify_signature(b'some message', mic_token)

        server_ctx.verify_signature.should_raise(gb.GSSError,
                                                 b'other message', mic_token)

    @ktu.krb_minversion_test("1.11", "returning tokens")
    def test_defer_step_error_on_method(self):
        gssctx.SecurityContext.__DEFER_STEP_ERRORS__ = True
        bdgs = gb.ChannelBindings(application_data=b'abcxyz')
        client_ctx = self._create_client_ctx(lifetime=400,
                                             channel_bindings=bdgs)

        client_token = client_ctx.step()
        client_token.should_be_a(bytes)

        bdgs.application_data = b'defuvw'
        server_ctx = gssctx.SecurityContext(creds=self.server_creds,
                                            channel_bindings=bdgs)
        server_ctx.step(client_token).should_be_a(bytes)
        server_ctx.encrypt.should_raise(gb.BadChannelBindingsError, b'test')

    @ktu.krb_minversion_test("1.11", "returning tokens")
    def test_defer_step_error_on_complete_property_access(self):
        gssctx.SecurityContext.__DEFER_STEP_ERRORS__ = True
        bdgs = gb.ChannelBindings(application_data=b'abcxyz')
        client_ctx = self._create_client_ctx(lifetime=400,
                                             channel_bindings=bdgs)

        client_token = client_ctx.step()
        client_token.should_be_a(bytes)

        bdgs.application_data = b'defuvw'
        server_ctx = gssctx.SecurityContext(creds=self.server_creds,
                                            channel_bindings=bdgs)
        server_ctx.step(client_token).should_be_a(bytes)

        def check_complete():
            return server_ctx.complete

        check_complete.should_raise(gb.BadChannelBindingsError)
