package org.xbib.logging.test;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.UUID;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xbib.logging.ExtHandler;
import org.xbib.logging.ExtLogRecord;
import org.xbib.logging.LogContext;
import org.xbib.logging.Logger;
import org.xbib.logging.LoggerNode;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

public class LogContextCloseTests {

    public LogContextCloseTests() {
    }

    @BeforeEach
    public void resetTestObjects() {
        TestErrorManager.POJO_OBJECT = null;
        TestFilter.POJO_OBJECT = null;
        TestFormatter.POJO_OBJECT = null;
        TestHandler.ERROR_MANAGER = null;
        TestHandler.FILTER = null;
        TestHandler.FORMATTER = null;
        TestHandler.HANDLERS = null;
        TestHandler.IS_CLOSED = false;
        TestHandler.POJO_OBJECT = null;
    }

    @Test
    public void testCloseLogContext() throws Exception {
        LogContext logContext = LogContext.create();

        // Create a test handler to use
        final TestHandler handler = new TestHandler();
        handler.setErrorManager(new TestErrorManager());
        handler.setFilter(new TestFilter());
        handler.setFormatter(new TestFormatter());
        handler.setLevel(org.xbib.logging.Level.TRACE);

        final Logger rootLogger = logContext.getLogger("");
        rootLogger.setLevel(org.xbib.logging.Level.WARN);
        final Logger testLogger = logContext.getLogger(LogContextCloseTests.class.getName());
        testLogger.setLevel(Level.FINE);
        final Logger randomLogger = logContext.getLogger(UUID.randomUUID().toString());
        randomLogger.setUseParentFilters(true);

        rootLogger.addHandler(handler);

        logContext.close();

        // Loggers should have no handlers and have been reset
        assertEquals(Level.INFO, rootLogger.getLevel());
        final Handler[] handlers = randomLogger.getHandlers();
        assertTrue(handlers == null || handlers.length == 0);

        assertEmptyContext(logContext, rootLogger, testLogger, randomLogger);
    }

    @Test
    public void testCloseWithAttachment() throws Exception {
        LogContext logContext = LogContext.create();
        final Logger.AttachmentKey<String> key = new Logger.AttachmentKey<>();
        final String value = "test value";
        Logger rootLogger = logContext.getLogger("");
        assertNull(rootLogger.attach(key, value));

        // Close and ensure the context is clean
        logContext.close();
        assertNull(rootLogger.getAttachment(key));
        assertEmptyContext(logContext, rootLogger);

        // Test attachIfAbsent()
        logContext = LogContext.create();
        rootLogger = logContext.getLogger("");
        assertNull(rootLogger.attachIfAbsent(key, value));

        // Close and ensure the context is clean
        logContext.close();
        assertNull(rootLogger.getAttachment(key));
        assertEmptyContext(logContext, rootLogger);

        // Test detach()
        logContext = LogContext.create();
        rootLogger = logContext.getLogger("");
        assertNull(rootLogger.attach(key, value));
        assertEquals(value, rootLogger.detach(key));
        logContext.close();
        assertNull(rootLogger.getAttachment(key));
        assertEmptyContext(logContext, rootLogger);
    }

    private void assertEmptyContext(final LogContext logContext, final Logger... loggers) {
        // Inspect the log context and ensure it's "empty"
        final LoggerNode rootLogger = logContext.getRootLoggerNode();
        final Handler[] handlers = rootLogger.getHandlers();
        assertTrue(handlers == null || handlers.length == 0, "Expected the handlers to be removed.");
        assertNull(rootLogger.getFilter(), "Expected the filter to be null");
        assertEquals(Level.INFO, rootLogger.getLevel(), "Expected the level to be INFO for logger the root logger");
        assertFalse(rootLogger.getUseParentFilters(),
                "Expected the useParentFilters to be false for the root logger");
        assertTrue(rootLogger.getUseParentHandlers(),
                "Expected the useParentHandlers to be true for the root logger");
        final Collection<LoggerNode> children = rootLogger.getChildren();
        if (!children.isEmpty()) {
            final StringBuilder msg = new StringBuilder(
                    "Expected no children to be remaining on the root logger. Remaining loggers: ");
            final Iterator<LoggerNode> iter = children.iterator();
            while (iter.hasNext()) {
                msg.append('\'').append(iter.next().getFullName()).append('\'');
                if (iter.hasNext()) {
                    msg.append(", ");
                }
            }
            fail(msg.toString());
        }

        for (Logger logger : loggers) {
            assertLoggerReset(logger);
        }
    }

