/*
 *
 * 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.bookkeeper.bookie;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.io.File;
import org.apache.bookkeeper.conf.ServerConfiguration;
import org.apache.bookkeeper.conf.TestBKConfiguration;
import org.apache.bookkeeper.discover.RegistrationManager;
import org.apache.bookkeeper.meta.MetadataBookieDriver;
import org.apache.bookkeeper.meta.zk.ZKMetadataBookieDriver;
import org.apache.bookkeeper.stats.NullStatsLogger;
import org.apache.bookkeeper.test.BookKeeperClusterTestCase;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;

/**
 * Testing StateManager cases.
 */
public class StateManagerTest extends BookKeeperClusterTestCase {

    @Rule
    public final TestName runtime = new TestName();
    final ServerConfiguration conf;
    MetadataBookieDriver driver;

    public StateManagerTest(){
        super(0);
        String ledgersPath = "/" + "ledgers" + runtime.getMethodName();
        baseClientConf.setMetadataServiceUri(zkUtil.getMetadataServiceUri(ledgersPath));
        baseConf.setMetadataServiceUri(zkUtil.getMetadataServiceUri(ledgersPath));
        conf = TestBKConfiguration.newServerConfiguration();
        driver = new ZKMetadataBookieDriver();

    }

    @Override
    public void setUp() throws Exception {
        super.setUp();
        zkUtil.createBKEnsemble("/" + runtime.getMethodName());
        File tmpDir = tmpDirs.createNew("stateManger", "test");
        conf.setJournalDirName(tmpDir.getPath())
                .setLedgerDirNames(new String[] { tmpDir.getPath() })
                .setJournalDirName(tmpDir.toString())
                .setMetadataServiceUri(zkUtil.getMetadataServiceUri());
    }

    @Override
    public void tearDown() throws Exception {
        super.tearDown();
        if (driver != null) {
            driver.close();
        }
    }

    /**
     * StateManager can transition between writable mode and readOnly mode if it was not created with readOnly mode.
     */
    @Test
    public void testNormalBookieTransitions() throws Exception {
        driver.initialize(conf, NullStatsLogger.INSTANCE);
        try (RegistrationManager rm = driver.createRegistrationManager();
             BookieStateManager stateManager = new BookieStateManager(conf, rm)) {
            rm.addRegistrationListener(() -> {
                stateManager.forceToUnregistered();
                // schedule a re-register operation
                stateManager.registerBookie(false);
            });
            stateManager.initState();
            stateManager.registerBookie(true).get();

            assertTrue(stateManager.isRunning());
            assertTrue(stateManager.isRegistered());

            stateManager.transitionToReadOnlyMode().get();
            assertTrue(stateManager.isReadOnly());

            stateManager.transitionToWritableMode().get();
            assertTrue(stateManager.isRunning());
            assertFalse(stateManager.isReadOnly());
            stateManager.close();
            assertFalse(stateManager.isRunning());
        }
    }

    @Test
    public void testReadOnlyDisableBookieTransitions() throws Exception {
        conf.setReadOnlyModeEnabled(false);
        // readOnly disabled bk stateManager
        driver.initialize(
            conf,
            NullStatsLogger.INSTANCE);

        RegistrationManager rm = driver.createRegistrationManager();
        BookieStateManager stateManager = new BookieStateManager(conf, rm);
        // simulate sync shutdown logic in bookie
        stateManager.setShutdownHandler(new StateManager.ShutdownHandler() {
            @Override
            public void shutdown(int code) {
                try {
                    if (stateManager.isRunning()) {
                        stateManager.forceToShuttingDown();
                        stateManager.forceToReadOnly();
                    }

                } finally {
                    stateManager.close();
                }
            }
        });
        rm.addRegistrationListener(() -> {
                stateManager.forceToUnregistered();
                // schedule a re-register operation
                stateManager.registerBookie(false);
            });

        stateManager.initState();
        stateManager.registerBookie(true).get();
        assertTrue(stateManager.isRunning());

        stateManager.transitionToReadOnlyMode().get();
        // stateManager2 will shutdown
        assertFalse(stateManager.isRunning());
        // different dimension of bookie state: running <--> down, read <--> write, unregistered <--> registered
        // bookie2 is set to readOnly when shutdown
        assertTrue(stateManager.isReadOnly());
    }

    @Test
    public void testReadOnlyBookieTransitions() throws Exception{
        // readOnlybk, which use override stateManager impl
        File tmpDir = tmpDirs.createNew("stateManger", "test-readonly");
        final ServerConfiguration readOnlyConf = TestBKConfiguration.newServerConfiguration();
        readOnlyConf.setJournalDirName(tmpDir.getPath())
                .setLedgerDirNames(new String[] { tmpDir.getPath() })
                .setJournalDirName(tmpDir.toString())
                .setMetadataServiceUri(zkUtil.getMetadataServiceUri())
                .setForceReadOnlyBookie(true);
        driver.initialize(readOnlyConf, NullStatsLogger.INSTANCE);

        ReadOnlyBookie readOnlyBookie = TestBookieImpl.buildReadOnly(
                new TestBookieImpl.ResourceBuilder(readOnlyConf)
                .withMetadataDriver(driver).build());
        readOnlyBookie.start();
        assertTrue(readOnlyBookie.isRunning());
        assertTrue(readOnlyBookie.isReadOnly());

        // transition has no effect if bookie start with readOnly mode
        readOnlyBookie.getStateManager().transitionToWritableMode().get();
        assertTrue(readOnlyBookie.isRunning());
        assertTrue(readOnlyBookie.isReadOnly());
        readOnlyBookie.shutdown();

    }

    /**
     * Verify the bookie reg.
     */
    @Test
    public void testRegistration() throws Exception {
        driver.initialize(
            conf,
            NullStatsLogger.INSTANCE);

        RegistrationManager rm = driver.createRegistrationManager();
        BookieStateManager stateManager = new BookieStateManager(conf, rm);
        rm.addRegistrationListener(() -> {
                stateManager.forceToUnregistered();
                // schedule a re-register operation
                stateManager.registerBookie(false);
            });

        // simulate sync shutdown logic in bookie
        stateManager.setShutdownHandler(new StateManager.ShutdownHandler() {
            @Override
            public void shutdown(int code) {
                try {
                    if (stateManager.isRunning()) {
                        stateManager.forceToShuttingDown();
                        stateManager.forceToReadOnly();
                    }

                } finally {
                    stateManager.close();
                }
            }
        });
        stateManager.initState();
        // up
        assertTrue(stateManager.isRunning());
        // unregistered
        assertFalse(stateManager.isRegistered());

        stateManager.registerBookie(true).get();
        // registered
        assertTrue(stateManager.isRegistered());
        stateManager.getShutdownHandler().shutdown(ExitCode.OK);
        // readOnly
        assertTrue(stateManager.isReadOnly());
    }

}
