package org.xbib.logging.ext.test.handlers;

import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.logging.ErrorManager;

import javax.net.ssl.SSLContext;

import org.xbib.logging.ExtLogRecord;
import org.xbib.logging.LogContext;
import org.xbib.logging.Logger;
import org.xbib.logging.ext.formatters.PatternFormatter;
import org.xbib.logging.ext.handlers.SocketHandler;
import org.xbib.logging.ext.handlers.SocketHandler.Protocol;
import org.junit.jupiter.api.Test;
import org.xbib.logging.ext.test.AssertingErrorManager;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

public class SocketHandlerTests extends AbstractHandlerTest {

    private final InetAddress address;

    public SocketHandlerTests() throws UnknownHostException {
        address = InetAddress.getByName(System.getProperty("org.xbib.test.address", "127.0.0.1"));
    }

    @Test
    public void testTcpConnection() throws Exception {
        try (SimpleServer server = SimpleServer.createTcpServer();
                SocketHandler handler = createHandler(Protocol.TCP, server.getPort())) {
            final ExtLogRecord record = createLogRecord("Test TCP handler");
            handler.publish(record);
            final String msg = server.timeoutPoll();
            assertNotNull(msg);
            assertEquals("Test TCP handler", msg);
        }
    }

    @Test
    public void testTlsConnection() throws Exception {
        try (SimpleServer server = SimpleServer.createTlsServer();
                SocketHandler handler = createHandler(Protocol.SSL_TCP, server.getPort())) {
            final ExtLogRecord record = createLogRecord("Test TLS handler");
            handler.publish(record);
            final String msg = server.timeoutPoll();
            assertNotNull(msg);
            assertEquals("Test TLS handler", msg);
        }
    }

    @Test
    public void testUdpConnection() throws Exception {
        try (SimpleServer server = SimpleServer.createUdpServer();
                SocketHandler handler = createHandler(Protocol.UDP, server.getPort())) {
            final ExtLogRecord record = createLogRecord("Test UDP handler");
            handler.publish(record);
            final String msg = server.timeoutPoll();
            assertNotNull(msg);
            assertEquals("Test UDP handler", msg);
        }
    }

    @Test
    public void testTcpPortChange() throws Exception {
        try (
                SimpleServer server1 = SimpleServer.createTcpServer();
                SimpleServer server2 = SimpleServer.createTcpServer();
                SocketHandler handler = createHandler(Protocol.TCP, server1.getPort())) {
            final int port = server1.getPort();
            final int altPort = server2.getPort();
            ExtLogRecord record = createLogRecord("Test TCP handler " + port);
            handler.publish(record);
            String msg = server1.timeoutPoll();
            assertNotNull(msg);
            assertEquals("Test TCP handler " + port, msg);

            // Change the port on the handler which should close the first connection and open a new one
            handler.setPort(altPort);
            record = createLogRecord("Test TCP handler " + altPort);
            handler.publish(record);
            msg = server2.timeoutPoll();
            assertNotNull(msg);
            assertEquals("Test TCP handler " + altPort, msg);

            // There should be nothing on server1, we won't know if the real connection is closed but we shouldn't
            // have any data remaining on the first server
            assertNull(server1.peek(), "Expected no data on server1");
        }
    }

    @Test
    public void testProtocolChange() throws Exception {
        SocketHandler handler = null;
        try {
            try (SimpleServer server = SimpleServer.createTcpServer()) {
                handler = createHandler(Protocol.TCP, server.getPort());
                final ExtLogRecord record = createLogRecord("Test TCP handler");
                handler.publish(record);
                final String msg = server.timeoutPoll();
                assertNotNull(msg);
                assertEquals("Test TCP handler", msg);
            }
            // wait until the OS really release used port
            Thread.sleep(50);

            // Change the protocol on the handler which should close the first connection and open a new one
            handler.setProtocol(Protocol.SSL_TCP);

            try (SimpleServer server = SimpleServer.createTlsServer(handler.getPort())) {
                final ExtLogRecord record = createLogRecord("Test TLS handler");
                handler.publish(record);
                final String msg = server.timeoutPoll();
                assertNotNull(msg);
                assertEquals("Test TLS handler", msg);
            }
        } finally {
            if (handler != null) {
                handler.close();
            }
        }
    }