    private void assertLoggerReset(final Logger logger) {
        String loggerName = logger.getName();
        final Level expectedLevel;
        if ("".equals(loggerName)) {
            loggerName = "root";
            expectedLevel = Level.INFO;
        } else {
            expectedLevel = null;
        }
        final Handler[] handlers = logger.getHandlers();
        assertNull(logger.getFilter(), "Expected the filter to be null for logger " + loggerName);
        assertTrue(handlers == null || handlers.length == 0, "Empty handlers expected for logger " + loggerName);
        assertEquals(expectedLevel, logger.getLevel(),
                "Expected the level to be " + expectedLevel + " for logger " + loggerName);
        assertFalse(logger.getUseParentFilters(),
                "Expected the useParentFilters to be false for logger " + loggerName);
        assertTrue(logger.getUseParentHandlers(),
                "Expected the useParentHandlers to be true for logger " + loggerName);
    }

    @SuppressWarnings("unused")
    public static class TestFilter implements Filter {

        private static PojoObject POJO_OBJECT;

        public TestFilter() {
        }

        @Override
        public boolean isLoggable(final LogRecord record) {
            return true;
        }

        public void setPojoObject(final PojoObject pojoObject) {
            POJO_OBJECT = pojoObject;
        }
    }

    @SuppressWarnings("unused")
    public static class TestFormatter extends Formatter {

        private static PojoObject POJO_OBJECT;

        public TestFormatter() {
        }

        public void setPojoObject(final PojoObject pojoObject) {
            POJO_OBJECT = pojoObject;
        }

        @Override
        public String format(final LogRecord record) {
            return ExtLogRecord.wrap(record).getFormattedMessage();
        }
    }

    @SuppressWarnings("unused")
    public static class TestErrorManager extends ErrorManager {

        private static PojoObject POJO_OBJECT;

        public TestErrorManager() {
        }

        public void setPojoObject(final PojoObject pojoObject) {
            POJO_OBJECT = pojoObject;
        }
    }

    @SuppressWarnings({ "unused", "WeakerAccess" })
    public static class TestHandler extends ExtHandler {
        private static PojoObject POJO_OBJECT;
        private static Handler[] HANDLERS;
        private static Formatter FORMATTER;
        private static Filter FILTER;
        private static ErrorManager ERROR_MANAGER;
        private static boolean IS_CLOSED;

        public TestHandler() {
            IS_CLOSED = false;
        }

        @Override
        public void close() throws SecurityException {
            // Null out static values
            POJO_OBJECT = null;
            FORMATTER = null;
            HANDLERS = null;
            FORMATTER = null;
            FILTER = null;
            ERROR_MANAGER = null;
            IS_CLOSED = true;
            super.close();
        }

        @Override
        public Handler[] setHandlers(final Handler[] newHandlers) throws SecurityException {
            HANDLERS = Arrays.copyOf(newHandlers, newHandlers.length);
            return super.setHandlers(newHandlers);
        }

        @Override
        public void addHandler(final Handler handler) throws SecurityException {
            if (handler == null) {
                throw new RuntimeException("Cannot add a null handler");
            }
            if (HANDLERS == null) {
                HANDLERS = new Handler[] { handler };
            } else {
                final int len = HANDLERS.length + 1;
                HANDLERS = Arrays.copyOf(HANDLERS, len);
                HANDLERS[len - 1] = handler;
            }
            super.addHandler(handler);
        }

        @Override
        public void removeHandler(final Handler handler) throws SecurityException {
            if (handler == null) {
                throw new RuntimeException("Cannot remove a null handler");
            }
            if (HANDLERS == null) {
                throw new RuntimeException("Attempting to remove a handler that does not exist: " + handler);
            } else {
                if (HANDLERS.length == 1) {
                    HANDLERS = null;
                } else {
                    boolean success = false;
                    final Handler[] newHandlers = new Handler[HANDLERS.length - 1];
                    int newIndex = 0;
                    for (int i = 0; i < HANDLERS.length; i++) {
                        final Handler current = HANDLERS[i];
                        if (!success && i > newHandlers.length) {
                            break;
                        }
                        if (handler != current) {
                            newHandlers[newIndex++] = current;
                        } else {
                            success = true;
                        }
                    }
                    if (!success) {
                        throw new RuntimeException("Failed to remove handler " + handler + " as it did no appear to exist.");
                    }
                }
            }
            super.removeHandler(handler);
        }

        @Override
        public void setFormatter(final Formatter newFormatter) throws SecurityException {
            FORMATTER = newFormatter;
            super.setFormatter(newFormatter);
        }

        @Override
        public void setFilter(final Filter newFilter) throws SecurityException {
            FILTER = newFilter;
            super.setFilter(newFilter);
        }

        @Override
        public void setErrorManager(final ErrorManager em) {
            ERROR_MANAGER = em;
            super.setErrorManager(em);
        }

        @Override
        public void setLevel(final Level newLevel) throws SecurityException {
            super.setLevel(newLevel);
        }

        public void setPojoObject(final PojoObject pojoObject) {
            POJO_OBJECT = pojoObject;
        }
    }

    @SuppressWarnings("WeakerAccess")
    public static class PojoObject {

        public PojoObject() {
        }
    }
}
