/*
 * Copyright 2013 NGDATA nv
 *
 * Licensed 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 com.ngdata.hbaseindexer.impl;

import com.google.common.base.Charsets;
import com.ngdata.hbaseindexer.model.api.IndexerDefinition;
import com.ngdata.hbaseindexer.model.api.IndexerDefinitionBuilder;
import com.ngdata.hbaseindexer.model.api.IndexerModelEvent;
import com.ngdata.hbaseindexer.model.api.IndexerModelEventType;
import com.ngdata.hbaseindexer.model.api.IndexerModelListener;
import com.ngdata.hbaseindexer.model.api.IndexerUpdateException;
import com.ngdata.hbaseindexer.model.api.WriteableIndexerModel;
import com.ngdata.hbaseindexer.model.impl.IndexerModelImpl;
import com.ngdata.sep.util.io.Closer;
import com.ngdata.sep.util.zookeeper.ZkUtil;
import com.ngdata.sep.util.zookeeper.ZooKeeperItf;

import org.apache.commons.io.FileUtils;
import org.apache.hadoop.hbase.zookeeper.MiniZooKeeperCluster;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import javax.management.MBeanServerConnection;
import javax.management.ObjectName;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

import static com.ngdata.hbaseindexer.model.api.IndexerDefinition.IncrementalIndexingState;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class IndexerModelImplTest {
    private static MiniZooKeeperCluster ZK_CLUSTER;
    private static File ZK_DIR;
    private static int ZK_CLIENT_PORT;

    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        ZK_DIR = new File(System.getProperty("java.io.tmpdir") + File.separator + "hbaseindexer.zklocktest");
        FileUtils.deleteDirectory(ZK_DIR);
        ZK_CLIENT_PORT = getFreePort();

        ZK_CLUSTER = new MiniZooKeeperCluster();
        ZK_CLUSTER.setDefaultClientPort(ZK_CLIENT_PORT);
        ZK_CLUSTER.startup(ZK_DIR);
    }

    @AfterClass
    public static void tearDownAfterClass() throws Exception {
        if (ZK_CLUSTER != null) {
            ZK_CLUSTER.shutdown();
        }
    }

    @Test
    public void testEvents() throws Exception {
        ZooKeeperItf zk1 = ZkUtil.connect("localhost:" + ZK_CLIENT_PORT, 15000);
        ZooKeeperItf zk2 = ZkUtil.connect("localhost:" + ZK_CLIENT_PORT, 15000);
        WriteableIndexerModel model1 = null;
        WriteableIndexerModel model2 = null;
        try {
            TestListener listener = new TestListener();

            model1 = new IndexerModelImpl(zk1, "/test");
            model1.registerListener(listener);

            // Create an indexer -- verify INDEXER_ADDED event
            IndexerDefinition indexer1 = new IndexerDefinitionBuilder()
                    .name("indexer1")
                    .configuration("my-conf".getBytes("UTF-8"))
                    .build();
            model1.addIndexer(indexer1);

            listener.waitForEvents(1);
            listener.verifyEvents(new IndexerModelEvent(IndexerModelEventType.INDEXER_ADDED, "indexer1"));

            // Verify that a fresh indexer model has the index
            model2 = new IndexerModelImpl(zk2, "/test");
            Collection<IndexerDefinition> indexers = model2.getIndexers();
            assertEquals("Expected indexer1, got " + indexers, 1, indexers.size());
            assertTrue(model2.hasIndexer("indexer1"));

            // Update the indexer -- verify INDEXER_UPDATED event
            indexer1 = new IndexerDefinitionBuilder()
                    .startFrom(indexer1)
                    .incrementalIndexingState(IncrementalIndexingState.SUBSCRIBE_DO_NOT_CONSUME)
                    .build();
            String lock = model1.lockIndexer("indexer1");
            model1.updateIndexer(indexer1, lock);

            listener.waitForEvents(1);
            listener.verifyEvents(new IndexerModelEvent(IndexerModelEventType.INDEXER_UPDATED, "indexer1"));
            model1.unlockIndexer(lock);

            // Delete the indexer -- verify INDEXER_DELETED event
            model1.deleteIndexerInternal("indexer1");
            listener.waitForEvents(1);
            listener.verifyEvents(new IndexerModelEvent(IndexerModelEventType.INDEXER_DELETED, "indexer1"));

            // Create some more indexes and verify we get the correct number of INDEXER_ADDED events
            IndexerModelEvent[] expectedEvents = new IndexerModelEvent[9];
            for (int i = 2; i <= 10; i++) {
                String name = "indexer" + i;
                IndexerDefinition indexer = new IndexerDefinitionBuilder()
                        .name(name)
                        .configuration("my-conf".getBytes("UTF-8"))
                        .build();
                model1.addIndexer(indexer);
                expectedEvents[i - 2] = new IndexerModelEvent(IndexerModelEventType.INDEXER_ADDED, name);
            }

            listener.waitForEvents(9);
            listener.verifyEvents(expectedEvents);

            // Terminate ZK connections: clients should automatically re-establish the connection and things
            // should work as before
            assertEquals(2, terminateZooKeeperConnections());

            // Do another index update and check we get an event
            IndexerDefinition indexer2 = new IndexerDefinitionBuilder()
                    .name("indexer2")
                    .incrementalIndexingState(IncrementalIndexingState.DO_NOT_SUBSCRIBE)
                    .configuration("my-conf".getBytes(Charsets.UTF_8))
                    .build();
            lock = model1.lockIndexer("indexer2");
            model1.updateIndexer(indexer2, lock);
            model1.unlockIndexer(lock);

            listener.waitForEvents(1);
            listener.verifyEvents(new IndexerModelEvent(IndexerModelEventType.INDEXER_UPDATED, "indexer2"));
        } finally {
            Closer.close(model1);
            Closer.close(model2);
            Closer.close(zk1);
            Closer.close(zk2);
        }
    }

    @Test
    public void testLocking() throws Exception {
        ZooKeeperItf zk1 = ZkUtil.connect("localhost:" + ZK_CLIENT_PORT, 15000);
        ZooKeeperItf zk2 = ZkUtil.connect("localhost:" + ZK_CLIENT_PORT, 15000);
        WriteableIndexerModel model1 = null;
        WriteableIndexerModel model2 = null;
        String indexerName = "lock_test_indexer";
        try {
            model1 = new IndexerModelImpl(zk1, "/test");
            model2 = new IndexerModelImpl(zk2, "/test");

            // Create an index
            IndexerDefinition indexer1 = new IndexerDefinitionBuilder()
                    .name(indexerName)
                    .configuration("foo".getBytes(Charsets.UTF_8))
                    .build();
            model1.addIndexer(indexer1);

            // Lock the index via the first client
            String lock = model1.lockIndexer(indexerName);

            // Try to update it via the second client
            indexer1 = new IndexerDefinitionBuilder()
                    .startFrom(indexer1)
                    .configuration("foo1".getBytes(Charsets.UTF_8))
                    .build();

            try {
                model2.updateIndexer(indexer1, lock + "foo");
                fail("Expected exception");
            } catch (IndexerUpdateException e) {
                // verify the exception says something about locks
                assertTrue(e.getMessage().contains("lock"));
            }

            // First client should be able to do the update though
            model1.updateIndexer(indexer1, lock);

            model1.unlockIndexer(lock);
            
            model1.deleteIndexerInternal(indexerName);
        } finally {
            Closer.close(model1);
            Closer.close(model2);
            Closer.close(zk1);
            Closer.close(zk2);
        }
    }

    private class TestListener implements IndexerModelListener {
        private Set<IndexerModelEvent> events = new HashSet<IndexerModelEvent>();

        @Override
        public void process(IndexerModelEvent event) {
            synchronized (this) {
                events.add(event);
                notifyAll();
            }
        }

        public void waitForEvents(int count) throws InterruptedException {
            long timeout = 1000;
            long now = System.currentTimeMillis();
            synchronized (this) {
                while (events.size() < count && System.currentTimeMillis() - now < timeout) {
                    wait(timeout);
                }
            }
        }

        public void verifyEvents(IndexerModelEvent... expectedEvents) {
            if (events.size() != expectedEvents.length) {
                if (events.size() > 0) {
                    System.out.println("The events are:");
                    for (IndexerModelEvent item : events) {
                        System.out.println(item.getType() + " - " + item.getIndexerName());
                    }
                } else {
                    System.out.println("There are no events.");
                }

                assertEquals("Expected number of events", expectedEvents.length, events.size());
            }

            Set<IndexerModelEvent> expectedEventsSet  = new HashSet<IndexerModelEvent>(Arrays.asList(expectedEvents));

            for (IndexerModelEvent event : expectedEvents) {
                if (!events.contains(event)) {
                    fail("Expected event not present among events: " + event);
                }
            }

            for (IndexerModelEvent event : events) {
                if (!expectedEventsSet.contains(event)) {
                    fail("Got an event which is not among the expected events: " + event);
                }
            }

            events.clear();
        }
    }

    public static int getFreePort() {
        ServerSocket socket = null;
        try {
            socket = new ServerSocket(0);
            return socket.getLocalPort();
        } catch (IOException e) {
            throw new RuntimeException("Error finding a free port", e);
        } finally {
            if (socket != null) {
                try {
                    socket.close();
                } catch (IOException e) {
                    throw new RuntimeException("Error closing ServerSocket used to detect a free port.", e);
                }
            }
        }
    }

    public int terminateZooKeeperConnections() throws Exception {
        MBeanServerConnection connection = java.lang.management.ManagementFactory.getPlatformMBeanServer();
        ObjectName replicationSources = new ObjectName("org.apache.ZooKeeperService:name0=*,name1=Connections,name2=*,name3=*");
        Set<ObjectName> mbeans = connection.queryNames(replicationSources, null);
        int connectionCount = mbeans.size();

        for (ObjectName name : mbeans) {
            connection.invoke(name, "terminateConnection", new Object[] {}, new String[] {});
        }

        return connectionCount;
    }
}