/**
 * 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.zookeeper.test;

import static org.apache.zookeeper.test.ClientBase.CONNECTION_TIMEOUT;

import java.util.concurrent.CountDownLatch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException.ConnectionLossException;
import org.apache.zookeeper.PortAssignment;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.Watcher.Event.KeeperState;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooKeeper.States;
import org.apache.zookeeper.server.quorum.QuorumPeerTestBase;
import org.junit.Assert;
import org.junit.Test;

public class ObserverTest extends QuorumPeerTestBase implements Watcher{
    protected static final Logger LOG =
        LoggerFactory.getLogger(ObserverTest.class);    
      
    CountDownLatch latch;
    ZooKeeper zk;
    WatchedEvent lastEvent = null;
          
    /**
     * This test ensures two things:
     * 1. That Observers can successfully proxy requests to the ensemble.
     * 2. That Observers don't participate in leader elections.
     * The second is tested by constructing an ensemble where a leader would
     * be elected if and only if an Observer voted. 
     * @throws Exception
     */
    @Test
    public void testObserver() throws Exception {
        ClientBase.setupTestEnv();
        // We expect two notifications before we want to continue        
        latch = new CountDownLatch(2);
        
        final int PORT_QP1 = PortAssignment.unique();
        final int PORT_QP2 = PortAssignment.unique();
        final int PORT_OBS = PortAssignment.unique();
        final int PORT_QP_LE1 = PortAssignment.unique();
        final int PORT_QP_LE2 = PortAssignment.unique();
        final int PORT_OBS_LE = PortAssignment.unique();

        final int CLIENT_PORT_QP1 = PortAssignment.unique();
        final int CLIENT_PORT_QP2 = PortAssignment.unique();
        final int CLIENT_PORT_OBS = PortAssignment.unique();

        
        String quorumCfgSection =
            "electionAlg=3\n" + 
            "server.1=127.0.0.1:" + (PORT_QP1)
            + ":" + (PORT_QP_LE1)
            + "\nserver.2=127.0.0.1:" + (PORT_QP2)
            + ":" + (PORT_QP_LE2)
            + "\nserver.3=127.0.0.1:" 
            + (PORT_OBS)+ ":" + (PORT_OBS_LE) + ":observer";
        String obsCfgSection =  quorumCfgSection + "\npeerType=observer";
        MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
        MainThread q2 = new MainThread(2, CLIENT_PORT_QP2, quorumCfgSection);
        MainThread q3 = new MainThread(3, CLIENT_PORT_OBS, obsCfgSection);
        q1.start();
        q2.start();
        q3.start();
        Assert.assertTrue("waiting for server 1 being up",
                ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP1,
                        CONNECTION_TIMEOUT));
        Assert.assertTrue("waiting for server 2 being up",
                ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP2,
                        CONNECTION_TIMEOUT));
        Assert.assertTrue("waiting for server 3 being up",
                ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_OBS,
                        CONNECTION_TIMEOUT));        
        
        zk = new ZooKeeper("127.0.0.1:" + CLIENT_PORT_OBS,
                ClientBase.CONNECTION_TIMEOUT, this);
        zk.create("/obstest", "test".getBytes(),Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT);
        
        // Assert that commands are getting forwarded correctly
        Assert.assertEquals(new String(zk.getData("/obstest", null, null)), "test");
        
        // Now check that other commands don't blow everything up
        zk.sync("/", null, null);
        zk.setData("/obstest", "test2".getBytes(), -1);
        zk.getChildren("/", false);
        
        Assert.assertEquals(zk.getState(), States.CONNECTED);
        
        LOG.info("Shutting down server 2");
        // Now kill one of the other real servers        
        q2.shutdown();
                
        Assert.assertTrue("Waiting for server 2 to shut down",
                    ClientBase.waitForServerDown("127.0.0.1:"+CLIENT_PORT_QP2, 
                                    ClientBase.CONNECTION_TIMEOUT));

        LOG.info("Server 2 down");

        // Now the resulting ensemble shouldn't be quorate         
        latch.await();        
        Assert.assertNotSame("Client is still connected to non-quorate cluster", 
                KeeperState.SyncConnected,lastEvent.getState());

        LOG.info("Latch returned");

        try {
            Assert.assertFalse("Shouldn't get a response when cluster not quorate!",
                    new String(zk.getData("/obstest", null, null)).equals("test"));
        }
        catch (ConnectionLossException c) {
            LOG.info("Connection loss exception caught - ensemble not quorate (this is expected)");
        }
        
        latch = new CountDownLatch(1);

        LOG.info("Restarting server 2");

        // Bring it back
        q2 = new MainThread(2, CLIENT_PORT_QP2, quorumCfgSection);
        q2.start();
        
        LOG.info("Waiting for server 2 to come up");
        Assert.assertTrue("waiting for server 2 being up",
                ClientBase.waitForServerUp("127.0.0.1:" + CLIENT_PORT_QP2,
                        CONNECTION_TIMEOUT));
        
        LOG.info("Server 2 started, waiting for latch");

        latch.await();
        // It's possible our session expired - but this is ok, shows we 
        // were able to talk to the ensemble
        Assert.assertTrue("Client didn't reconnect to quorate ensemble (state was" +
                lastEvent.getState() + ")",
                (KeeperState.SyncConnected==lastEvent.getState() ||
                KeeperState.Expired==lastEvent.getState())); 

        LOG.info("Shutting down all servers");

        q1.shutdown();
        q2.shutdown();
        q3.shutdown();
        
        LOG.info("Closing zk client");

        zk.close();        
        Assert.assertTrue("Waiting for server 1 to shut down",
                ClientBase.waitForServerDown("127.0.0.1:"+CLIENT_PORT_QP1, 
                                ClientBase.CONNECTION_TIMEOUT));
        Assert.assertTrue("Waiting for server 2 to shut down",
                ClientBase.waitForServerDown("127.0.0.1:"+CLIENT_PORT_QP2, 
                                ClientBase.CONNECTION_TIMEOUT));
        Assert.assertTrue("Waiting for server 3 to shut down",
                ClientBase.waitForServerDown("127.0.0.1:"+CLIENT_PORT_OBS, 
                                ClientBase.CONNECTION_TIMEOUT));
    
    }
    
    /**
     * Implementation of watcher interface.
     */
    public void process(WatchedEvent event) {
        lastEvent = event;
        latch.countDown();
        LOG.info("Latch got event :: " + event);        
    }    
    
    /**
     * This test ensures that an Observer does not elect itself as a leader, or
     * indeed come up properly, if it is the lone member of an ensemble.
     * @throws Exception
     */
    @Test
    public void testObserverOnly() throws Exception {
        ClientBase.setupTestEnv();
        final int CLIENT_PORT_QP1 = PortAssignment.unique();        
        
        String quorumCfgSection =
            "server.1=127.0.0.1:" + (PortAssignment.unique())
            + ":" + (PortAssignment.unique()) + ":observer\npeerType=observer\n";
                    
        MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
        q1.start();
        q1.join(ClientBase.CONNECTION_TIMEOUT);
        Assert.assertFalse(q1.isAlive());
    }    
    
    /**
     * Ensure that observer only comes up when a proper ensemble is configured.
     * (and will not come up with standalone server).
     */
    @Test
    public void testObserverWithStandlone() throws Exception {
        ClientBase.setupTestEnv();
        final int CLIENT_PORT_QP1 = PortAssignment.unique();        

        String quorumCfgSection =
            "server.1=127.0.0.1:" + (PortAssignment.unique())
            + ":" + (PortAssignment.unique()) + ":observer\n"
            + "server.2=127.0.0.1:" + (PortAssignment.unique())
            + ":" + (PortAssignment.unique()) + "\npeerType=observer\n";

        MainThread q1 = new MainThread(1, CLIENT_PORT_QP1, quorumCfgSection);
        q1.start();
        q1.join(ClientBase.CONNECTION_TIMEOUT);
        Assert.assertFalse(q1.isAlive());
    }    

}
