/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.log4j.net;

import junit.framework.TestCase;

import org.apache.log4j.AsyncAppender;
import org.apache.log4j.Layout;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.ListErrorHandler;
import org.apache.log4j.HTMLLayout;

import java.util.StringTokenizer;
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.Date;
import java.util.Calendar;

/**
 * Tests for SyslogAppender
 */
public class SyslogAppenderTest extends TestCase {
    /**
     * Create new instance of SyslogAppenderTest.
     *
     * @param testName test name
     */
    public SyslogAppenderTest(final String testName) {
        super(testName);
    }

    /**
     * Resets configuration after every test.
     */
    public void tearDown() {
        LogManager.resetConfiguration();
    }

    /**
     * Test default constructor.
     */
    public void testDefaultConstructor() {
        SyslogAppender appender = new SyslogAppender();
        assertEquals("user", appender.getFacility());
        assertEquals(false, appender.getFacilityPrinting());
        assertNull(appender.getLayout());
        assertNull(appender.getSyslogHost());
        assertTrue(appender.requiresLayout());
    }

    /**
     * Test two parameter constructor.
     */
    public void testTwoParamConstructor() {
        Layout layout = new PatternLayout();
        SyslogAppender appender = new SyslogAppender(layout, 24);
        assertEquals("daemon", appender.getFacility());
        assertEquals(false, appender.getFacilityPrinting());
        assertEquals(layout, appender.getLayout());
        assertNull(appender.getSyslogHost());
        assertTrue(appender.requiresLayout());
    }

    /**
     * Test two parameter constructor with unexpected facility.
     */
    public void testTwoParamConstructorBadFacility() {
        Layout layout = new PatternLayout();
        SyslogAppender appender = new SyslogAppender(layout, 25);
        assertEquals("user", appender.getFacility());
        assertEquals(false, appender.getFacilityPrinting());
        assertEquals(layout, appender.getLayout());
        assertNull(appender.getSyslogHost());
        assertTrue(appender.requiresLayout());
    }

    /**
     * Test three parameter constructor.
     */
    public void testThreeParamConstructor() {
        Layout layout = new PatternLayout();
        SyslogAppender appender = new SyslogAppender(layout, "syslog.example.org", 24);
        assertEquals("daemon", appender.getFacility());
        assertEquals(false, appender.getFacilityPrinting());
        assertEquals(layout, appender.getLayout());
        assertEquals("syslog.example.org", appender.getSyslogHost());
        assertTrue(appender.requiresLayout());
    }

    /**
     * Test getFacilityString for expected facility codes.
     */
    public void testGetFacilityString() {
        String expected =
                "kern user mail daemon auth syslog lpr news " + "uucp cron authpriv ftp local0 local1 local2 local3 "
                        + "local4 local5 local6 local7 ";
        StringBuffer actual = new StringBuffer();

        for (int i = 0; i <= 11; i++) {
            actual.append(SyslogAppender.getFacilityString(i << 3));
            actual.append(' ');
        }

        for (int i = 16; i <= 23; i++) {
            actual.append(SyslogAppender.getFacilityString(i << 3));
            actual.append(' ');
        }

        assertEquals(expected, actual.toString());
    }

    /**
     * Test getFacilityString for some unexpected facility codes.
     */
    public void testGetFacilityStringUnexpected() {
        assertNull(SyslogAppender.getFacilityString(1));
        assertNull(SyslogAppender.getFacilityString(12 << 3));
    }

    /**
     * Test getFacility with a bogus facility name.
     */
    public void testGetFacilityBogus() {
        assertEquals(-1, SyslogAppender.getFacility("bogus"));
    }

    /**
     * Test getFacility with a null facility name.
     */
    public void testGetFacilityNull() {
        assertEquals(-1, SyslogAppender.getFacility(null));
    }

    /**
     * Test getFacility for expected system facility names.
     */
    public void testGetFacilitySystemNames() {
        String[] names = new String[] { "kErn", "usEr", "MaIL", "daemOn", "auTh", "syslOg", "lPr", "newS", "Uucp",
                "croN", "authprIv", "ftP" };

        for (int i = 0; i <= 11; i++) {
            assertEquals(i << 3, SyslogAppender.getFacility(names[i]));
        }
    }

    /**
     * Test getFacility for expected system facility names.
     */
    public void testGetFacilityLocalNames() {
        String[] names = new String[] { "lOcal0", "LOCAL1", "loCal2", "locAl3", "locaL4", "local5", "LOCal6",
                "loCAL7" };

        for (int i = 0; i <= 7; i++) {
            assertEquals((16 + i) << 3, SyslogAppender.getFacility(names[i]));
        }
    }

