import os
import socket
import tempfile
import threading
import time
import uuid
import unittest
from unittest.mock import Mock, MagicMock, patch

import pytest

from kazoo.testing import KazooTestCase
from kazoo.exceptions import (
    AuthFailedError,
    BadArgumentsError,
    BadVersionError,
    ConfigurationError,
    ConnectionClosedError,
    ConnectionLoss,
    InvalidACLError,
    NoAuthError,
    NoNodeError,
    NodeExistsError,
    SessionExpiredError,
    KazooException,
)
from kazoo.protocol.connection import _CONNECTION_DROP
from kazoo.protocol.states import KeeperState, KazooState
from kazoo.tests.util import CI_ZK_VERSION


class TestClientTransitions(KazooTestCase):
    @staticmethod
    def make_event():
        return threading.Event()

    def test_connection_and_disconnection(self):
        states = []
        rc = threading.Event()

        @self.client.add_listener
        def listener(state):
            states.append(state)
            if state == KazooState.CONNECTED:
                rc.set()

        self.client.stop()
        assert states == [KazooState.LOST]
        states.pop()

        self.client.start()
        rc.wait(2)
        assert states == [KazooState.CONNECTED]
        rc.clear()
        states.pop()
        self.expire_session(self.make_event)
        rc.wait(2)

        req_states = [KazooState.LOST, KazooState.CONNECTED]
        assert states == req_states


class TestClientConstructor(unittest.TestCase):
    def _makeOne(self, *args, **kw):
        from kazoo.client import KazooClient

        return KazooClient(*args, **kw)

    def test_invalid_handler(self):
        from kazoo.handlers.threading import SequentialThreadingHandler

        with pytest.raises(ConfigurationError):
            self._makeOne(handler=SequentialThreadingHandler)

    def test_chroot(self):
        assert self._makeOne(hosts="127.0.0.1:2181/").chroot == ""
        assert self._makeOne(hosts="127.0.0.1:2181/a").chroot == "/a"
        assert self._makeOne(hosts="127.0.0.1/a").chroot == "/a"
        assert self._makeOne(hosts="127.0.0.1/a/b").chroot == "/a/b"
        assert (
            self._makeOne(hosts="127.0.0.1:2181,127.0.0.1:2182/a/b").chroot
            == "/a/b"
        )

    def test_connection_timeout(self):
        from kazoo.handlers.threading import KazooTimeoutError

        client = self._makeOne(hosts="127.0.0.1:9")
        assert client.handler.timeout_exception is KazooTimeoutError

        with pytest.raises(KazooTimeoutError):
            client.start(0.1)

    def test_ordered_host_selection(self):
        client = self._makeOne(
            hosts="127.0.0.1:9,127.0.0.2:9/a", randomize_hosts=False
        )
        hosts = [h for h in client.hosts]
        assert hosts == [("127.0.0.1", 9), ("127.0.0.2", 9)]

    def test_invalid_hostname(self):
        client = self._makeOne(hosts="nosuchhost/a")
        timeout = client.handler.timeout_exception
        with pytest.raises(timeout):
            client.start(0.1)

    def test_another_invalid_hostname(self):
        with pytest.raises(ValueError):
            self._makeOne(hosts="/nosuchhost/a")

    def test_retry_options_dict(self):
        from kazoo.retry import KazooRetry

        client = self._makeOne(
            command_retry=dict(max_tries=99), connection_retry=dict(delay=99)
        )
        assert type(client._conn_retry) is KazooRetry
        assert type(client._retry) is KazooRetry
        assert client._retry.max_tries == 99
        assert client._conn_retry.delay == 99


