/*
 * 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());
    }

}
