/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.curator.framework.imps;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import com.google.common.collect.Lists;
import org.apache.curator.framework.AuthInfo;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.api.BackgroundCallback;
import org.apache.curator.framework.api.CuratorEventType;
import org.apache.curator.framework.state.ConnectionState;
import org.apache.curator.framework.state.ConnectionStateListener;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.curator.retry.RetryOneTime;
import org.apache.curator.test.BaseClassForTests;
import org.apache.curator.test.Timing;
import org.apache.curator.utils.CloseableUtils;
import org.apache.curator.utils.ZKPaths;
import org.apache.curator.x.async.AsyncCuratorFramework;
import org.apache.curator.x.async.AsyncStage;
import org.apache.curator.x.async.api.CreateOption;
import org.apache.curator.x.async.api.DeleteOption;
import org.apache.curator.x.async.api.ExistsOption;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.data.ACL;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

@SuppressWarnings("deprecation")
public class TestFramework extends BaseClassForTests
{
    @BeforeEach
    @Override
    public void setup() throws Exception
    {
        System.setProperty("znode.container.checkIntervalMs", "1000");
        super.setup();
    }

    @AfterEach
    @Override
    public void teardown() throws Exception
    {
        System.clearProperty("znode.container.checkIntervalMs");
        super.teardown();
    }

