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

import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

import org.xbib.logging.ExtLogRecord;
import org.xbib.logging.ext.formatters.PatternFormatter;
import org.xbib.logging.ext.handlers.SyslogHandler;
import org.xbib.logging.ext.handlers.SyslogHandler.SyslogType;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.xbib.logging.ext.test.AssertingErrorManager;

public class SyslogHandlerTests {

    private static final String ENCODING = "UTF-8";

    private static final String BOM = Character.toString((char) 0xFEFF);

    private static final String MSG = "This is a test message";

    private static final String HOSTNAME = "localhost";

    private static final int PORT = 10999;

    private SyslogHandler handler;

    public SyslogHandlerTests() {
    }

    @BeforeEach
    public void setupHandler() throws Exception {
        handler = new SyslogHandler(HOSTNAME, PORT);
        handler.setFormatter(new PatternFormatter("%s"));
        handler.setErrorManager(AssertingErrorManager.of());
    }

    @AfterEach
    public void closeHandler() throws Exception {
        // Close the handler
        handler.flush();
        handler.close();
    }

    @Test
    public void testRFC5424Tcp() throws Exception {
        // Setup the handler
        handler.setSyslogType(SyslogType.RFC5424);
        handler.setMessageDelimiter("\n");
        handler.setUseMessageDelimiter(true);
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        handler.setOutputStream(out);

        Instant instant = getInstant();
        // Create the record
        handler.setHostname("test");
        ExtLogRecord record = createRecord(instant, MSG);
        String expectedMessage = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java "
                + ProcessHandle.current().pid()
                + " - - " + BOM + MSG + '\n';
        handler.publish(record);
        Assertions.assertEquals(expectedMessage, createString(out));

        // Create the record
        out.reset();
        record = createRecord(instant, MSG);
        expectedMessage = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java "
                + ProcessHandle.current().pid()
                + " - - " + BOM + MSG + '\n';
        handler.publish(record);
        Assertions.assertEquals(expectedMessage, createString(out));

        out.reset();
        instant = instant.plus(22L, ChronoUnit.DAYS);
        record = createRecord(instant, MSG);
        handler.setHostname("test");
        handler.setAppName("java");
        expectedMessage = "<14>1 2012-01-31T04:39:22.000" + getTimeZone(instant) + " test java "
                + ProcessHandle.current().pid()
                + " - - "
                + BOM + MSG + '\n';
        handler.publish(record);
        Assertions.assertEquals(expectedMessage, createString(out));
    }

    @Test
    public void testRFC31644Format() throws Exception {
        // Setup the handler
        handler.setSyslogType(SyslogType.RFC3164);
        handler.setHostname("test");
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        handler.setOutputStream(out);
        Instant instant = getInstant();
        // Create the record
        ExtLogRecord record = createRecord(instant, MSG);
        handler.publish(record);
        String expectedMessage = "<14>Jan  9 04:39:22 test java["
                + ProcessHandle.current().pid()
                + "]: " + MSG;
        Assertions.assertEquals(expectedMessage, createString(out));
        out.reset();
        instant = instant.plus(22L, ChronoUnit.DAYS);
        record = createRecord(instant, MSG);
        handler.publish(record);
        expectedMessage = "<14>Jan 31 04:39:22 test java["
                + ProcessHandle.current().pid()
                + "]: " + MSG;
        Assertions.assertEquals(expectedMessage, createString(out));
    }

    @Test
    public void testOctetCounting() throws Exception {
        // Setup the handler
        handler.setSyslogType(SyslogType.RFC5424);
        handler.setUseMessageDelimiter(false);
        handler.setUseCountingFraming(true);
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        handler.setOutputStream(out);
        Instant instant = getInstant();
        // Create the record
        handler.setHostname("test");
        ExtLogRecord record = createRecord(instant, MSG);
        String expectedMessage = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java "
                + ProcessHandle.current().pid()
                + " - - " + BOM + MSG;
        expectedMessage = byteLen(expectedMessage) + " " + expectedMessage;
        handler.publish(record);
        Assertions.assertEquals(expectedMessage, createString(out));
    }

    @Test
    public void testTruncation() throws Exception {
        // Setup the handler
        handler.setSyslogType(SyslogType.RFC5424);
        handler.setUseMessageDelimiter(false);
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        handler.setOutputStream(out);

        Instant instant = getInstant();
        // Create the record
        handler.setHostname("test");
        final String part1 = "This is a longer message and should be truncated after this.";
        final String part2 = "Truncated portion of the message that will not be shown in.";
        final String message = part1 + " " + part2;

        final String header = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java "
                + ProcessHandle.current().pid()
                + " - - " + BOM;

        handler.setMaxLength(byteLen(header, part1));
        handler.setTruncate(true);

        ExtLogRecord record = createRecord(instant, message);
        String expectedMessage = header + part1;
        handler.publish(record);
        Assertions.assertEquals(expectedMessage, createString(out));

        out.reset();
        // Wrap a message
        handler.setTruncate(false);
        handler.publish(record);
        // Extra space from message
        expectedMessage = header + part1 + header + " " + part2;
        Assertions.assertEquals(expectedMessage, createString(out));
    }

