package org.productivity.java.syslog4j.test.net.base;

import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.productivity.java.syslog4j.Syslog;
import org.productivity.java.syslog4j.SyslogConstants;
import org.productivity.java.syslog4j.SyslogIF;
import org.productivity.java.syslog4j.SyslogRuntimeException;
import org.productivity.java.syslog4j.impl.message.pci.PCISyslogMessage;
import org.productivity.java.syslog4j.impl.message.structured.StructuredSyslogMessage;
import org.productivity.java.syslog4j.impl.message.structured.StructuredSyslogMessageIF;
import org.productivity.java.syslog4j.impl.multiple.MultipleSyslog;
import org.productivity.java.syslog4j.impl.multiple.MultipleSyslogConfig;
import org.productivity.java.syslog4j.server.SyslogServer;
import org.productivity.java.syslog4j.server.SyslogServerEventIF;
import org.productivity.java.syslog4j.server.SyslogServerIF;
import org.productivity.java.syslog4j.server.SyslogServerSessionEventHandlerIF;
import org.productivity.java.syslog4j.server.impl.event.structured.StructuredSyslogServerEvent;
import org.productivity.java.syslog4j.server.impl.net.AbstractNetSyslogServerConfig;
import org.productivity.java.syslog4j.server.impl.net.tcp.TCPNetSyslogServerConfig;
import org.productivity.java.syslog4j.test.base.AbstractBaseTest;
import org.productivity.java.syslog4j.util.SyslogUtility;

public abstract class AbstractNetSyslog4jTest extends AbstractBaseTest {
	protected static String APP_ID = "Syslog4jTest";
	
	public static class ClientThread implements Runnable {
		protected SyslogIF syslog = null;
		protected List messages = null;
		public static int active = 0;
		
		protected synchronized void incrementActive() {
			active++;
		}

		protected synchronized void decrementActive() {
			active--;
		}
		
		public ClientThread(SyslogIF syslog, List messages) {
			this.syslog = syslog;
			this.messages = messages;
		}
		
		public void run() {
			incrementActive();
			for(int i=0; i<this.messages.size(); i++) {
				String message = (String) this.messages.get(i);
				try {
					this.syslog.info(message);
					
				} catch (SyslogRuntimeException sre) {
					System.err.println(sre);
				}
			}
			decrementActive();
		}
	}

	protected class RecorderHandler implements SyslogServerSessionEventHandlerIF {
		private static final long serialVersionUID = 7364480542656523264L;
		
		protected List recordedEvents = new LinkedList();
		
		public List getRecordedEvents() {
			return this.recordedEvents;
		}

		public void initialize(SyslogServerIF syslogServer) {
			//
		}

		public Object sessionOpened(SyslogServerIF syslogServer, SocketAddress socketAddress) {
			return null;
		}

		public void event(Object session, SyslogServerIF syslogServer, SocketAddress socketAddress, SyslogServerEventIF event) {
			if (event instanceof StructuredSyslogServerEvent) {
				this.recordedEvents.add(event.getMessage().substring("Syslog4jTest: ".length()));
				
			} else {
				String recordedEvent = SyslogUtility.newString(syslogServer.getConfig(),event.getRaw());
				
				recordedEvent = recordedEvent.substring(recordedEvent.toUpperCase().indexOf("[TEST]"));
	
				synchronized(this.recordedEvents) {
					this.recordedEvents.add(recordedEvent);
				}
			}
		}

		public void exception(Object session, SyslogServerIF syslogServer, SocketAddress socketAddress, Exception exception) {
			fail(exception.getMessage());
		}

		public void sessionClosed(Object session, SyslogServerIF syslogServer, SocketAddress socketAddress, boolean timeout) {
			//
		}

		public void destroy(SyslogServerIF syslogServer) {
			//
		}
	}
	
	public static final int TEST_PORT = 10514;

	protected SyslogServerIF server = null;
	
	protected abstract String getClientProtocol();
	protected abstract String getServerProtocol() throws Exception;
	
	protected abstract int getMessageCount();

	protected RecorderHandler recorderEventHandler = new RecorderHandler();
	
	protected SyslogIF getSyslog(String protocol) {
		if (!Syslog.exists(protocol)) {
			fail("Protocol \"" + protocol + "\" does not exist");
		}
		
		SyslogIF syslog = Syslog.getInstance(protocol);
		
		if (!(syslog instanceof MultipleSyslog)) {
			syslog.getConfig().setIdent(APP_ID);
		}
		
		if (!(syslog.getConfig() instanceof MultipleSyslogConfig)) {
			syslog.getConfig().setPort(TEST_PORT);
		}
		
		return syslog;
	}
	