    /**
     * Test setFacilityPrinting.
     */
    public void testSetFacilityPrinting() {
        SyslogAppender appender = new SyslogAppender();
        assertFalse(appender.getFacilityPrinting());
        appender.setFacilityPrinting(true);
        assertTrue(appender.getFacilityPrinting());
        appender.setFacilityPrinting(false);
        assertFalse(appender.getFacilityPrinting());
    }

    /**
     * Test of SyslogAppender constants.
     */
    public void testConstants() {
        assertEquals(0 << 3, SyslogAppender.LOG_KERN);
        assertEquals(1 << 3, SyslogAppender.LOG_USER);
        assertEquals(2 << 3, SyslogAppender.LOG_MAIL);
        assertEquals(3 << 3, SyslogAppender.LOG_DAEMON);
        assertEquals(4 << 3, SyslogAppender.LOG_AUTH);
        assertEquals(5 << 3, SyslogAppender.LOG_SYSLOG);
        assertEquals(6 << 3, SyslogAppender.LOG_LPR);
        assertEquals(7 << 3, SyslogAppender.LOG_NEWS);
        assertEquals(8 << 3, SyslogAppender.LOG_UUCP);
        assertEquals(9 << 3, SyslogAppender.LOG_CRON);
        assertEquals(10 << 3, SyslogAppender.LOG_AUTHPRIV);
        assertEquals(11 << 3, SyslogAppender.LOG_FTP);
        assertEquals(16 << 3, SyslogAppender.LOG_LOCAL0);
        assertEquals(17 << 3, SyslogAppender.LOG_LOCAL1);
        assertEquals(18 << 3, SyslogAppender.LOG_LOCAL2);
        assertEquals(19 << 3, SyslogAppender.LOG_LOCAL3);
        assertEquals(20 << 3, SyslogAppender.LOG_LOCAL4);
        assertEquals(21 << 3, SyslogAppender.LOG_LOCAL5);
        assertEquals(22 << 3, SyslogAppender.LOG_LOCAL6);
        assertEquals(23 << 3, SyslogAppender.LOG_LOCAL7);
    }

    /**
     * Test setFacility with null. Should have no effect.
     */
    public void testSetFacilityKern() {
        SyslogAppender appender = new SyslogAppender();
        appender.setFacility("kern");
        appender.setFacility(null);
        assertEquals("kern", appender.getFacility());
    }

    /**
     * Test setFacility with null. Should have no effect.
     */
    public void testSetFacilityNull() {
        SyslogAppender appender = new SyslogAppender();
        appender.setFacility("kern");
        appender.setFacility(null);
        assertEquals("kern", appender.getFacility());
    }

    /**
     * Test setFacility with bogus value. Should reset to user.
     */
    public void testSetFacilityBogus() {
        SyslogAppender appender = new SyslogAppender();
        appender.setFacility("kern");
        appender.setFacility("bogus");
        assertEquals("user", appender.getFacility());
    }

    /**
     * Tests calling setFacility after appender has been activated.
     */
    public void testSetFacilityAfterActivation() {
        SyslogAppender appender = new SyslogAppender();
        appender.setName("foo");
        appender.setThreshold(Level.INFO);
        appender.setSyslogHost("localhost");
        appender.setFacility("user");
        appender.setLayout(new PatternLayout("%m%n"));

        ListErrorHandler errorHandler = new ListErrorHandler();
        appender.setErrorHandler(errorHandler);
        appender.activateOptions();
        appender.setFacility("kern");
        assertEquals("kern", appender.getFacility());
    }

    /**
     * Tests that append method drops messages below threshold. Can't reach isSevereAsThreshold call in
     * SyslogAppender.append since it is checked in AppenderSkeleton.doAppend.
     */
    public void testAppendBelowThreshold() {
        SyslogAppender appender = new SyslogAppender();
        appender.setThreshold(Level.ERROR);
        appender.activateOptions();

        Logger logger = Logger.getRootLogger();
        logger.addAppender(appender);
        logger.info("Should not be logged by SyslogAppenderTest.testAppendBelowThreshold.");
    }

    /**
     * Tests that append method drops messages below threshold.
     */
    public void testAppendNoHost() {
        SyslogAppender appender = new SyslogAppender();
        appender.setName("foo");
        appender.setThreshold(Level.INFO);

        ListErrorHandler errorHandler = new ListErrorHandler();
        appender.setErrorHandler(errorHandler);
        appender.setLayout(new PatternLayout("%m%n"));
        appender.activateOptions();

        Logger logger = Logger.getRootLogger();
        logger.addAppender(appender);
        logger.info("Should not be logged by SyslogAppenderTest.testAppendNoHost.");
        assertEquals(1, errorHandler.size());

        //
        // Appender is misspelled in implementation
        //
        assertEquals("No syslog host is set for SyslogAppedender named \"foo\".", errorHandler.getMessage(0));
    }