class TestAuthentication(KazooTestCase):
    def _makeAuth(self, *args, **kwargs):
        from kazoo.security import make_digest_acl

        return make_digest_acl(*args, **kwargs)

    def test_auth(self):
        username = uuid.uuid4().hex
        password = uuid.uuid4().hex

        digest_auth = "%s:%s" % (username, password)
        acl = self._makeAuth(username, password, all=True)

        client = self._get_client()
        client.start()
        client.add_auth("digest", digest_auth)
        client.default_acl = (acl,)

        try:
            client.create("/1")
            client.create("/1/2")
            client.ensure_path("/1/2/3")

            eve = self._get_client()

            eve.start()

            with pytest.raises(NoAuthError):
                eve.get("/1/2")

            # try again with the wrong auth token
            eve.add_auth("digest", "badbad:bad")

            with pytest.raises(NoAuthError):
                eve.get("/1/2")

        finally:
            # Ensure we remove the ACL protected nodes
            client.delete("/1", recursive=True)
            eve.stop()
            eve.close()

    def test_connect_auth(self):
        username = uuid.uuid4().hex
        password = uuid.uuid4().hex

        digest_auth = "%s:%s" % (username, password)
        acl = self._makeAuth(username, password, all=True)

        client = self._get_client(auth_data=[("digest", digest_auth)])
        client.start()
        try:
            client.create("/1", acl=(acl,))
            # give ZK a chance to copy data to other node
            time.sleep(0.1)

            with pytest.raises(NoAuthError):
                self.client.get("/1")

        finally:
            client.delete("/1")
            client.stop()
            client.close()

    def test_unicode_auth(self):
        username = r"xe4/\hm"
        password = r"/\xe4hm"
        digest_auth = "%s:%s" % (username, password)
        acl = self._makeAuth(username, password, all=True)

        client = self._get_client()
        client.start()
        client.add_auth("digest", digest_auth)
        client.default_acl = (acl,)

        try:
            client.create("/1")
            client.ensure_path("/1/2/3")

            eve = self._get_client()
            eve.start()

            with pytest.raises(NoAuthError):
                eve.get("/1/2")

            # try again with the wrong auth token
            eve.add_auth("digest", "badbad:bad")

            with pytest.raises(NoAuthError):
                eve.get("/1/2")

        finally:
            # Ensure we remove the ACL protected nodes
            client.delete("/1", recursive=True)
            eve.stop()
            eve.close()

    def test_invalid_auth(self):
        client = self._get_client()
        client.start()

        with pytest.raises(TypeError):
            client.add_auth("digest", ("user", "pass"))

        with pytest.raises(TypeError):
            client.add_auth(None, ("user", "pass"))

    def test_async_auth(self):
        client = self._get_client()
        client.start()
        username = uuid.uuid4().hex
        password = uuid.uuid4().hex
        digest_auth = "%s:%s" % (username, password)
        result = client.add_auth_async("digest", digest_auth)
        assert result.get() is True

    def test_async_auth_failure(self):
        client = self._get_client()
        client.start()
        username = uuid.uuid4().hex
        password = uuid.uuid4().hex
        digest_auth = "%s:%s" % (username, password)

        with pytest.raises(AuthFailedError):
            client.add_auth("unknown-scheme", digest_auth)

    def test_add_auth_on_reconnect(self):
        client = self._get_client()
        client.start()
        client.add_auth("digest", "jsmith:jsmith")
        client._connection._socket.shutdown(socket.SHUT_RDWR)
        while not client.connected:
            time.sleep(0.1)
        assert ("digest", "jsmith:jsmith") in client.auth_data


class TestConnection(KazooTestCase):
    @staticmethod
    def make_event():
        return threading.Event()

    @staticmethod
    def make_condition():
        return threading.Condition()

    def test_chroot_warning(self):
        k = self._get_nonchroot_client()
        k.chroot = "abba"
        try:
            with patch("warnings.warn") as mock_func:
                k.start()
                assert mock_func.called
        finally:
            k.stop()

    def test_session_expire(self):
        from kazoo.protocol.states import KazooState

        cv = self.make_event()

        def watch_events(event):
            if event == KazooState.LOST:
                cv.set()

        self.client.add_listener(watch_events)
        self.expire_session(self.make_event)
        cv.wait(3)
        assert cv.is_set()

    def test_bad_session_expire(self):
        from kazoo.protocol.states import KazooState

        cv = self.make_event()
        ab = self.make_event()

        def watch_events(event):
            if event == KazooState.LOST:
                ab.set()
                raise Exception("oops")
                cv.set()

        self.client.add_listener(watch_events)
        self.expire_session(self.make_event)
        ab.wait(0.5)
        assert ab.is_set()
        cv.wait(0.5)
        assert not cv.is_set()

    def test_state_listener(self):
        from kazoo.protocol.states import KazooState

        states = []
        condition = self.make_condition()

        def listener(state):
            with condition:
                states.append(state)
                condition.notify_all()

        self.client.stop()
        assert self.client.state == KazooState.LOST
        self.client.add_listener(listener)
        self.client.start(5)

        with condition:
            if not states:
                condition.wait(5)

        assert len(states) == 1
        assert states[0] == KazooState.CONNECTED

    def test_invalid_listener(self):
        with pytest.raises(ConfigurationError):
            self.client.add_listener(15)

    def test_listener_only_called_on_real_state_change(self):
        from kazoo.protocol.states import KazooState

        assert self.client.state == KazooState.CONNECTED
        called = [False]
        condition = self.make_event()

        def listener(state):
            called[0] = True
            condition.set()

        self.client.add_listener(listener)
        self.client._make_state_change(KazooState.CONNECTED)
        condition.wait(3)
        assert called[0] is False

    def test_no_connection(self):
        client = self.client
        client.stop()
        assert client.connected is False
        assert client.client_id is None

        with pytest.raises(ConnectionClosedError):
            client.exists("/")

    def test_close_connecting_connection(self):
        client = self.client
        client.stop()
        ev = self.make_event()

        def close_on_connecting(state):
            if state in (KazooState.CONNECTED, KazooState.LOST):
                ev.set()

        client.add_listener(close_on_connecting)
        client.start()

        # Wait until we connect
        ev.wait(5)
        ev.clear()
        self.client._call(_CONNECTION_DROP, client.handler.async_result())

        client.stop()

        # ...and then wait until the connection is lost
        ev.wait(5)

        with pytest.raises(ConnectionClosedError):
            self.client.create("/foobar")

    def test_double_start(self):
        assert self.client.connected is True
        self.client.start()
        assert self.client.connected is True

    def test_double_stop(self):
        self.client.stop()
        assert self.client.connected is False
        self.client.stop()
        assert self.client.connected is False

    def test_restart(self):
        assert self.client.connected is True
        self.client.restart()
        assert self.client.connected is True

    def test_closed(self):
        client = self.client
        client.stop()

        write_sock = client._connection._write_sock

        # close the connection to free the socket
        client.close()
        assert client._connection._write_sock is None

        # sneak in and patch client to simulate race between a thread
        # calling stop(); close() and one running a command
        oldstate = client._state
        client._state = KeeperState.CONNECTED
        client._connection._write_sock = write_sock

        try:
            # simulate call made after write socket is closed
            with pytest.raises(ConnectionClosedError):
                client.exists("/")

            # simulate call made after write socket is set to None
            client._connection._write_sock = None

            with pytest.raises(ConnectionClosedError):
                client.exists("/")

        finally:
            # reset for teardown
            client._state = oldstate
            client._connection._write_sock = None

    def test_watch_trigger_expire(self):
        client = self.client
        cv = self.make_event()

        client.create("/test", b"")

        def test_watch(event):
            cv.set()

        client.get("/test/", watch=test_watch)
        self.expire_session(self.make_event)

        cv.wait(3)
        assert cv.is_set()


