package com.atlassian.stash.internal.maintenance;

import com.atlassian.hazelcast.test.HazelcastCluster;
import com.atlassian.johnson.Johnson;
import com.atlassian.johnson.event.EventLevel;
import com.atlassian.johnson.event.EventType;
import com.hazelcast.core.ISet;
import org.junit.*;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.core.io.ClassPathResource;

import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

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

@RunWith(MockitoJUnitRunner.class)
public class HazelcastMaintenanceModeHelperTest {

    // maximum wait time for ItemListener callback
    private final long MAX_WAIT = 2000L;

    @ClassRule
    public static HazelcastCluster cluster = new HazelcastCluster.Builder().size(2).build();
    private MockHelper localHelper;

    @BeforeClass
    public static void setupJohnson() throws IOException {
        Johnson.initialize(new ClassPathResource("minimal-johnson-config.xml").getURL().getPath());
    }

    @AfterClass
    public static void tearDownJohnson() {
        Johnson.terminate();
    }

    @Before
    public void setup() {
        cluster.reset();
        localHelper = new MockHelper();
    }

    @Test
    public void testJoinClusterDefault() throws Exception {
        ISet<JohnsonMaintenanceEvent> events = getEventsSet();
        HazelcastMaintenanceModeHelper helper = new HazelcastMaintenanceModeHelper(events, localHelper);
        helper.init();

        // there are no events in the cluster-wide set -> the cluster is not in maintenance mode. The local node should
        // not be locked
        assertFalse(localHelper.wasLocked(0));
    }

    @Test
    public void testJoinClusterInMaintenanceMode() throws Exception {
        // prepare the cluster state
        JohnsonMaintenanceEvent event = createMaintenanceEvent();
        ISet<JohnsonMaintenanceEvent> events = getEventsSet();
        events.add(event);

        // now create a new helper
        HazelcastMaintenanceModeHelper helper = new HazelcastMaintenanceModeHelper(events, localHelper);
        helper.init();

        assertTrue(localHelper.wasLocked(0));
    }

    @Test
    public void testLockAndUnlockLocally() throws Exception {
        ISet<JohnsonMaintenanceEvent> events = getEventsSet();
        HazelcastMaintenanceModeHelper helper = new HazelcastMaintenanceModeHelper(events, localHelper);
        helper.init();

        // lock locally
        JohnsonMaintenanceEvent event = createMaintenanceEvent();
        helper.lock(event);
        assertTrue(localHelper.wasLocked(MAX_WAIT)); // locking happens async through the ItemListener
        assertEquals(1, events.size());

        helper.unlock(event);
        assertTrue(localHelper.wasUnlocked(MAX_WAIT)); // unlocking happens async through the ItemListener
        assertTrue(events.isEmpty());
    }

    @Test
    public void testLockAndUnlockRemotely() throws Exception {
        ISet<JohnsonMaintenanceEvent> events = getEventsSet();
        HazelcastMaintenanceModeHelper helper = new HazelcastMaintenanceModeHelper(events, localHelper);
        helper.init();

        // lock remotely - add the event to the events set
        JohnsonMaintenanceEvent event = createMaintenanceEvent();
        events.add(event);

        assertTrue(localHelper.wasLocked(MAX_WAIT)); // locking happens async through the ItemListener

        // unlock remotely - remove the event from the events set
        events.remove(event);
        assertTrue(localHelper.wasUnlocked(MAX_WAIT)); // unlocking happens async through the ItemListener
    }

    private JohnsonMaintenanceEvent createMaintenanceEvent() {
        return new TaskMaintenanceEvent(EventType.get("performing-maintenance"), "backup task",
                EventLevel.get("maintenance"), MaintenanceType.BACKUP);
    }

    private ISet<JohnsonMaintenanceEvent> getEventsSet() {
        return cluster.getNode(0).getSet("maintenance.events");
    }

    private static class MockHelper implements MaintenanceModeHelper {

        private CountDownLatch lockLatch = new CountDownLatch(1);
        private CountDownLatch unlockLatch = new CountDownLatch(1);

        @Override
        public void lock(@Nonnull JohnsonMaintenanceEvent event) {
            lockLatch.countDown();
        }

        @Override
        public void unlock(@Nonnull JohnsonMaintenanceEvent event) {
            unlockLatch.countDown();
        }

        boolean wasLocked(long timeoutMillis) throws InterruptedException {
            return lockLatch.await(timeoutMillis, TimeUnit.SECONDS);
        }

        boolean wasUnlocked(long timeoutMillis) throws InterruptedException {
            return unlockLatch.await(timeoutMillis, TimeUnit.MILLISECONDS);
        }
    }
}