    /**
     * Tests append method under normal conditions.
     */
    public void testAppend() {
        SyslogAppender appender = new SyslogAppender();
        appender.setName("foo");
        appender.setThreshold(Level.INFO);
        appender.setSyslogHost("localhost");
        appender.setFacility("user");
        appender.setLayout(new PatternLayout("%m%n"));

        ListErrorHandler errorHandler = new ListErrorHandler();
        appender.setErrorHandler(errorHandler);
        appender.activateOptions();

        //
        // wrap SyslogAppender with an Async since appender may
        // hang if syslogd is not accepting network messages
        //
        AsyncAppender asyncAppender = new AsyncAppender();
        asyncAppender.addAppender(appender);
        asyncAppender.activateOptions();

        Logger logger = Logger.getRootLogger();
        logger.addAppender(asyncAppender);

        Exception e = new Exception("Expected exception from SyslogAppenderTest.testAppend");
        logger.info("Expected message from log4j unit test SyslogAppenderTest.testAppend.", e);
        assertEquals(0, errorHandler.size());
    }

    /**
     * Tests SyslogAppender with IPv6 address.
     */
    public void testIPv6() {
        SyslogAppender appender = new SyslogAppender();
        appender.setSyslogHost("::1");
    }

    /**
     * Tests SyslogAppender with IPv6 address enclosed in square brackets.
     */
    public void testIPv6InBrackets() {
        SyslogAppender appender = new SyslogAppender();
        appender.setSyslogHost("[::1]");
    }

    /**
     * Tests SyslogAppender with IPv6 address enclosed in square brackets followed by port specification.
     */
    public void testIPv6AndPort() {
        SyslogAppender appender = new SyslogAppender();
        appender.setSyslogHost("[::1]:1514");
    }

    /**
     * Tests SyslogAppender with host name enclosed in square brackets followed by port specification.
     */
    public void testHostNameAndPort() {
        SyslogAppender appender = new SyslogAppender();
        appender.setSyslogHost("localhost:1514");
    }

    /**
     * Tests SyslogAppender with IPv4 address followed by port specification.
     */
    public void testIPv4AndPort() {
        SyslogAppender appender = new SyslogAppender();
        appender.setSyslogHost("127.0.0.1:1514");
    }

    private static String[] log(final boolean header, final String msg, final Exception ex, final int packets)
            throws Exception {
        DatagramSocket ds = new DatagramSocket();
        ds.setSoTimeout(2000);

        SyslogAppender appender = new SyslogAppender();
        appender.setSyslogHost("localhost:" + ds.getLocalPort());
        appender.setName("name");
        appender.setHeader(header);
        PatternLayout pl = new PatternLayout("%m");
        appender.setLayout(pl);
        appender.activateOptions();

        Logger l = Logger.getRootLogger();
        l.addAppender(appender);
        if (ex == null) {
            l.info(msg);
        } else {
            l.error(msg, ex);
        }
        appender.close();
        String[] retval = new String[packets];
        byte[] buf = new byte[1000];
        for (int i = 0; i < packets; i++) {
            DatagramPacket p = new DatagramPacket(buf, 0, buf.length);
            ds.receive(p);
            retval[i] = new String(p.getData(), 0, p.getLength());
        }
        ds.close();
        return retval;
    }

    public void testActualLogging() throws Exception {
        String s = log(false, "greetings", null, 1)[0];
        StringTokenizer st = new StringTokenizer(s, "<>() ");
        assertEquals("14", st.nextToken());
        assertEquals("greetings", st.nextToken());
    }

    /**
     * Exception with printStackTrace that breaks earlier SyslogAppender.
     */
    private static class MishandledException extends Exception {
        private static final long serialVersionUID = 1L;

        /*
         * Create new instance.
         */
        public MishandledException() {
        }

        /**
         * Print stack trace.
         *
         * @param w print writer, may not be null.
         */
        public void printStackTrace(final java.io.PrintWriter w) {
            w.println("Mishandled stack trace follows:");
            w.println("");
            w.println("No tab here");
            w.println("\ttab here");
            w.println("\t");
        }
    }

    /**
     * Tests fix for bug 40502.
     *
     * @throws Exception on IOException.
     */
    public void testBadTabbing() throws Exception {
        String[] s = log(false, "greetings", new MishandledException(), 6);
        StringTokenizer st = new StringTokenizer(s[0], "<>() ");
        assertEquals("11", st.nextToken());
        assertEquals("greetings", st.nextToken());
        assertEquals("<11>Mishandled stack trace follows:", s[1]);
        assertEquals("<11>", s[2]);
        assertEquals("<11>No tab here", s[3]);
        assertEquals("<11>" + SyslogAppender.TAB + "tab here", s[4]);
        assertEquals("<11>" + SyslogAppender.TAB, s[5]);
    }