class TestClient(KazooTestCase):
    def _makeOne(self, *args):
        from kazoo.handlers.threading import SequentialThreadingHandler

        return SequentialThreadingHandler(*args)

    def _getKazooState(self):
        from kazoo.protocol.states import KazooState

        return KazooState

    def test_server_version_retries_fail(self):
        client = self.client
        side_effects = [
            "",
            "zookeeper.version=",
            "zookeeper.version=1.",
            "zookeeper.ver",
        ]
        client.command = MagicMock()
        client.command.side_effect = side_effects
        with pytest.raises(KazooException):
            client.server_version(retries=len(side_effects) - 1)

    def test_server_version_retries_eventually_ok(self):
        client = self.client
        actual_version = "zookeeper.version=1.2"
        side_effects = []
        for i in range(0, len(actual_version) + 1):
            side_effects.append(actual_version[0:i])
        client.command = MagicMock()
        client.command.side_effect = side_effects
        assert client.server_version(retries=len(side_effects) - 1) == (1, 2)

    def test_client_id(self):
        client_id = self.client.client_id
        assert type(client_id) is tuple
        # make sure password is of correct length
        assert len(client_id[1]) == 16

    def test_connected(self):
        client = self.client
        assert client.connected

    def test_create(self):
        client = self.client
        path = client.create("/1")
        assert path == "/1"
        assert client.exists("/1")

    def test_create_on_broken_connection(self):
        client = self.client
        client.start()

        client._state = KeeperState.EXPIRED_SESSION
        with pytest.raises(SessionExpiredError):
            client.create("/closedpath", b"bar")

        client._state = KeeperState.AUTH_FAILED
        with pytest.raises(AuthFailedError):
            client.create("/closedpath", b"bar")

        client.stop()
        client.close()

        with pytest.raises(ConnectionClosedError):
            client.create("/closedpath", b"bar")

    def test_create_null_data(self):
        client = self.client
        client.create("/nulldata", None)
        value, _ = client.get("/nulldata")
        assert value is None

    def test_create_empty_string(self):
        client = self.client
        client.create("/empty", b"")
        value, _ = client.get("/empty")
        assert value == b""

    def test_create_unicode_path(self):
        client = self.client
        path = client.create("/ascii")
        assert path == "/ascii"
        path = client.create("/\xe4hm")
        assert path == "/\xe4hm"

    def test_create_async_returns_unchrooted_path(self):
        client = self.client
        path = client.create_async("/1").get()
        assert path == "/1"

    def test_create_invalid_path(self):
        client = self.client
        with pytest.raises(TypeError):
            client.create(("a",))
        with pytest.raises(ValueError):
            client.create(".")
        with pytest.raises(ValueError):
            client.create("/a/../b")
        with pytest.raises(BadArgumentsError):
            client.create("/b\x00")
        with pytest.raises(BadArgumentsError):
            client.create("/b\x1e")

    def test_create_invalid_arguments(self):
        from kazoo.security import OPEN_ACL_UNSAFE

        single_acl = OPEN_ACL_UNSAFE[0]
        client = self.client
        with pytest.raises(TypeError):
            client.create("a", acl="all")
        with pytest.raises(TypeError):
            client.create("a", acl=single_acl)
        with pytest.raises(TypeError):
            client.create("a", value=["a"])
        with pytest.raises(TypeError):
            client.create("a", ephemeral="yes")
        with pytest.raises(TypeError):
            client.create("a", sequence="yes")
        with pytest.raises(TypeError):
            client.create("a", makepath="yes")

    def test_create_value(self):
        client = self.client
        client.create("/1", b"bytes")
        data, stat = client.get("/1")
        assert data == b"bytes"

    def test_create_unicode_value(self):
        client = self.client
        with pytest.raises(TypeError):
            client.create("/1", "\xe4hm")

    def test_create_large_value(self):
        client = self.client
        kb_512 = b"a" * (512 * 1024)
        client.create("/1", kb_512)
        assert client.exists("/1")
        mb_2 = b"a" * (2 * 1024 * 1024)
        with pytest.raises(ConnectionLoss):
            client.create("/2", mb_2)

    def test_create_acl_duplicate(self):
        from kazoo.security import OPEN_ACL_UNSAFE

        single_acl = OPEN_ACL_UNSAFE[0]
        client = self.client
        client.create("/1", acl=[single_acl, single_acl])
        acls, stat = client.get_acls("/1")
        # ZK >3.4 removes duplicate ACL entries
        if CI_ZK_VERSION:
            version = CI_ZK_VERSION
        else:
            version = client.server_version()
        assert len(acls) == 1 if version > (3, 4) else 2

    def test_create_acl_empty_list(self):
        from kazoo.security import OPEN_ACL_UNSAFE

        client = self.client
        client.create("/1", acl=[])
        acls, stat = client.get_acls("/1")
        assert acls == OPEN_ACL_UNSAFE

    def test_version_no_connection(self):
        self.client.stop()
        with pytest.raises(ConnectionLoss):
            self.client.server_version()

    def test_create_ephemeral(self):
        client = self.client
        client.create("/1", b"ephemeral", ephemeral=True)
        data, stat = client.get("/1")
        assert data == b"ephemeral"
        assert stat.ephemeralOwner == client.client_id[0]

    def test_create_no_ephemeral(self):
        client = self.client
        client.create("/1", b"val1")
        data, stat = client.get("/1")
        assert not stat.ephemeralOwner

    def test_create_ephemeral_no_children(self):
        from kazoo.exceptions import NoChildrenForEphemeralsError

        client = self.client
        client.create("/1", b"ephemeral", ephemeral=True)
        with pytest.raises(NoChildrenForEphemeralsError):
            client.create("/1/2", b"val1")
        with pytest.raises(NoChildrenForEphemeralsError):
            client.create("/1/2", b"val1", ephemeral=True)

    def test_create_sequence(self):
        client = self.client
        client.create("/folder")
        path = client.create("/folder/a", b"sequence", sequence=True)
        assert path == "/folder/a0000000000"
        path2 = client.create("/folder/a", b"sequence", sequence=True)
        assert path2 == "/folder/a0000000001"
        path3 = client.create("/folder/", b"sequence", sequence=True)
        assert path3 == "/folder/0000000002"

    def test_create_ephemeral_sequence(self):
        basepath = "/" + uuid.uuid4().hex
        realpath = self.client.create(
            basepath, b"sandwich", sequence=True, ephemeral=True
        )
        assert basepath != realpath and realpath.startswith(basepath)
        data, stat = self.client.get(realpath)
        assert data == b"sandwich"

    def test_create_makepath(self):
        self.client.create("/1/2", b"val1", makepath=True)
        data, stat = self.client.get("/1/2")
        assert data == b"val1"

        self.client.create("/1/2/3/4/5", b"val2", makepath=True)
        data, stat = self.client.get("/1/2/3/4/5")
        assert data == b"val2"

        with pytest.raises(NodeExistsError):
            self.client.create("/1/2/3/4/5", b"val2", makepath=True)

    def test_create_makepath_incompatible_acls(self):
        from kazoo.client import KazooClient
        from kazoo.security import make_digest_acl_credential, CREATOR_ALL_ACL

        credential = make_digest_acl_credential("username", "password")
        alt_client = KazooClient(
            self.cluster[0].address + self.client.chroot,
            max_retries=5,
            auth_data=[("digest", credential)],
            handler=self._makeOne(),
        )
        alt_client.start()
        alt_client.create("/1/2", b"val2", makepath=True, acl=CREATOR_ALL_ACL)

        try:
            with pytest.raises(NoAuthError):
                self.client.create("/1/2/3/4/5", b"val2", makepath=True)

        finally:
            alt_client.delete("/", recursive=True)
            alt_client.stop()

    def test_create_no_makepath(self):
        with pytest.raises(NoNodeError):
            self.client.create("/1/2", b"val1")
        with pytest.raises(NoNodeError):
            self.client.create("/1/2", b"val1", makepath=False)

        self.client.create("/1/2", b"val1", makepath=True)
        with pytest.raises(NoNodeError):
            self.client.create("/1/2/3/4", b"val1", makepath=False)

    def test_create_exists(self):
        from kazoo.exceptions import NodeExistsError

        client = self.client
        path = client.create("/1")
        with pytest.raises(NodeExistsError):
            client.create(path)

    def test_create_stat(self):
        if CI_ZK_VERSION:
            version = CI_ZK_VERSION
        else:
            version = self.client.server_version()
        if not version or version < (3, 5):
            pytest.skip("Must use Zookeeper 3.5 or above")
        client = self.client
        path, stat1 = client.create("/1", b"bytes", include_data=True)
        data, stat2 = client.get("/1")
        assert data == b"bytes"
        assert stat1 == stat2

    def test_create_get_set(self):
        nodepath = "/" + uuid.uuid4().hex

        self.client.create(nodepath, b"sandwich", ephemeral=True)

        data, stat = self.client.get(nodepath)
        assert data == b"sandwich"

        newstat = self.client.set(nodepath, b"hats", stat.version)
        assert newstat
        assert newstat.version > stat.version

        # Some other checks of the ZnodeStat object we got
        assert newstat.acl_version == stat.acl_version
        assert newstat.created == stat.ctime / 1000.0
        assert newstat.last_modified == newstat.mtime / 1000.0
        assert newstat.owner_session_id == stat.ephemeralOwner
        assert newstat.creation_transaction_id == stat.czxid
        assert newstat.last_modified_transaction_id == newstat.mzxid
        assert newstat.data_length == newstat.dataLength
        assert newstat.children_count == stat.numChildren
        assert newstat.children_version == stat.cversion

    def test_get_invalid_arguments(self):
        client = self.client
        with pytest.raises(TypeError):
            client.get(("a", "b"))
        with pytest.raises(TypeError):
            client.get("a", watch=True)

    def test_bad_argument(self):
        client = self.client
        client.ensure_path("/1")
        with pytest.raises(TypeError):
            self.client.set("/1", 1)

    def test_ensure_path(self):
        client = self.client
        client.ensure_path("/1/2")
        assert client.exists("/1/2")

        client.ensure_path("/1/2/3/4")
        assert client.exists("/1/2/3/4")

    def test_sync(self):
        client = self.client
        assert client.sync("/") == "/"
        # Albeit surprising, you can sync anything, even what does not exist.
        assert client.sync("/not_there") == "/not_there"

    def test_exists(self):
        nodepath = "/" + uuid.uuid4().hex

        exists = self.client.exists(nodepath)
        assert exists is None

        self.client.create(nodepath, b"sandwich", ephemeral=True)
        exists = self.client.exists(nodepath)
        assert exists
        assert isinstance(exists.version, int)

        multi_node_nonexistent = "/" + uuid.uuid4().hex + "/hats"
        exists = self.client.exists(multi_node_nonexistent)
        assert exists is None

    def test_exists_invalid_arguments(self):
        client = self.client
        with pytest.raises(TypeError):
            client.exists(("a", "b"))
        with pytest.raises(TypeError):
            client.exists("a", watch=True)

    def test_exists_watch(self):
        nodepath = "/" + uuid.uuid4().hex
        event = self.client.handler.event_object()

        def w(watch_event):
            assert watch_event.path == nodepath
            event.set()

        exists = self.client.exists(nodepath, watch=w)
        assert exists is None

        self.client.create(nodepath, ephemeral=True)

        event.wait(1)
        assert event.is_set() is True

    def test_exists_watcher_exception(self):
        nodepath = "/" + uuid.uuid4().hex
        event = self.client.handler.event_object()

        # if the watcher throws an exception, all we can really do is log it
        def w(watch_event):
            assert watch_event.path == nodepath
            event.set()

            raise Exception("test exception in callback")

        exists = self.client.exists(nodepath, watch=w)
        assert exists is None

        self.client.create(nodepath, ephemeral=True)

        event.wait(1)
        assert event.is_set() is True

    def test_create_delete(self):
        nodepath = "/" + uuid.uuid4().hex

        self.client.create(nodepath, b"zzz")

        self.client.delete(nodepath)

        exists = self.client.exists(nodepath)
        assert exists is None

    def test_get_acls(self):
        from kazoo.security import make_digest_acl

        user = "user"
        passw = "pass"
        acl = make_digest_acl(user, passw, all=True)
        client = self.client
        try:
            client.create("/a", acl=[acl])
            client.add_auth("digest", "{}:{}".format(user, passw))
            assert acl in client.get_acls("/a")[0]
        finally:
            client.delete("/a")

    def test_get_acls_invalid_arguments(self):
        client = self.client
        with pytest.raises(TypeError):
            client.get_acls(("a", "b"))

    def test_set_acls(self):
        from kazoo.security import make_digest_acl

        user = "user"
        passw = "pass"
        acl = make_digest_acl(user, passw, all=True)
        client = self.client
        client.create("/a")
        try:
            client.set_acls("/a", [acl])
            client.add_auth("digest", "{}:{}".format(user, passw))
            assert acl in client.get_acls("/a")[0]
        finally:
            client.delete("/a")

    def test_set_acls_empty(self):
        client = self.client
        client.create("/a")
        with pytest.raises(InvalidACLError):
            client.set_acls("/a", [])

    def test_set_acls_no_node(self):
        from kazoo.security import OPEN_ACL_UNSAFE

        client = self.client
        with pytest.raises(NoNodeError):
            client.set_acls("/a", OPEN_ACL_UNSAFE)

    def test_set_acls_invalid_arguments(self):
        from kazoo.security import OPEN_ACL_UNSAFE

        single_acl = OPEN_ACL_UNSAFE[0]
        client = self.client
        with pytest.raises(TypeError):
            client.set_acls(("a", "b"), ())
        with pytest.raises(TypeError):
            client.set_acls("a", single_acl)
        with pytest.raises(TypeError):
            client.set_acls("a", "all")
        with pytest.raises(TypeError):
            client.set_acls("a", [single_acl], "V1")

    def test_set(self):
        client = self.client
        client.create("a", b"first")
        stat = client.set("a", b"second")
        data, stat2 = client.get("a")
        assert data == b"second"
        assert stat == stat2

    def test_set_null_data(self):
        client = self.client
        client.create("/nulldata", b"not none")
        client.set("/nulldata", None)
        value, _ = client.get("/nulldata")
        assert value is None

    def test_set_empty_string(self):
        client = self.client
        client.create("/empty", b"not empty")
        client.set("/empty", b"")
        value, _ = client.get("/empty")
        assert value == b""

    def test_set_invalid_arguments(self):
        client = self.client
        client.create("a", b"first")
        with pytest.raises(TypeError):
            client.set(("a", "b"), b"value")
        with pytest.raises(TypeError):
            client.set("a", ["v", "w"])
        with pytest.raises(TypeError):
            client.set("a", b"value", "V1")

    def test_delete(self):
        client = self.client
        client.ensure_path("/a/b")
        assert "b" in client.get_children("a")
        client.delete("/a/b")
        assert "b" not in client.get_children("a")

    def test_delete_recursive(self):
        client = self.client
        client.ensure_path("/a/b/c")
        client.ensure_path("/a/b/d")
        client.delete("/a/b", recursive=True)
        client.delete("/a/b/c", recursive=True)
        assert "b" not in client.get_children("a")

    def test_delete_invalid_arguments(self):
        client = self.client
        client.ensure_path("/a/b")
        with pytest.raises(TypeError):
            client.delete("/a/b", recursive="all")
        with pytest.raises(TypeError):
            client.delete(("a", "b"))
        with pytest.raises(TypeError):
            client.delete("/a/b", version="V1")

    def test_get_children(self):
        client = self.client
        client.ensure_path("/a/b/c")
        client.ensure_path("/a/b/d")
        assert client.get_children("/a") == ["b"]
        assert set(client.get_children("/a/b")) == set(["c", "d"])
        assert client.get_children("/a/b/c") == []

    def test_get_children2(self):
        client = self.client
        client.ensure_path("/a/b")
        children, stat = client.get_children("/a", include_data=True)
        value, stat2 = client.get("/a")
        assert children == ["b"]
        assert stat2.version == stat.version

    def test_get_children2_many_nodes(self):
        client = self.client
        client.ensure_path("/a/b")
        client.ensure_path("/a/c")
        client.ensure_path("/a/d")
        children, stat = client.get_children("/a", include_data=True)
        value, stat2 = client.get("/a")
        assert set(children) == set(["b", "c", "d"])
        assert stat2.version == stat.version

    def test_get_children_no_node(self):
        client = self.client
        with pytest.raises(NoNodeError):
            client.get_children("/none")
        with pytest.raises(NoNodeError):
            client.get_children("/none", include_data=True)

    def test_get_children_invalid_path(self):
        client = self.client
        with pytest.raises(ValueError):
            client.get_children("../a")

    def test_get_children_invalid_arguments(self):
        client = self.client
        with pytest.raises(TypeError):
            client.get_children(("a", "b"))
        with pytest.raises(TypeError):
            client.get_children("a", watch=True)
        with pytest.raises(TypeError):
            client.get_children("a", include_data="yes")

    def test_invalid_auth(self):
        from kazoo.exceptions import AuthFailedError
        from kazoo.protocol.states import KeeperState

        client = self.client
        client.stop()
        client._state = KeeperState.AUTH_FAILED

        with pytest.raises(AuthFailedError):
            client.get("/")

    def test_client_state(self):
        from kazoo.protocol.states import KeeperState

        assert self.client.client_state == KeeperState.CONNECTED

    def test_update_host_list(self):
        from kazoo.client import KazooClient
        from kazoo.protocol.states import KeeperState

        hosts = self.cluster[0].address
        # create a client with only one server in its list
        client = KazooClient(hosts=hosts)
        client.start()

        # try to change the chroot, not currently allowed
        with pytest.raises(ConfigurationError):
            client.set_hosts(hosts + "/new_chroot")

        # grow the cluster to 3
        client.set_hosts(self.servers)

        # shut down the first host
        try:
            self.cluster[0].stop()
            time.sleep(5)
            assert client.client_state == KeeperState.CONNECTED
        finally:
            self.cluster[0].run()

    # utility for test_request_queuing*
    def _make_request_queuing_client(self):
        from kazoo.client import KazooClient

        server = self.cluster[0]
        handler = self._makeOne()
        # create a client with only one server in its list, and
        # infinite retries
        client = KazooClient(
            hosts=server.address + self.client.chroot,
            handler=handler,
            connection_retry=dict(
                max_tries=-1,
                delay=0.1,
                backoff=1,
                max_jitter=0.0,
                sleep_func=handler.sleep_func,
            ),
        )

        return client, server

    # utility for test_request_queuing*
    def _request_queuing_common(self, client, server, path, expire_session):
        ev_suspended = client.handler.event_object()
        ev_connected = client.handler.event_object()

        def listener(state):
            if state == KazooState.SUSPENDED:
                ev_suspended.set()
            elif state == KazooState.CONNECTED:
                ev_connected.set()

        client.add_listener(listener)

        # wait for the client to connect
        client.start()

        try:
            # force the client to suspend
            server.stop()

            ev_suspended.wait(5)
            assert ev_suspended.is_set()
            ev_connected.clear()

            # submit a request, expecting it to be queued
            result = client.create_async(path)
            assert len(client._queue) != 0
            assert result.ready() is False
            assert client.state == KazooState.SUSPENDED

            # optionally cause a SessionExpiredError to occur by
            # mangling the first byte of the session password.
            if expire_session:
                b0 = b"\x00"
                if client._session_passwd[0] == 0:
                    b0 = b"\xff"
                client._session_passwd = b0 + client._session_passwd[1:]
        finally:
            server.run()

        # wait for the client to reconnect (either with a recovered
        # session, or with a new one if expire_session was set)
        ev_connected.wait(5)
        assert ev_connected.is_set()

        return result

    def test_request_queuing_session_recovered(self):
        path = "/" + uuid.uuid4().hex
        client, server = self._make_request_queuing_client()

        try:
            result = self._request_queuing_common(
                client=client, server=server, path=path, expire_session=False
            )

            assert result.get() == path
            assert client.exists(path) is not None
        finally:
            client.stop()

    def test_request_queuing_session_expired(self):
        path = "/" + uuid.uuid4().hex
        client, server = self._make_request_queuing_client()

        try:
            result = self._request_queuing_common(
                client=client, server=server, path=path, expire_session=True
            )

            assert len(client._queue) == 0
            with pytest.raises(SessionExpiredError):
                result.get()
        finally:
            client.stop()