    @Test
    public void testQuietDelete() throws Exception
    {
        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        try
        {
            client.start();
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);

            async.delete().withOptions(EnumSet.of(DeleteOption.quietly)).forPath("/foo/bar");

            final BlockingQueue<Integer> rc = new LinkedBlockingQueue<>();
            BackgroundCallback backgroundCallback = (client1, event) -> rc.add(event.getResultCode());
            async.delete().withOptions(EnumSet.of(DeleteOption.quietly)).forPath("/foo/bar/hey").handle((v, e) -> {
                if ( e == null )
                {
                    rc.add(KeeperException.Code.OK.intValue());
                }
                else
                {
                    rc.add(((KeeperException)e).code().intValue());
                }
                return null;
            });

            Integer code = rc.poll(new Timing().milliseconds(), TimeUnit.MILLISECONDS);
            assertNotNull(code);
            assertEquals(code.intValue(), KeeperException.Code.OK.intValue());
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testNamespaceWithWatcher() throws Exception
    {
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
        CuratorFramework client = builder.connectString(server.getConnectString()).namespace("aisa").retryPolicy(new RetryOneTime(1)).build();
        client.start();
        try
        {
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            BlockingQueue<String> queue = new LinkedBlockingQueue<String>();
            async.create().forPath("/base").
                thenRun(() -> async.watched().getChildren().forPath("/base").event().handle((event, x) -> {
                    try
                    {
                        queue.put(event.getPath());
                    }
                    catch ( InterruptedException e )
                    {
                        throw new Error(e);
                    }
                    return null;
                }))
                .thenRun(() -> async.create().forPath("/base/child"));

            String path = queue.take();
            assertEquals(path, "/base");
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testCreateACLSingleAuth() throws Exception
    {
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
        CuratorFramework client = builder
            .connectString(server.getConnectString())
            .authorization("digest", "me1:pass1".getBytes())
            .retryPolicy(new RetryOneTime(1))
            .build();
        client.start();
        try
        {
            ACL acl = new ACL(ZooDefs.Perms.WRITE, ZooDefs.Ids.AUTH_IDS);
            List<ACL> aclList = Lists.newArrayList(acl);
            client.create().withACL(aclList).forPath("/test", "test".getBytes());
            client.close();

            // Try setting data with me1:pass1
            client = builder
                .connectString(server.getConnectString())
                .authorization("digest", "me1:pass1".getBytes())
                .retryPolicy(new RetryOneTime(1))
                .build();
            client.start();
            try
            {
                AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
                async.setData().forPath("/test", "test".getBytes()).toCompletableFuture().get();
            }
            catch ( ExecutionException e )
            {
                fail("Auth failed");
            }
            client.close();

            // Try setting data with something:else
            client = builder
                .connectString(server.getConnectString())
                .authorization("digest", "something:else".getBytes())
                .retryPolicy(new RetryOneTime(1))
                .build();
            client.start();
            try
            {
                AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
                async.setData().forPath("/test", "test".getBytes()).toCompletableFuture().get();
                fail("Should have failed with auth exception");
            }
            catch ( ExecutionException e )
            {
                // expected
            }
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testCreateACLMultipleAuths() throws Exception
    {
        // Add a few authInfos
        List<AuthInfo> authInfos = new ArrayList<AuthInfo>();
        authInfos.add(new AuthInfo("digest", "me1:pass1".getBytes()));
        authInfos.add(new AuthInfo("digest", "me2:pass2".getBytes()));

        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
        CuratorFramework client = builder
            .connectString(server.getConnectString())
            .authorization(authInfos)
            .retryPolicy(new RetryOneTime(1))
            .build();
        client.start();
        try
        {
            ACL acl = new ACL(ZooDefs.Perms.WRITE, ZooDefs.Ids.AUTH_IDS);
            List<ACL> aclList = Lists.newArrayList(acl);
            client.create().withACL(aclList).forPath("/test", "test".getBytes());
            client.close();

            // Try setting data with me1:pass1
            client = builder
                .connectString(server.getConnectString())
                .authorization("digest", "me1:pass1".getBytes())
                .retryPolicy(new RetryOneTime(1))
                .build();
            client.start();
            try
            {
                AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
                async.setData().forPath("/test", "test".getBytes()).toCompletableFuture().get();
            }
            catch ( ExecutionException e )
            {
                fail("Auth failed");
            }
            client.close();

            // Try setting data with me1:pass1
            client = builder
                .connectString(server.getConnectString())
                .authorization("digest", "me2:pass2".getBytes())
                .retryPolicy(new RetryOneTime(1))
                .build();
            client.start();
            try
            {
                AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
                async.setData().forPath("/test", "test".getBytes()).toCompletableFuture().get();
            }
            catch ( ExecutionException e )
            {
                fail("Auth failed");
            }
            client.close();

            // Try setting data with something:else
            client = builder
                .connectString(server.getConnectString())
                .authorization("digest", "something:else".getBytes())
                .retryPolicy(new RetryOneTime(1))
                .build();
            client.start();
            try
            {
                AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
                async.setData().forPath("/test", "test".getBytes()).toCompletableFuture().get();
                fail("Should have failed with auth exception");
            }
            catch ( ExecutionException e )
            {
                // expected
            }
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testCreateACLWithReset() throws Exception
    {
        Timing timing = new Timing();
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
        CuratorFramework client = builder
            .connectString(server.getConnectString())
            .sessionTimeoutMs(timing.session())
            .connectionTimeoutMs(timing.connection())
            .authorization("digest", "me:pass".getBytes())
            .retryPolicy(new ExponentialBackoffRetry(100, 5))
            .build();
        client.start();
        try
        {
            final CountDownLatch lostLatch = new CountDownLatch(1);
            ConnectionStateListener listener = (client1, newState) ->
            {
                if ( newState == ConnectionState.LOST )
                {
                    lostLatch.countDown();
                }
            };
            client.getConnectionStateListenable().addListener(listener);

            ACL acl = new ACL(ZooDefs.Perms.WRITE, ZooDefs.Ids.AUTH_IDS);
            List<ACL> aclList = Lists.newArrayList(acl);
            client.create().withACL(aclList).forPath("/test", "test".getBytes());

            server.stop();
            assertTrue(timing.awaitLatch(lostLatch));
            try
            {
                AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
                async.checkExists().forPath("/").toCompletableFuture().get();
                fail("Connection should be down");
            }
            catch ( ExecutionException e )
            {
                // expected
            }

            server.restart();
            try
            {
                AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
                async.setData().forPath("/test", "test".getBytes()).toCompletableFuture().get();
            }
            catch ( ExecutionException e )
            {
                fail("Auth failed", e);
            }
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testCreateParents() throws Exception
    {
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
        CuratorFramework client = builder.connectString(server.getConnectString()).retryPolicy(new RetryOneTime(1)).build();
        client.start();
        try
        {
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            async.create().withOptions(EnumSet.of(CreateOption.createParentsIfNeeded)).forPath("/one/two/three", "foo".getBytes()).toCompletableFuture().get();
            byte[] data = async.getData().forPath("/one/two/three").toCompletableFuture().get();
            assertArrayEquals(data, "foo".getBytes());

            async.create().withOptions(EnumSet.of(CreateOption.createParentsIfNeeded)).forPath("/one/two/another", "bar".getBytes());
            data = async.getData().forPath("/one/two/another").toCompletableFuture().get();
            assertArrayEquals(data, "bar".getBytes());
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testCreateParentContainers() throws Exception
    {
        if ( !checkForContainers() )
        {
            return;
        }

        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
        CuratorFramework client = builder.connectString(server.getConnectString()).retryPolicy(new RetryOneTime(1)).build();
        try
        {
            client.start();
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            async.create().withOptions(EnumSet.of(CreateOption.createParentsAsContainers)).forPath("/one/two/three", "foo".getBytes()).toCompletableFuture().get();
            byte[] data = async.getData().forPath("/one/two/three").toCompletableFuture().get();
            assertArrayEquals(data, "foo".getBytes());

            async.delete().forPath("/one/two/three").toCompletableFuture().get();
            new Timing().sleepABit();

            assertNull(async.checkExists().forPath("/one/two").toCompletableFuture().get());
            new Timing().sleepABit();
            assertNull(async.checkExists().forPath("/one").toCompletableFuture().get());
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testCreateWithProtection() throws ExecutionException, InterruptedException
    {
        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        try
        {
            client.start();
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            String path = async.create().withOptions(Collections.singleton(CreateOption.doProtected)).forPath("/yo").toCompletableFuture().get();
            String node = ZKPaths.getNodeFromPath(path);
            // CURATOR-489: confirm that the node contains a valid UUID, eg '_c_53345f98-9423-4e0c-a7b5-9f819e3ec2e1-yo'
            assertTrue(ProtectedUtils.isProtectedZNode(node));
            assertEquals(ProtectedUtils.normalize(node), "yo");
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    private boolean checkForContainers()
    {
        if ( ZKPaths.getContainerCreateMode() == CreateMode.PERSISTENT )
        {
            System.out.println("Not using CreateMode.CONTAINER enabled version of ZooKeeper");
            return false;
        }
        return true;
    }

    @Test
    public void testCreatingParentsTheSame() throws Exception
    {
        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        try
        {
            client.start();
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);

            assertNull(client.checkExists().forPath("/one/two"));
            async.create().withOptions(EnumSet.of(CreateOption.createParentsAsContainers)).forPath("/one/two/three").toCompletableFuture().get();
            assertNotNull(async.checkExists().forPath("/one/two").toCompletableFuture().get());

            async.delete().withOptions(EnumSet.of(DeleteOption.deletingChildrenIfNeeded)).forPath("/one").toCompletableFuture().get();
            assertNull(client.checkExists().forPath("/one"));

            assertNull(async.checkExists().forPath("/one/two").toCompletableFuture().get());
            async.checkExists().withOptions(EnumSet.of(ExistsOption.createParentsAsContainers)).forPath("/one/two/three").toCompletableFuture().get();
            assertNotNull(async.checkExists().forPath("/one/two").toCompletableFuture().get());
            assertNull(async.checkExists().forPath("/one/two/three").toCompletableFuture().get());
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testExistsCreatingParents() throws Exception
    {
        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        try
        {
            client.start();
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);

            assertNull(async.checkExists().forPath("/one/two").toCompletableFuture().get());
            async.checkExists().withOptions(EnumSet.of(ExistsOption.createParentsAsContainers)).forPath("/one/two/three").toCompletableFuture().get();
            assertNotNull(async.checkExists().forPath("/one/two").toCompletableFuture().get());
            assertNull(async.checkExists().forPath("/one/two/three").toCompletableFuture().get());
            assertNull(async.checkExists().withOptions(EnumSet.of(ExistsOption.createParentsAsContainers)).forPath("/one/two/three").toCompletableFuture().get());
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testSyncNew() throws Exception
    {
        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        client.start();
        try
        {
            client.create().forPath("/head");
            assertNotNull(client.checkExists().forPath("/head"));

            final CountDownLatch latch = new CountDownLatch(1);
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            async.sync().forPath("/head").handle((v, e) -> {
                assertNull(v);
                assertNull(e);
                latch.countDown();
                return null;
            });
            assertTrue(latch.await(10, TimeUnit.SECONDS));
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testBackgroundDelete() throws Exception
    {
        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        client.start();
        try
        {
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            CountDownLatch latch = new CountDownLatch(1);
            async.create().forPath("/head").thenRun(() ->
                async.delete().forPath("/head").handle((v, e) -> {
                    assertNull(v);
                    assertNull(e);
                    latch.countDown();
                    return null;
                })
            );
            assertTrue(latch.await(10, TimeUnit.SECONDS));
            assertNull(client.checkExists().forPath("/head"));
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testBackgroundDeleteWithChildren() throws Exception
    {
        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        client.start();
        try
        {
            client.getCuratorListenable().addListener
                ((client1, event) ->
                {
                    if ( event.getType() == CuratorEventType.DELETE )
                    {
                        assertEquals(event.getPath(), "/one/two");
                        ((CountDownLatch)event.getContext()).countDown();
                    }
                });

            CountDownLatch latch = new CountDownLatch(1);
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            async.create().withOptions(EnumSet.of(CreateOption.createParentsIfNeeded)).forPath("/one/two/three/four").thenRun(() ->
                async.delete().withOptions(EnumSet.of(DeleteOption.deletingChildrenIfNeeded)).forPath("/one/two").handle((v, e) -> {
                    assertNull(v);
                    assertNull(e);
                    latch.countDown();
                    return null;
                })
            );
            assertTrue(latch.await(10, TimeUnit.SECONDS));
            assertNull(client.checkExists().forPath("/one/two"));
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testDeleteGuaranteedWithChildren() throws Exception
    {
        CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder();
        CuratorFramework client = builder.connectString(server.getConnectString()).retryPolicy(new RetryOneTime(1)).build();
        client.start();
        try
        {
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            async.create().withOptions(EnumSet.of(CreateOption.createParentsIfNeeded)).forPath("/one/two/three/four/five/six", "foo".getBytes()).toCompletableFuture().get();
            async.delete().withOptions(EnumSet.of(DeleteOption.guaranteed, DeleteOption.deletingChildrenIfNeeded)).forPath("/one/two/three/four/five").toCompletableFuture().get();
            assertNull(async.checkExists().forPath("/one/two/three/four/five").toCompletableFuture().get());
            async.delete().withOptions(EnumSet.of(DeleteOption.guaranteed, DeleteOption.deletingChildrenIfNeeded)).forPath("/one/two").toCompletableFuture().get();
            assertNull(async.checkExists().forPath("/one/two").toCompletableFuture().get());
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testGetSequentialChildren() throws Exception
    {
        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        client.start();
        try
        {
            Semaphore semaphore = new Semaphore(0);
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            async.create().forPath("/head").thenRun(() -> {
                for ( int i = 0; i < 10; ++i )
                {
                    async.create().withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath("/head/child").thenRun(semaphore::release);
                }
            });

            assertTrue(new Timing().acquireSemaphore(semaphore, 10));
            List<String> children = async.getChildren().forPath("/head").toCompletableFuture().get();
            assertEquals(children.size(), 10);
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }

    @Test
    public void testBackgroundGetDataWithWatch() throws Exception
    {
        final byte[] data1 = {1, 2, 3};
        final byte[] data2 = {4, 5, 6, 7};

        CuratorFramework client = CuratorFrameworkFactory.newClient(server.getConnectString(), new RetryOneTime(1));
        client.start();
        try
        {
            AsyncCuratorFramework async = AsyncCuratorFramework.wrap(client);
            async.create().forPath("/test", data1).toCompletableFuture().get();

            CountDownLatch watchedLatch = new CountDownLatch(1);
            CountDownLatch backgroundLatch = new CountDownLatch(1);
            AsyncStage<byte[]> stage = async.watched().getData().forPath("/test");
            stage.event().handle((event, x) -> {
                assertEquals(event.getPath(), "/test");
                watchedLatch.countDown();
                return null;
            });
            stage.handle((d, x) -> {
                assertArrayEquals(d, data1);
                backgroundLatch.countDown();
                return null;
            });

            assertTrue(backgroundLatch.await(10, TimeUnit.SECONDS));

            async.setData().forPath("/test", data2);
            assertTrue(watchedLatch.await(10, TimeUnit.SECONDS));
            byte[] checkData = async.getData().forPath("/test").toCompletableFuture().get();
            assertArrayEquals(checkData, data2);
        }
        finally
        {
            CloseableUtils.closeQuietly(client);
        }
    }
}
