package org.infinispan.server.test.client.hotrod;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;

import org.infinispan.arquillian.core.InfinispanResource;
import org.infinispan.arquillian.core.RemoteInfinispanServer;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryCreated;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryModified;
import org.infinispan.client.hotrod.annotation.ClientCacheEntryRemoved;
import org.infinispan.client.hotrod.annotation.ClientListener;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.client.hotrod.event.ClientCacheEntryCreatedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryModifiedEvent;
import org.infinispan.client.hotrod.event.ClientCacheEntryRemovedEvent;
import org.infinispan.client.hotrod.event.ClientEvent;
import org.infinispan.commons.io.ByteBuffer;
import org.infinispan.commons.io.ByteBufferFactory;
import org.infinispan.commons.io.ByteBufferFactoryImpl;
import org.infinispan.commons.marshall.AbstractMarshaller;
import org.infinispan.commons.marshall.Marshaller;
import org.infinispan.server.test.category.HotRodSingleNode;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.container.test.api.TargetsContainer;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;

/**
 * Tests for the HotRod client receiving events serialized with a custom marshaller.
 *
 * @author Galder Zamarreño
 */
@RunWith(Arquillian.class)
@Category(HotRodSingleNode.class)
public class HotRodCustomMarshallerEventIT {

    private final String TEST_CACHE_NAME = "default";

    static RemoteCacheManager remoteCacheManager;
    RemoteCache<Id, Id> remoteCache;

    @InfinispanResource("container1")
    RemoteInfinispanServer server1;

    @Deployment(testable = false, name = "marshaller")
    @TargetsContainer("container1")
    public static Archive<?> deploy1() {
        return createArchive();
    }

    @Before
    public void initialize() {
        if (remoteCacheManager == null) {
            Configuration config = createRemoteCacheManagerConfiguration();
            remoteCacheManager = new RemoteCacheManager(config, true);
        }
        remoteCache = remoteCacheManager.getCache(TEST_CACHE_NAME);
    }

    private Configuration createRemoteCacheManagerConfiguration() {
        ConfigurationBuilder config = new ConfigurationBuilder();
        for (RemoteInfinispanServer server : getServers()) {
            config.addServer().host(server.getHotrodEndpoint().getInetAddress().getHostName())
                    .port(server.getHotrodEndpoint().getPort());
        }

        config.marshaller("org.infinispan.server.test.client.hotrod.HotRodCustomMarshallerEventIT$IdMarshaller");
        return config.build();
    }

    @AfterClass
    public static void release() {
        if (remoteCacheManager != null) {
            remoteCacheManager.stop();
        }
    }

    private static Archive<?> createArchive() {
        return ShrinkWrap.create(JavaArchive.class, "marshaller.jar")
                .addClasses(Id.class, IdMarshaller.class)
                .addAsServiceProvider(Marshaller.class, IdMarshaller.class);
    }

    private List<RemoteInfinispanServer> getServers() {
        List<RemoteInfinispanServer> servers = new ArrayList<>();
        servers.add(server1);
        return Collections.unmodifiableList(servers);
    }

    @Test
    public void testEventReceiveBasic() {
        final IdEventListener eventListener = new IdEventListener();
        remoteCache.addClientListener(eventListener);
        try {
            // Created events
            remoteCache.put(new Id(1), new Id(11));
            ClientCacheEntryCreatedEvent<Id> created = eventListener.pollEvent();
            assertEquals(new Id(1), created.getKey());
            remoteCache.put(new Id(2), new Id(22));
            created = eventListener.pollEvent();
            assertEquals(new Id(2), created.getKey());
            // Modified events
            remoteCache.put(new Id(1), new Id(111));
            ClientCacheEntryModifiedEvent<Id> modified = eventListener.pollEvent();
            assertEquals(new Id(1), modified.getKey());
            // Remove events
            remoteCache.remove(new Id(1));
            ClientCacheEntryRemovedEvent<Id> removed = eventListener.pollEvent();
            assertEquals(new Id(1), removed.getKey());
            remoteCache.remove(new Id(2));
            removed = eventListener.pollEvent();
            assertEquals(new Id(2), removed.getKey());
        } finally {
            remoteCache.removeClientListener(eventListener);
        }
    }

    @ClientListener
    public static class IdEventListener {
        public BlockingQueue<ClientEvent> events = new ArrayBlockingQueue<>(128);

        @ClientCacheEntryCreated
        @ClientCacheEntryModified
        @ClientCacheEntryRemoved
        public void handleCreatedEvent(ClientEvent e) {
            events.add(e);
        }

        public <E extends ClientEvent> E pollEvent() {
            try {
                E event = (E) events.poll(10, TimeUnit.SECONDS);
                assertNotNull(event);
                return event;
            } catch (InterruptedException e) {
                throw new AssertionError(e);
            }
        }
    }

    public static class Id {
        final byte id;
        public Id(int id) {
            this.id = (byte) id;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            Id id1 = (Id) o;

            return id == id1.id;

        }

        @Override
        public int hashCode() {
            return id;
        }
    }

    public static class IdMarshaller extends AbstractMarshaller {
        @Override
        protected ByteBuffer objectToBuffer(Object o, int estimatedSize) {
            Id obj = (Id) o;
            ByteBufferFactory factory = new ByteBufferFactoryImpl();
            return factory.newByteBuffer(new byte[]{obj.id}, 0, 1);
        }

        @Override
        public Object objectFromByteBuffer(byte[] buf, int offset, int length) {
            return new Id(buf[0]);
        }

        @Override
        public boolean isMarshallable(Object o) throws Exception {
            return true;
        }
    }
}