class TestSSLClient(KazooTestCase):
    def setUp(self):
        if CI_ZK_VERSION and CI_ZK_VERSION < (3, 5):
            pytest.skip("Must use Zookeeper 3.5 or above")
        ssl_path = tempfile.mkdtemp()
        key_path = os.path.join(ssl_path, "key.pem")
        cert_path = os.path.join(ssl_path, "cert.pem")
        cacert_path = os.path.join(ssl_path, "cacert.pem")
        with open(key_path, "wb") as key_file:
            key_file.write(
                self.cluster.get_ssl_client_configuration()["client_key"]
            )
        with open(cert_path, "wb") as cert_file:
            cert_file.write(
                self.cluster.get_ssl_client_configuration()["client_cert"]
            )
        with open(cacert_path, "wb") as cacert_file:
            cacert_file.write(
                self.cluster.get_ssl_client_configuration()["ca_cert"]
            )
        self.setup_zookeeper(
            use_ssl=True, keyfile=key_path, certfile=cert_path, ca=cacert_path
        )

    def test_create(self):
        client = self.client
        path = client.create("/1")
        assert path == "/1"
        assert client.exists("/1")


dummy_dict = {
    "aversion": 1,
    "ctime": 0,
    "cversion": 1,
    "czxid": 110,
    "dataLength": 1,
    "ephemeralOwner": "ben",
    "mtime": 1,
    "mzxid": 1,
    "numChildren": 0,
    "pzxid": 1,
    "version": 1,
}


