/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.products.ov.channel.cc.commons.spring;

import static java.util.Collections.synchronizedSet;
import static java.util.concurrent.Executors.newSingleThreadExecutor;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;

/**
 * Tests of {@link AbstractApplication}.
 */
public class AbstractApplicationTest {
    private static final String[] ARGS = {"1", "2", "3" };

    private static ExecutorService executor;

    private TestableApplication application;

    @AfterClass
    public static void afterClass() {
        executor.shutdown();
    }

    @BeforeClass
    public static void beforeClass() {
        executor = newSingleThreadExecutor();
    }

    @Before
    public void setUp() throws Exception {
        application = new TestableApplication();
    }

    @Test
    public void testIsShutdown()
            throws InterruptedException, ExecutionException {
        assertFalse(application.isShutdown());
        startApplication();
        assertFalse(application.isShutdown());
        Thread.sleep(1000);
        application.shutdown();
        assertTrue(application.isShutdown());
    }

    @Test
    public void testRun() throws InterruptedException, ExecutionException {
        Future<Void> future = startApplication();
        Thread.sleep(1000);
        application.shutdown();
        future.get();
        assertArrayEquals(ARGS, application.getArgs());
        assertTrue(application.getPhases()
            .containsAll(EnumSet.allOf(Phase.class)));

    }

    @Test
    public void testRunException() throws InterruptedException {
        ApplicationException exception = new ApplicationException("test");
        application.setException(exception);
        Future<Void> future = startApplication();
        try {
            future.get();
            fail("ExecutionException is expected.");
        } catch (ExecutionException e) {
            assertEquals(exception, e.getCause());
        }
    }

    @Test
    public void testRunShutdown()
            throws InterruptedException, ExecutionException {
        application.shutdown();
        startApplication().get();
        assertEquals(1, application.getPhases().size());
        assertTrue(application.getPhases().contains(Phase.START));
    }

    @Test
    public void testShutdownApplicationException()
            throws InterruptedException {
        Future<Void> future = startApplication();
        ApplicationException exception = new ApplicationException("test");
        application.shutdown(exception);
        try {
            future.get();
            fail("Execution exception is expected");
        } catch (ExecutionException e) {
            assertEquals(exception, e.getCause());
        }
    }

    @Test
    @Ignore
    public void testShutdownApplicationExceptionTwice()
            throws InterruptedException {
        Future<Void> future = startApplication();
        ApplicationException exception1 = new ApplicationException("test");
        application.shutdown(exception1);
        ApplicationException exception2 = new ApplicationException("test");
        application.shutdown(exception2);
        try {
            future.get();
            fail("Execution exception is expected");
        } catch (ExecutionException e) {
            assertEquals(exception1, e.getCause());
            assertEquals(exception2, exception1.getSuppressed()[0]);
        }
    }

    @Test
    public void testShutdownTwice() throws InterruptedException {
        Future<Void> future = startApplication();
        ApplicationException exception = new ApplicationException("test");
        application.shutdown(exception);
        application.shutdown();
        try {
            future.get();
            fail("Execution exception is expected");
        } catch (ExecutionException e) {
            assertEquals(exception, e.getCause());
        }
    }

    @Test
    public void testWaitForShutdown()
            throws InterruptedException, ExecutionException {
        Future<Void> future = startApplication();
        Thread.sleep(1000);
        assertFalse(future.isDone());
        assertEquals(2, application.getPhases().size());
        assertTrue(application.getPhases()
            .containsAll(EnumSet.of(Phase.START, Phase.RUNNING)));
        application.shutdown();
        future.get();
        assertTrue(application.getPhases()
            .containsAll(EnumSet.allOf(Phase.class)));
    }

    private Future<Void> startApplication() {
        return executor.submit(() -> {
            application.run(ARGS);
            return null;
        });
    }

    private static enum Phase {
        START, RUNNING, STOP
    }

    private static class TestableApplication extends AbstractApplication {
        private final Set<Phase> phases =
            synchronizedSet(EnumSet.noneOf(Phase.class));

        private ApplicationException exception;

        private String[] args;

        public String[] getArgs() {
            return args;
        }

        public Set<Phase> getPhases() {
            return phases;
        }

        public void setException(ApplicationException exception) {
            this.exception = exception;
        }

        @Override
        protected void doRun(String[] args) throws ApplicationException {
            this.args = args;
            phases.add(Phase.START);
            if (!isShutdown()) {
                phases.add(Phase.RUNNING);
                if (exception != null) {
                    throw exception;
                }
                waitForShutdown();
                phases.add(Phase.STOP);
            }
        }
    }
}
