/*
 * 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.flink.runtime.leaderelection;

import org.apache.flink.configuration.Configuration;
import org.apache.flink.configuration.HighAvailabilityOptions;
import org.apache.flink.runtime.highavailability.nonha.embedded.EmbeddedLeaderService;
import org.apache.flink.runtime.testingUtils.TestingUtils;
import org.apache.flink.runtime.util.ZooKeeperUtils;
import org.apache.flink.util.TestLogger;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.test.TestingServer;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.util.Arrays;
import java.util.Collection;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

/**
 * Tests for leader election.
 */
@RunWith(Parameterized.class)
public class LeaderElectionTest extends TestLogger {

	enum LeaderElectionType {
		ZooKeeper,
		Embedded,
		Standalone
	}

	@Parameterized.Parameters(name = "Leader election: {0}")
	public static Collection<LeaderElectionType> parameters () {
		return Arrays.asList(LeaderElectionType.values());
	}

	private final ServiceClass serviceClass;

	public LeaderElectionTest(LeaderElectionType leaderElectionType) {
		switch(leaderElectionType) {
			case ZooKeeper:
				serviceClass = new ZooKeeperServiceClass();
				break;
			case Embedded:
				serviceClass = new EmbeddedServiceClass();
				break;
			case Standalone:
				serviceClass = new StandaloneServiceClass();
				break;
			default:
				throw new IllegalArgumentException(String.format("Unknown leader election type: %s.", leaderElectionType));
		}
	}

	@Before
	public void setup() throws Exception {
		serviceClass.setup();
	}

	@After
	public void teardown() throws Exception {
		serviceClass.teardown();
	}

	@Test
	public void testHasLeadership() throws Exception {
		final LeaderElectionService leaderElectionService = serviceClass.createLeaderElectionService();
		final ManualLeaderContender manualLeaderContender = new ManualLeaderContender();

		try {
			assertThat(leaderElectionService.hasLeadership(UUID.randomUUID()), is(false));

			leaderElectionService.start(manualLeaderContender);

			final UUID leaderSessionId = manualLeaderContender.waitForLeaderSessionId();

			assertThat(leaderElectionService.hasLeadership(leaderSessionId), is(true));
			assertThat(leaderElectionService.hasLeadership(UUID.randomUUID()), is(false));

			leaderElectionService.confirmLeaderSessionID(leaderSessionId);

			assertThat(leaderElectionService.hasLeadership(leaderSessionId), is(true));

			leaderElectionService.stop();

			assertThat(leaderElectionService.hasLeadership(leaderSessionId), is(false));
		} finally {
			manualLeaderContender.rethrowError();
		}
	}

	private static final class ManualLeaderContender implements LeaderContender {

		private static final UUID NULL_LEADER_SESSION_ID = new UUID(0L, 0L);

		private final ArrayBlockingQueue<UUID> leaderSessionIds = new ArrayBlockingQueue<>(10);

		private volatile Exception exception;

		@Override
		public void grantLeadership(UUID leaderSessionID) {
			leaderSessionIds.offer(leaderSessionID);
		}

		@Override
		public void revokeLeadership() {
			leaderSessionIds.offer(NULL_LEADER_SESSION_ID);
		}

		@Override
		public String getAddress() {
			return "foobar";
		}

		@Override
		public void handleError(Exception exception) {
			this.exception = exception;
		}

		void rethrowError() throws Exception {
			if (exception != null) {
				throw exception;
			}
		}

		UUID waitForLeaderSessionId() throws InterruptedException {
			return leaderSessionIds.take();
		}
	}

	private interface ServiceClass {
		void setup() throws Exception;

		void teardown() throws Exception;

		LeaderElectionService createLeaderElectionService() throws Exception;
	}

	private static final class ZooKeeperServiceClass implements ServiceClass {

		private TestingServer testingServer;

		private CuratorFramework client;

		private Configuration configuration;

		@Override
		public void setup() throws Exception {
			try {
				testingServer = new TestingServer();
			} catch (Exception e) {
				throw new RuntimeException("Could not start ZooKeeper testing cluster.", e);
			}

			configuration = new Configuration();

			configuration.setString(HighAvailabilityOptions.HA_ZOOKEEPER_QUORUM, testingServer.getConnectString());
			configuration.setString(HighAvailabilityOptions.HA_MODE, "zookeeper");

			client = ZooKeeperUtils.startCuratorFramework(configuration);
		}

		@Override
		public void teardown() throws Exception {
			if (client != null) {
				client.close();
				client = null;
			}

			if (testingServer != null) {
				testingServer.stop();
				testingServer = null;
			}
		}

		@Override
		public LeaderElectionService createLeaderElectionService() throws Exception {
			return ZooKeeperUtils.createLeaderElectionService(client, configuration);
		}
	}

	private static final class EmbeddedServiceClass implements ServiceClass {
		private EmbeddedLeaderService embeddedLeaderService;

		@Override
		public void setup() {
			embeddedLeaderService = new EmbeddedLeaderService(TestingUtils.defaultExecutionContext());
		}

		@Override
		public void teardown() {
			if (embeddedLeaderService != null) {
				embeddedLeaderService.shutdown();
				embeddedLeaderService = null;
			}
		}

		@Override
		public LeaderElectionService createLeaderElectionService() throws Exception {
			return embeddedLeaderService.createLeaderElectionService();
		}
	}

	private static final class StandaloneServiceClass implements ServiceClass {

		@Override
		public void setup() throws Exception {
			// noop
		}

		@Override
		public void teardown() throws Exception {
			// noop
		}

		@Override
		public LeaderElectionService createLeaderElectionService() throws Exception {
			return new StandaloneLeaderElectionService();
		}
	}
}