class TestClientTransactions(KazooTestCase):
    def setUp(self):
        KazooTestCase.setUp(self)
        skip = False
        if CI_ZK_VERSION and CI_ZK_VERSION < (3, 4):
            skip = True
        elif CI_ZK_VERSION and CI_ZK_VERSION >= (3, 4):
            skip = False
        else:
            ver = self.client.server_version()
            if ver[1] < 4:
                skip = True
        if skip:
            pytest.skip("Must use Zookeeper 3.4 or above")

    def test_basic_create(self):
        t = self.client.transaction()
        t.create("/freddy")
        t.create("/fred", ephemeral=True)
        t.create("/smith", sequence=True)
        results = t.commit()
        assert len(results) == 3
        assert results[0] == "/freddy"
        assert results[2].startswith("/smith0") is True

    def test_bad_creates(self):
        args_list = [
            (True,),
            ("/smith", 0),
            ("/smith", b"", "bleh"),
            ("/smith", b"", None, "fred"),
            ("/smith", b"", None, True, "fred"),
        ]

        for args in args_list:
            with pytest.raises(TypeError):
                t = self.client.transaction()
                t.create(*args)

    def test_default_acl(self):
        from kazoo.security import make_digest_acl

        username = uuid.uuid4().hex
        password = uuid.uuid4().hex

        digest_auth = "%s:%s" % (username, password)
        acl = make_digest_acl(username, password, all=True)

        self.client.add_auth("digest", digest_auth)
        self.client.default_acl = (acl,)

        t = self.client.transaction()
        t.create("/freddy")
        results = t.commit()
        assert results[0] == "/freddy"

    def test_basic_delete(self):
        self.client.create("/fred")
        t = self.client.transaction()
        t.delete("/fred")
        results = t.commit()
        assert results[0] is True

    def test_bad_deletes(self):
        args_list = [
            (True,),
            ("/smith", "woops"),
        ]

        for args in args_list:
            with pytest.raises(TypeError):
                t = self.client.transaction()
                t.delete(*args)

    def test_set(self):
        self.client.create("/fred", b"01")
        t = self.client.transaction()
        t.set_data("/fred", b"oops")
        t.commit()
        res = self.client.get("/fred")
        assert res[0] == b"oops"

    def test_bad_sets(self):
        args_list = [(42, 52), ("/smith", False), ("/smith", b"", "oops")]

        for args in args_list:
            with pytest.raises(TypeError):
                t = self.client.transaction()
                t.set_data(*args)

    def test_check(self):
        self.client.create("/fred")
        version = self.client.get("/fred")[1].version
        t = self.client.transaction()
        t.check("/fred", version)
        t.create("/blah")
        results = t.commit()
        assert results[0] is True
        assert results[1] == "/blah"

    def test_bad_checks(self):
        args_list = [(42, 52), ("/smith", "oops")]

        for args in args_list:
            with pytest.raises(TypeError):
                t = self.client.transaction()
                t.check(*args)

    def test_bad_transaction(self):
        from kazoo.exceptions import RolledBackError, NoNodeError

        t = self.client.transaction()
        t.create("/fred")
        t.delete("/smith")
        results = t.commit()
        assert results[0].__class__ == RolledBackError
        assert results[1].__class__ == NoNodeError

    def test_bad_commit(self):
        t = self.client.transaction()
        t.committed = True

        with pytest.raises(ValueError):
            t.commit()

    def test_bad_context(self):
        with pytest.raises(TypeError):
            with self.client.transaction() as t:
                t.check(4232)

    def test_context(self):
        with self.client.transaction() as t:
            t.create("/smith", b"32")
        assert self.client.get("/smith")[0] == b"32"