	protected boolean isSyslogServerTcpBacklog() {
		return false;
	}

	protected void startServerThread(String protocol) {
		assertTrue(SyslogServer.exists(protocol));
		
		this.server = SyslogServer.getInstance(protocol);
		
		if (isSyslogServerTcpBacklog() && this.server.getConfig() instanceof TCPNetSyslogServerConfig) {
			((TCPNetSyslogServerConfig) this.server.getConfig()).setBacklog(0);
		}
		
		AbstractNetSyslogServerConfig config = (AbstractNetSyslogServerConfig) this.server.getConfig();
		config.setPort(TEST_PORT);
		config.addEventHandler(this.recorderEventHandler);

		if (this.server.getThread() == null) {
			Thread t = new Thread(this.server);
			t.setName("SyslogServer: " + protocol);
			t.start();
			
			this.server.setThread(t);
			
			assertEquals(t,this.server.getThread());
		}
	}

	public void setUp() throws Exception {
		String protocol = getServerProtocol();
		
		startServerThread(protocol);
		SyslogUtility.sleep(100);
	}

	protected void verifySendReceive(List events, boolean sortEvents, boolean sortRecordedEvents) {
		verifySendReceive(events,sortEvents,sortRecordedEvents,100);
	}

	protected void verifySendReceive(List events, boolean sortEvents, boolean sortRecordedEvents, int threads) {
		boolean done = false;
		
		long start = System.currentTimeMillis();
		
		while(!done) {
			int eventCount = events.size();
			int recordedEventCount = this.recorderEventHandler.recordedEvents.size(); 
			
			int perc = (int) (((double) recordedEventCount / (double) eventCount) * 100000) / 1000;
			
			String detail = "Count: " + perc + "% " + recordedEventCount + "/" + eventCount + " : " + ClientThread.active;
			
			if (eventCount > recordedEventCount) {
				System.out.println(detail);

			} else if (eventCount < recordedEventCount) {
				detail = "Problem - Sent Events: " + eventCount + " Recorded Events: " + recordedEventCount;
				
				System.err.println(detail);

				fail(detail);
				
			} else {
				System.out.println(detail);
				
				done = true;
			}
			
			if (!done) {
				long now = System.currentTimeMillis();
				
				if (now - start > 600 * threads) {
					detail = "Problem: " + eventCount + " " + recordedEventCount;
					
					System.err.println(detail);
	
					fail(detail);
					
					done = true;
				}
			}
			
			SyslogUtility.sleep(200);
		}
		
		if (sortEvents) {
			Collections.sort(events);
		}
		
		List recordedEvents = this.recorderEventHandler.getRecordedEvents();
		
		if (sortRecordedEvents) {
			Collections.sort(recordedEvents);
		}
		
		for(int i=0; i < events.size(); i++) {
			String sentEvent = (String) events.get(i);
			
			String recordedEvent = (String) recordedEvents.get(i);
			
			if (!sentEvent.equals(recordedEvent)) {
				System.out.println("SENT: " + sentEvent);
				System.out.println("RCVD: " + recordedEvent);
				
				fail("Sent and recorded events do not match");
			}
		}
	}

	public void _testSendReceive(boolean sortEvents, boolean sortRecordedEvents){
		List events = new ArrayList();
		
		String protocol = getClientProtocol();
		
		SyslogIF syslog = getSyslog(protocol);
		
		for(int i=0; i<getMessageCount(); i++) {
			String message = "[TEST] " + i + " / " + System.currentTimeMillis();
			
			syslog.info(message);
			events.add(message);
		}
		
		SyslogUtility.sleep(200);
		
		syslog.flush();
		
		verifySendReceive(events,sortEvents,sortRecordedEvents);
	}

	public void _testSendReceivePCIMessages(boolean sortEvents, boolean sortRecordedEvents){
		List events = new ArrayList();
		
		String protocol = getClientProtocol();
		
		SyslogIF syslog = getSyslog(protocol);

		PCISyslogMessage message = new PCISyslogMessage();
		message.setUserId("[TEST]");
		
		syslog.debug(message);
		events.add(message.createMessage());
		
		syslog.info(message);
		events.add(message.createMessage());
		
		syslog.notice(message);
		events.add(message.createMessage());
		
		syslog.warn(message);
		events.add(message.createMessage());
		
		syslog.error(message);
		events.add(message.createMessage());
		
		syslog.critical(message);
		events.add(message.createMessage());

		syslog.alert(message);
		events.add(message.createMessage());

		syslog.emergency(message);
		events.add(message.createMessage());

		syslog.log(SyslogConstants.LEVEL_INFO,message);
		events.add(message.createMessage());

		SyslogUtility.sleep(100);
		
		syslog.flush();
		
		verifySendReceive(events,sortEvents,sortRecordedEvents);
	}