    @Test
    public void testTcpReconnect() throws Exception {
        SocketHandler handler = null;
        try {

            // Publish a record to a running server
            try (SimpleServer server = SimpleServer.createTcpServer()) {
                handler = createHandler(Protocol.TCP, server.getPort());
                handler.setErrorManager(AssertingErrorManager.of(ErrorManager.FLUSH_FAILURE));
                final ExtLogRecord record = createLogRecord("Test TCP handler");
                handler.publish(record);
                final String msg = server.timeoutPoll();
                assertNotNull(msg);
                assertEquals("Test TCP handler", msg);
            }
            // wait until the OS really release used port
            Thread.sleep(50);

            // Publish a record to a down server, this likely won't put the handler in an error state yet. However once
            // we restart the server and loop the first socket should fail before a reconnect is attempted.
            final ExtLogRecord record = createLogRecord("Test TCP handler");
            handler.publish(record);
            try (
                    SimpleServer server = SimpleServer.createTcpServer(handler.getPort())) {
                final SocketHandler socketHandler = handler;
                // Keep writing a record until a successful record is published or a timeout occurs
                final String msg = timeout(() -> {
                    final ExtLogRecord r = createLogRecord("Test TCP handler");
                    socketHandler.publish(r);
                    try {
                        return server.poll();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }, 10);
                assertNotNull(msg);
                assertEquals("Test TCP handler", msg);
            }
        } finally {
            if (handler != null) {
                handler.close();
            }
        }
    }

    @Test
    public void testTlsConfig() throws Exception {
        try (SimpleServer server = SimpleServer.createTlsServer()) {
            final int port = server.getPort();
            final LogContext logContext = LogContext.create();
            final PatternFormatter patternFormatter = new PatternFormatter("%s\n");
            final SocketHandler socketHandler = new SocketHandler();
            socketHandler.setSocketFactory(SSLContext.getDefault().getSocketFactory());
            socketHandler.setProtocol(Protocol.SSL_TCP);
            socketHandler.setAddress(address);
            socketHandler.setPort(port);
            socketHandler.setAutoFlush(true);
            socketHandler.setEncoding("utf-8");
            socketHandler.setFormatter(patternFormatter);
            socketHandler.setErrorManager(AssertingErrorManager.of());
            // Create the root logger
            final Logger rootLogger = logContext.getLogger("");
            rootLogger.addHandler(socketHandler);
            rootLogger.info("Test TCP handler " + port + " 1");
            String msg = server.timeoutPoll();
            assertNotNull(msg);
            assertEquals("Test TCP handler " + port + " 1", msg);
        }
    }

    private SocketHandler createHandler(final Protocol protocol, final int port) throws UnsupportedEncodingException {
        final SocketHandler handler = new SocketHandler(protocol, address, port);
        handler.setAutoFlush(true);
        handler.setEncoding("utf-8");
        handler.setFormatter(new PatternFormatter("%s\n"));
        handler.setErrorManager(AssertingErrorManager.of());

        return handler;
    }

    private static <R> R timeout(final Supplier<R> supplier, final int timeout) throws InterruptedException {
        R value = null;
        long t = timeout * 1000L;
        final long sleep = 100L;
        while (t > 0) {
            final long before = System.currentTimeMillis();
            value = supplier.get();
            if (value != null) {
                break;
            }
            t -= (System.currentTimeMillis() - before);
            TimeUnit.MILLISECONDS.sleep(sleep);
            t -= sleep;
        }
        assertFalse((t <= 0), () -> String.format("Failed to get value in %d seconds.", timeout));
        return value;
    }
}