class TestSessionCallbacks(unittest.TestCase):
    def test_session_callback_states(self):
        from kazoo.protocol.states import KazooState, KeeperState
        from kazoo.client import KazooClient

        client = KazooClient()
        client._handle = 1
        client._live.set()

        result = client._session_callback(KeeperState.CONNECTED)
        assert result is None

        # Now with stopped
        client._stopped.set()
        result = client._session_callback(KeeperState.CONNECTED)
        assert result is None

        # Test several state transitions
        client._stopped.clear()
        client.start_async = lambda: True
        client._session_callback(KeeperState.CONNECTED)
        assert client.state == KazooState.CONNECTED

        client._session_callback(KeeperState.AUTH_FAILED)
        assert client.state == KazooState.LOST

        client._handle = 1
        client._session_callback(-250)
        assert client.state == KazooState.SUSPENDED


class TestCallbacks(KazooTestCase):
    def test_async_result_callbacks_are_always_called(self):
        # create a callback object
        callback_mock = Mock()

        # simulate waiting for a response
        async_result = self.client.handler.async_result()
        async_result.rawlink(callback_mock)

        # begin the procedure to stop the client
        self.client.stop()

        # the response has just been received;
        # this should be on another thread,
        # simultaneously with the stop procedure
        async_result.set_exception(
            Exception("Anything that throws an exception")
        )

        # with the fix the callback should be called
        assert callback_mock.call_count > 0