	public void _testSendReceiveStructuredMessages(boolean sortEvents, boolean sortRecordedEvents){
		List events = new ArrayList();
		
		String protocol = getClientProtocol();
		
		SyslogIF syslog = getSyslog(protocol);

		this.server.getConfig().setUseStructuredData(true);

		Map m2 = new HashMap();
		m2.put("foo","bar");

		Map m1 = new HashMap();
		m1.put("testa",m2);
		
		StructuredSyslogMessageIF message = new StructuredSyslogMessage("[TEST]",m1,"testb");
		
		syslog.debug(message);
		events.add(message.createMessage());
		
		syslog.info(message);
		events.add(message.createMessage());
		
		syslog.notice(message);
		events.add(message.createMessage());
		
		syslog.warn(message);
		events.add(message.createMessage());
		
		syslog.error(message);
		events.add(message.createMessage());
		
		syslog.critical(message);
		events.add(message.createMessage());

		syslog.alert(message);
		events.add(message.createMessage());

		syslog.emergency(message);
		events.add(message.createMessage());

		syslog.log(SyslogConstants.LEVEL_INFO,message);
		events.add(message.createMessage());

		syslog.log(SyslogConstants.LEVEL_INFO,message.createMessage());
		events.add(message.createMessage());

		SyslogUtility.sleep(100);
		
		syslog.flush();
		
		verifySendReceive(events,sortEvents,sortRecordedEvents);

		this.server.getConfig().setUseStructuredData(false);
	}

	public void _testThreadedSendReceive(int threads, boolean sortEvents, boolean sortRecordedEvents){
		_testThreadedSendReceive(threads,sortEvents,sortRecordedEvents,null);
	}
	
	public void _testThreadedSendReceive(int threads, boolean sortEvents, boolean sortRecordedEvents, List backLogEvents){
		List events = new ArrayList();
		
		String protocol = getClientProtocol();
		
		SyslogIF syslog = getSyslog(protocol);

		for(int t=0; t<threads; t++) {
			List messageList = new ArrayList();
			
			for(int i=0; i<getMessageCount(); i++) {
				String message = "[TEST] " + t + " / " + i + " / " + System.currentTimeMillis();
				
				messageList.add(message);
				events.add(message);
			}
			
			Runnable r = new ClientThread(syslog,messageList);
			
			Thread thread = new Thread(r);
			thread.setName("Syslog: " + protocol + " [" + t + "]");
			
			thread.start();
		}

		SyslogUtility.sleep(100);
		
		syslog.flush();
		
		if (backLogEvents != null) {
			List recordedEvents = this.recorderEventHandler.getRecordedEvents();
			
			int haveCount = recordedEvents.size() + backLogEvents.size();
			
			long startTime = System.currentTimeMillis();
			
			while(haveCount < events.size()) {
				System.out.println("Count: " + haveCount + "/" + events.size());
				SyslogUtility.sleep(250);
				
				haveCount = recordedEvents.size() + backLogEvents.size();
				
				long currentTime = System.currentTimeMillis();
				
				if ((currentTime - startTime) > 5000) {
					break;
				}
			}
			
			System.out.println("Sent Events:     " + events.size());
			System.out.println("BackLog Events:  " + backLogEvents.size());
			System.out.println("Recorded Events: " + recordedEvents.size());
			
			if (backLogEvents.size() < 1) {
				fail("No backLog events received");
			}

			if (recordedEvents.size() < 1) {
				fail("No recorded events received");
			}

			if ((recordedEvents.size() + backLogEvents.size()) != events.size()) {
				fail("Lost some events");
			}
			
			recordedEvents.addAll(backLogEvents);
		}
		
		verifySendReceive(events,sortEvents,sortRecordedEvents);
	}

	public void tearDown() {
		System.out.print("Shutting down Syslog...");
		Syslog.shutdown();
		System.out.println("done.");

		SyslogUtility.sleep(100);
		
		System.out.print("Shutting down SyslogServer...");
		SyslogServer.shutdown();
		System.out.println("done.");
		
		SyslogUtility.sleep(100);

		Syslog.initialize();
		SyslogServer.initialize();
	}
}