    @Test
    public void testMultibyteTruncation() throws Exception {
        String part1 = "This is a longer message and should be truncated after this  À あ";
        String part2 = "あ À Truncated portion of the message that will not be shown in.";
        testMultibyteTruncation(part1, part2, 1);

        // Test some characters with surrogates
        part1 = "Truncate after double byte 𥹖";
        part2 = "second message 𥹖";
        testMultibyteTruncation(part1, part2, 2);

    }

    @Test
    public void testNullMessage() throws Exception {
        // Setup the handler
        handler.setSyslogType(SyslogType.RFC5424);
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        handler.setOutputStream(out);
        Instant instant = getInstant();
        // Create the record
        handler.setHostname("test");
        ExtLogRecord record = createRecord(instant, null);
        final String expectedMessage = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java "
                + ProcessHandle.current().pid()
                + " - - " + BOM + "null";
        handler.publish(record);
        Assertions.assertEquals(expectedMessage, createString(out));
    }

    private void testMultibyteTruncation(final String part1, final String part2, final int charsToTruncate) throws Exception {
        // Setup the handler
        handler.setSyslogType(SyslogType.RFC5424);
        handler.setUseMessageDelimiter(false);
        final ByteArrayOutputStream out = new ByteArrayOutputStream();
        handler.setOutputStream(out);
        Instant instant = getInstant();
        // Create the record
        handler.setHostname("test");
        String message = part1 + " " + part2;

        final String header = "<14>1 2012-01-09T04:39:22.000" + getTimeZone(instant) + " test java "
                + ProcessHandle.current().pid()
                + " - - " + BOM;

        handler.setMaxLength(byteLen(header, part1));
        handler.setTruncate(true);

        ExtLogRecord record = createRecord(instant, message);
        String expectedMessage = header + part1;
        handler.publish(record);
        Assertions.assertArrayEquals(expectedMessage.getBytes(ENCODING), out.toByteArray(),
                String.format("Expected: %s:%n Received: %s", expectedMessage, createString(out)));

        out.reset();
        // Wrap a message
        handler.setTruncate(false);
        handler.publish(record);
        // Extra space from message
        expectedMessage = header + part1 + header + " " + part2;
        Assertions.assertArrayEquals(expectedMessage.getBytes(ENCODING), out.toByteArray(),
                String.format("Expected: %s:%n Received: %s", expectedMessage, createString(out)));

        // Reset out, write the message with a maximum length of the current length minus 1 to ensure the multi-byte character was not written at the end
        out.reset();
        handler.setTruncate(true);
        handler.setMaxLength(byteLen(header, part1) - 1);
        expectedMessage = header + part1.substring(0, part1.length() - charsToTruncate);
        handler.publish(record);
        Assertions.assertArrayEquals(expectedMessage.getBytes(ENCODING), out.toByteArray(),
                String.format("Expected: %s:%n Received: %s", expectedMessage, createString(out)));
    }

    private static ExtLogRecord createRecord(final Instant instant, final String message) {
        final String loggerName = SyslogHandlerTests.class.getName();
        final ExtLogRecord record = new ExtLogRecord(Level.INFO, message, loggerName);
        record.setMillis(instant.toEpochMilli());
        return record;
    }

    private static Instant getInstant() {
        return LocalDateTime.parse("2012-01-09T04:39:22").toInstant(ZoneOffset.UTC);
    }

    private static String getTimeZone(Instant instant) {
        ZoneOffset zoneOffset = ZoneOffset.UTC.getRules().getOffset(instant);
        long seconds = zoneOffset.getTotalSeconds();
        long hours = TimeUnit.HOURS.convert(seconds, TimeUnit.SECONDS);
        long minutes = TimeUnit.MINUTES.convert(seconds, TimeUnit.SECONDS) % 60;
        return (seconds >= 0L ? "+" : "-") + String.format("%02d:%02d", hours, minutes);
    }

    private static String createString(final ByteArrayOutputStream out) throws UnsupportedEncodingException {
        return out.toString(ENCODING);
    }

    private static int byteLen(final String s) throws UnsupportedEncodingException {
        return s.getBytes(ENCODING).length;
    }

    private static int byteLen(final String s1, final String s2) throws UnsupportedEncodingException {
        return s1.getBytes(ENCODING).length + s2.getBytes(ENCODING).length;
    }
}