class TestNonChrootClient(KazooTestCase):
    def test_create(self):
        client = self._get_nonchroot_client()
        assert client.chroot == ""
        client.start()
        node = uuid.uuid4().hex
        path = client.create(node, ephemeral=True)
        client.delete(path)
        client.stop()

    def test_unchroot(self):
        client = self._get_nonchroot_client()
        client.chroot = "/a"
        # Unchroot'ing the chroot path should return "/"
        assert client.unchroot("/a") == "/"
        assert client.unchroot("/a/b") == "/b"
        assert client.unchroot("/b/c") == "/b/c"


class TestReconfig(KazooTestCase):
    def setUp(self):
        KazooTestCase.setUp(self)

        if CI_ZK_VERSION:
            version = CI_ZK_VERSION
        else:
            version = self.client.server_version()
        if not version or version < (3, 5):
            pytest.skip("Must use Zookeeper 3.5 or above")

    def test_no_super_auth(self):
        with pytest.raises(NoAuthError):
            self.client.reconfig(
                joining="server.999=0.0.0.0:1234:2345:observer;3456",
                leaving=None,
                new_members=None,
            )

    def test_add_remove_observer(self):
        def free_sock_port():
            s = socket.socket()
            s.bind(("", 0))
            return s, s.getsockname()[1]

        username = "super"
        password = "test"
        digest_auth = "%s:%s" % (username, password)
        client = self._get_client(auth_data=[("digest", digest_auth)])
        client.start()

        # get ports for election, zab and client endpoints. we need to use
        # ports for which we'd immediately get a RST upon connect(); otherwise
        # the cluster could crash if it gets a SocketTimeoutException:
        # https://issues.apache.org/jira/browse/ZOOKEEPER-2202
        s1, port1 = free_sock_port()
        s2, port2 = free_sock_port()
        s3, port3 = free_sock_port()

        joining = "server.100=0.0.0.0:%d:%d:observer;0.0.0.0:%d" % (
            port1,
            port2,
            port3,
        )
        data, _ = client.reconfig(
            joining=joining, leaving=None, new_members=None
        )
        assert joining.encode("utf8") in data

        data, _ = client.reconfig(
            joining=None, leaving="100", new_members=None
        )
        assert joining.encode("utf8") not in data

        # try to add it again, but a config number in the future
        curver = int(data.decode().split("\n")[-1].split("=")[1], base=16)
        with pytest.raises(BadVersionError):
            self.client.reconfig(
                joining=joining,
                leaving=None,
                new_members=None,
                from_config=curver + 1,
            )

    def test_bad_input(self):
        with pytest.raises(BadArgumentsError):
            self.client.reconfig(
                joining="some thing", leaving=None, new_members=None
            )