    /**
     * Tests presence of timestamp if header = true.
     *
     * @throws Exception if IOException.
     */
    public void testHeaderLogging() throws Exception {
        Date preDate = new Date();
        String s = log(true, "greetings", null, 1)[0];
        Date postDate = new Date();
        assertEquals("<14>", s.substring(0, 4));

        String syslogDateStr = s.substring(4, 20);
        SimpleDateFormat fmt = new SimpleDateFormat("MMM dd HH:mm:ss ", Locale.ENGLISH);
        Date syslogDate = fmt.parse(syslogDateStr);
        Calendar cal = Calendar.getInstance(Locale.ENGLISH);
        cal.setTime(syslogDate);
        int syslogMonth = cal.get(Calendar.MONTH);
        int syslogDay = cal.get(Calendar.DATE);
        if (syslogDay < 10) {
            assertEquals(' ', syslogDateStr.charAt(4));
        }
        cal.setTime(preDate);
        int preMonth = cal.get(Calendar.MONTH);
        cal.set(Calendar.MILLISECOND, 0);
        preDate = cal.getTime();
        int syslogYear;
        if (preMonth == syslogMonth) {
            syslogYear = cal.get(Calendar.YEAR);
        } else {
            cal.setTime(postDate);
            syslogYear = cal.get(Calendar.YEAR);
        }
        cal.setTime(syslogDate);
        cal.set(Calendar.YEAR, syslogYear);
        syslogDate = cal.getTime();
        assertTrue(syslogDate.compareTo(preDate) >= 0);
        assertTrue(syslogDate.compareTo(postDate) <= 0);
    }

    /**
     * Tests that any header or footer in layout is sent.
     *
     * @throws Exception if exception during test.
     */
    public void testLayoutHeader() throws Exception {
        DatagramSocket ds = new DatagramSocket();
        ds.setSoTimeout(2000);

        SyslogAppender appender = new SyslogAppender();
        appender.setSyslogHost("localhost:" + ds.getLocalPort());
        appender.setName("name");
        appender.setHeader(false);
        HTMLLayout pl = new HTMLLayout();
        appender.setLayout(pl);
        appender.activateOptions();

        Logger l = Logger.getRootLogger();
        l.addAppender(appender);
        l.info("Hello, World");
        appender.close();
        String[] s = new String[3];
        byte[] buf = new byte[1000];
        for (int i = 0; i < 3; i++) {
            DatagramPacket p = new DatagramPacket(buf, 0, buf.length);
            ds.receive(p);
            s[i] = new String(p.getData(), 0, p.getLength());
        }
        ds.close();
        assertEquals("<14><!DOCTYPE", s[0].substring(0, 13));
        assertEquals("<14></table>", s[2].substring(0, 12));
    }

    /**
     * Tests that syslog packets do not exceed 1024 bytes. See bug 42087.
     *
     * @throws Exception if exception during test.
     */
    public void testBigPackets() throws Exception {
        DatagramSocket ds = new DatagramSocket();
        ds.setSoTimeout(2000);

        SyslogAppender appender = new SyslogAppender();
        appender.setSyslogHost("localhost:" + ds.getLocalPort());
        appender.setName("name");
        appender.setHeader(false);
        PatternLayout pl = new PatternLayout("%m");
        appender.setLayout(pl);
        appender.activateOptions();

        Logger l = Logger.getRootLogger();
        l.addAppender(appender);
        StringBuffer msgbuf = new StringBuffer();
        while (msgbuf.length() < 8000) {
            msgbuf.append("0123456789");
        }
        String msg = msgbuf.toString();
        l.info(msg);
        appender.close();
        String[] s = new String[8];
        byte[] buf = new byte[1200];
        for (int i = 0; i < 8; i++) {
            DatagramPacket p = new DatagramPacket(buf, 0, buf.length);
            ds.receive(p);
            assertTrue(p.getLength() <= 1024);
            s[i] = new String(p.getData(), 0, p.getLength());
        }
        ds.close();
        StringBuffer rcvbuf = new StringBuffer(s[0]);
        rcvbuf.delete(0, 4);
        for (int i = 1; i < 8; i++) {
            rcvbuf.setLength(rcvbuf.length() - 3);
            rcvbuf.append(s[i].substring(s[i].indexOf("...") + 3));
        }
        assertEquals(msg.length(), rcvbuf.length());
        assertEquals(msg, rcvbuf.toString());
    }

}
