/*
 * 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.felix.scr.impl.logger;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.io.File;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.ServiceLoader;

import org.apache.felix.scr.impl.logger.InternalLogger.Level;
import org.apache.felix.scr.impl.logger.LogManager.LoggerFacade;
import org.apache.felix.scr.impl.logger.LogService.LogEntry;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceReference;
import org.osgi.framework.hooks.service.FindHook;
import org.osgi.framework.launch.Framework;
import org.osgi.service.log.LogLevel;
import org.osgi.service.log.Logger;

import aQute.bnd.osgi.Builder;
import aQute.bnd.osgi.Jar;
import aQute.bnd.osgi.JarResource;
import aQute.lib.io.IO;

public class LoggerTest {

	private Framework	framework;
	private Bundle		scr;
	private Bundle		component;
	private Bundle		log;
	private File		tmp	= IO.getFile("target/tmp/loggertest");

	@Before
	public void setup() throws Exception {
		Map<String, String> configuration = new HashMap<>();
		configuration.put(Constants.FRAMEWORK_STORAGE, tmp.getAbsolutePath());
		configuration.put(Constants.FRAMEWORK_STORAGE_CLEAN, Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
		framework = ServiceLoader.load(org.osgi.framework.launch.FrameworkFactory.class).iterator().next()
				.newFramework(configuration);
		framework.init();
		framework.start();
        // hide LoggerFactory from equinox
        framework.getBundleContext().registerService(FindHook.class, new FindHook()
        {

            @Override
            public void find(BundleContext context, String name, String filter,
                boolean allServices, Collection<ServiceReference<?>> references)
            {
                if (name != null && "org.osgi.service.log.LoggerFactory".equals(name))
                {
                    references.removeIf((r) -> r.getBundle().getBundleId() == 0);
                }
            }
        }, null);

		scr = framework.getBundleContext().installBundle("scr", makeBundle("scr").openInputStream());
		component = framework.getBundleContext().installBundle("component",
				makeBundle("component").openInputStream());
		log = framework.getBundleContext().installBundle("component", makeBundle("log").openInputStream());
		scr.start();
		component.start();
		log.start();
	}

	@After
	public void after() throws Exception {
		framework.stop();
		framework.waitForStop(100000);
	}

	static class Buf extends FilterOutputStream {
		StringWriter		buffer	= new StringWriter();
		final PrintStream	stream;

		public Buf(PrintStream out) {
			super(out);
			this.stream = out;
		}

		@Override
		public void write(int b) throws IOException {
			stream.write(b);
			buffer.write(b);
		}

		PrintStream out() {
			return new PrintStream(this);
		}

		public void reset() {
			buffer = new StringWriter();
		}
	}

	@Test
	public void formatTest() {
		LogService l = new LogService(log.getBundleContext());
		l.register();
		LogConfiguration config = mock(LogConfiguration.class);
		when(config.getLogLevel()).thenReturn(Level.DEBUG);
		ExtLogManager elm = new ExtLogManager(scr.getBundleContext(), config);
        elm.open();

		ComponentLogger clog = elm.component(component, "i.c", "c");

		clog.log(Level.ERROR, "error {0} {1} {2}", null, "a", 1, scr);
		LogEntry le = l.entries.get(0);
		assertThat(le.format).isEqualTo("[c] error a 1 bundle scr:0.0.0 (1)");
	}

	@Test
	public void testStandardOutput() throws IOException {
	    LogConfiguration config = mock(LogConfiguration.class);
		when(config.getLogLevel()).thenReturn(Level.DEBUG);
		
		ExtLogManager elm = new ExtLogManager(scr.getBundleContext(), config);
        elm.open();
        
		try (Buf out = new Buf(System.out);
				Buf err = new Buf(System.err);) {
			try {
				System.setOut(out.out());
				System.setErr(err.out());
				ScrLogger scr = elm.scr();
				{
					scr.log(Level.AUDIT, "audit ", null);
					scr.log(Level.AUDIT, "audit", new Exception("FOOBAR"));
					scr.log(Level.TRACE, "audit", new Exception("FOOBAR"));

					String logged = (Level.AUDIT.err() ? err : out).buffer.toString();
					assertThat(logged).startsWith("AUDIT : audit").contains("FOOBAR");
				}

				{
					int n = 0;
					reset(config);

					for (Level configLevel : Level.values()) {

						for (Level requestedLevel : Level.values()) {
							out.reset();
							err.reset();
							when(config.getLogLevel()).thenReturn(configLevel);

							String message = configLevel + ":" + requestedLevel;
							scr.log(requestedLevel, message, null);
							verify(config, times(++n)).getLogLevel();

							String logged = (requestedLevel.err() ? err : out).buffer.toString();

							if (configLevel.implies(requestedLevel)) {
								assertThat(logged).contains(message);
							} else {
								assertThat(logged).isEmpty();
							}
						}
					}
				}
			} finally {
				System.setOut(out.stream);
				System.setErr(err.stream);
			}
		}
	}

	@Test
	public void testSelectionOfExtensionManager() {
		LogService l = new LogService(log.getBundleContext());
		l.register();

		LogConfiguration config = mock(LogConfiguration.class);
		when(config.getLogLevel()).thenReturn(Level.DEBUG);
		when(config.isLogEnabled()).thenReturn(true);
		when(config.isLogExtensionEnabled()).thenReturn(true);
		
		ScrLogger logger = ScrLoggerFactory.create(scr.getBundleContext(), config);
		BundleLogger bundle = logger.bundle(component);
		bundle.log(Level.ERROR, "Ext", null);
		assertThat(l.entries).hasSize(1);
		LogEntry le = l.entries.get(0);
        assertThat(le.bundle).isEqualTo(scr);
	}

	@Test
	public void testSelectionOfBaseManager() {
		LogService l = new LogService(log.getBundleContext());
		l.register();

		LogConfiguration config = mock(LogConfiguration.class);
		
		when(config.isLogEnabled()).thenReturn(true);
		when(config.isLogExtensionEnabled()).thenReturn(false);
		when(config.getLogLevel()).thenReturn(Level.DEBUG);

		ScrLogger logger = ScrLoggerFactory.create(scr.getBundleContext(), config);
		BundleLogger bundle = logger.bundle(component);
		bundle.log(Level.ERROR, "Ext", null);
		assertThat(l.entries).hasSize(1);
		LogEntry le = l.entries.get(0);
		assertThat(le.bundle).isEqualTo(component);
	}

	@Test
	public void testExtensionLogLevelNotLoggingWhenRootSetToInfoAndLevelIsDebug() {
	    LogConfiguration config = mock(LogConfiguration.class);
		
		when(config.isLogEnabled()).thenReturn(true);
		when(config.isLogExtensionEnabled()).thenReturn(true);
		when(config.getLogLevel()).thenReturn(Level.DEBUG);

		LogService l = new LogService(log.getBundleContext());
		l.register();
		l.levels.put(Logger.ROOT_LOGGER_NAME, LogLevel.INFO);
		l.defaultLogLevel = LogLevel.TRACE;

		ScrLogger lscr = ScrLoggerFactory.create(scr.getBundleContext(), config);

		assertThat(lscr.isLogEnabled(Level.DEBUG)).isFalse();
		assertThat(lscr.isLogEnabled(Level.INFO)).isTrue();

		lscr.log(Level.DEBUG, "I should not be reported", null);

		assertThat(l.entries).isEmpty();
	}

	@Test
	public void testExtensionLogLevelNotLoggingWhenPartialNameSetToInfoAndLevelIsDebug() {
	    LogConfiguration config = mock(LogConfiguration.class);
		
		when(config.isLogEnabled()).thenReturn(true);    
		when(config.isLogExtensionEnabled()).thenReturn(true);
		when(config.getLogLevel()).thenReturn(Level.DEBUG);

		LogService l = new LogService(log.getBundleContext());
		l.defaultLogLevel = LogLevel.TRACE;
		l.register();
		l.levels.put("org.apache.felix.scr", LogLevel.INFO);

		ScrLogger lscr = ScrLoggerFactory.create(scr.getBundleContext(), config);

		assertThat(lscr.isLogEnabled(Level.DEBUG)).isFalse();
		assertThat(lscr.isLogEnabled(Level.INFO)).isTrue();

		lscr.log(Level.DEBUG, "I should not be reported", null);

		assertThat(l.entries).isEmpty();
	}

	@Test
	public void testExtensionLogManager() {
	    LogConfiguration config = mock(LogConfiguration.class);
		
		when(config.isLogExtensionEnabled()).thenReturn(true);
		when(config.getLogLevel()).thenReturn(Level.DEBUG);

		LogService l = new LogService(log.getBundleContext());
		l.register();

		ScrLogManager lm = new ExtLogManager(scr.getBundleContext(), config);
        lm.open();

		{
			l.entries.clear();
			ScrLogger scr = lm.scr();
			scr.log(Level.ERROR, "Scr", null);
			assertThat(l.entries).hasSize(1);
			LogEntry le = l.entries.get(0);
			assertThat(le.format).isEqualTo("Scr");
			assertThat(le.bundle).isEqualTo(this.scr);
			assertThat(le.loggername).isEqualTo(ExtLogManager.SCR_LOGGER_NAME);
		}

		{
			l.entries.clear();
			BundleLogger blog = lm.bundle(component);
			blog.log(Level.ERROR, "Bundle", null);
			assertThat(l.entries).hasSize(1);
			LogEntry le = l.entries.get(0);
			assertThat(le.format).isEqualTo("Bundle");
            assertThat(le.bundle).isEqualTo(scr);
			assertThat(le.loggername).isEqualTo(ExtLogManager.SCR_LOGGER_PREFIX + "component");
		}

		{
			l.entries.clear();
			ComponentLogger clog = lm.component(component, "implementation.class", "name");
			clog.log(Level.ERROR, "Component", null);
			assertThat(l.entries).hasSize(1);
			LogEntry le = l.entries.get(0);
			assertThat(le.format).isEqualTo("[name] Component");
			assertThat(le.bundle).isEqualTo(scr);
			assertThat(le.loggername).isEqualTo(ExtLogManager.SCR_LOGGER_PREFIX + "component.name");

			l.entries.clear();
			clog.setComponentId(100);
			clog.log(Level.ERROR, "Component", null);
			le = l.entries.get(0);
			assertThat(le.format).isEqualTo("[name(100)] Component");
		}

		{
			lm.scr().close();
		}
	}
	
	@Test
    public void testDisabledLogging() {
	    LogConfiguration config = mock(LogConfiguration.class);
        
        when(config.isLogEnabled()).thenReturn(false);

        LogService l = new LogService(log.getBundleContext());
        l.register();
        l.levels.put(Logger.ROOT_LOGGER_NAME, LogLevel.ERROR);
        l.defaultLogLevel = LogLevel.ERROR;

        ScrLogger lscr = ScrLoggerFactory.create(scr.getBundleContext(), config);

        assertThat(lscr.isLogEnabled(Level.DEBUG)).isFalse();
        assertThat(lscr.isLogEnabled(Level.INFO)).isFalse();

        lscr.log(Level.DEBUG, "I should not be reported", null);

        assertThat(l.entries).isEmpty();
    }

	@Test
	public void testBackwardCompatibilityOutput() {
	    LogConfiguration config = mock(LogConfiguration.class);
		
		when(config.isLogExtensionEnabled()).thenReturn(false);
		when(config.getLogLevel()).thenReturn(Level.DEBUG);

		LogService l = new LogService(log.getBundleContext());
		l.register();

		ScrLogManager lm = new ScrLogManager(scr.getBundleContext(), config);
        lm.open();

		{
			l.entries.clear();
			ScrLogger scr = lm.scr();
			scr.log(Level.ERROR, "Scr", null);
			assertThat(l.entries).hasSize(1);
			LogEntry le = l.entries.get(0);
			assertThat(le.format).isEqualTo("bundle scr:0.0.0 (1) Scr");
			assertThat(le.bundle).isEqualTo(this.scr);
			assertThat(le.loggername).isEqualTo(Logger.ROOT_LOGGER_NAME);
		}

		{
			l.entries.clear();
			BundleLogger blog = lm.bundle(component);
			blog.log(Level.ERROR, "Bundle", null);
			assertThat(l.entries).hasSize(1);
			LogEntry le = l.entries.get(0);
			assertThat(le.format).isEqualTo("bundle component:0.0.0 (2) Bundle");
			assertThat(le.bundle).isEqualTo(component);
			assertThat(le.loggername).isEqualTo(Logger.ROOT_LOGGER_NAME);
		}

		{
			l.entries.clear();
			ComponentLogger clog = lm.component(component, "implementation.class", "name");
			clog.log(Level.ERROR, "Component", null);
			assertThat(l.entries).hasSize(1);
			LogEntry le = l.entries.get(0);
			assertThat(le.format).isEqualTo("bundle component:0.0.0 (2)[implementation.class] : Component");
			assertThat(le.bundle).isEqualTo(component);
			assertThat(le.loggername).isEqualTo("implementation.class");

			l.entries.clear();
			clog.setComponentId(100);
			clog.log(Level.ERROR, "Component", null);
			le = l.entries.get(0);
			assertThat(le.format).isEqualTo("bundle component:0.0.0 (2)[implementation.class(100)] : Component");
		}
	}

	@Test
	public void testLifeCycle() {
	    LogConfiguration config = mock(LogConfiguration.class);
	    
		LogManager lm = new LogManager(scr.getBundleContext(), config);
        lm.open();
        
		LoggerFacade facade = lm.getLogger(scr, "lifecycle", LoggerFacade.class);
		assertThat(facade.logger).isNull();

		LogService l = new LogService(log.getBundleContext());
		l.register();
		assertThat(l.loggers.get(scr)).isNull();

        Logger logger = (Logger) facade.getLogger();

		assertThat(logger).isNotNull();
		assertThat(facade.logger).isEqualTo(logger);
		assertThat(l.loggers.get(scr)).hasSize(1);

		assertThat(logger.getName()).isEqualTo("lifecycle");

        Logger logger2 = (Logger) facade.getLogger();
		assertThat(logger2).isEqualTo(logger);
		assertThat(l.loggers.get(scr)).hasSize(1);

		l.unregister();
		l.register();

		assertThat(facade.logger).isNull();
        logger = (Logger) facade.getLogger();
		assertThat(l.loggers.get(scr)).hasSize(2);

		assertThat(facade.logger).isNotNull();
		lm.close();
		assertThat(facade.logger).isNull();
	}

	@Test
	public void testPrioritiesLogService() {
	    LogConfiguration config = mock(LogConfiguration.class);
	    
		LogManager lm = new LogManager(scr.getBundleContext(), config);
        lm.open();
        
		LoggerFacade facade = lm.getLogger(scr, "lifecycle", LoggerFacade.class);
		assertThat(facade.logger).isNull();

		LogService la = new LogService(log.getBundleContext());
		la.register();
        Logger loggera = (Logger) facade.getLogger();
		assertThat(loggera).isNotNull();
		assertThat(facade.logger).isEqualTo(loggera);
		assertThat(la.loggers.get(scr)).hasSize(1);

		LogService higherRanking = new LogService(log.getBundleContext()).ranking(10).register();

		assertThat(facade.logger).isNull();

        Logger loggerb = (Logger) facade.getLogger();
		assertThat(loggerb).isNotNull();
		assertThat(higherRanking.loggers.get(scr)).hasSize(1);

		LogService lowerRanking = new LogService(log.getBundleContext()).ranking(-10).register();
		assertThat(facade.logger).isNotNull();
		assertThat(lowerRanking.loggers.get(scr)).isNull();
	}

	@Test
	public void testLifeCycleOfComponentBundle() throws BundleException, InterruptedException {
	    LogConfiguration config = mock(LogConfiguration.class);

		LogService l = new LogService(log.getBundleContext());
		l.register();

		ScrLogManager lm = new ScrLogManager(scr.getBundleContext(), config);
        lm.open();
		lm.component(component, "implementation.class", "component");

		assertThat(lm.lock.domains).hasSize(1);
		component.stop();

		lm.close();
		for (int i = 0; i < 100; i++) {
			if (lm.lock.domains.isEmpty())
				return;
			Thread.sleep(10);
		}
		fail("domains not cleared within 1 sec");
	}

	@Test
	public void testLogLevels() {
	    LogConfiguration config = mock(LogConfiguration.class);

		when(config.getLogLevel()).thenReturn(Level.DEBUG);
		when(config.isLogExtensionEnabled()).thenReturn(false);

		LogService l = new LogService(log.getBundleContext());

		l.register();

		ScrLogManager lm = new ScrLogManager(scr.getBundleContext(), config);
        lm.open();
        
		ScrLogger facade = lm.scr();

		assert LogLevel.values().length == Level.values().length;
		for (int i = 0; i < LogLevel.values().length; i++) {
			assert LogLevel.values()[i].name().equals(Level.values()[i].name());
		}

		Exception ex = new Exception("exception");

		for (LogLevel logLevel : LogLevel.values()) {

			l.defaultLogLevel = logLevel;

			for (Level level : Level.values()) {

				if (logLevel.ordinal() >= level.ordinal())
					assertThat(facade.isLogEnabled(level)).isTrue();
				else
					assertThat(facade.isLogEnabled(level)).isFalse();

				facade.log(level, "level " + level, null);
				facade.log(level, "level " + level, ex);
			}
		}
		assertThat(l.entries).hasSize(42);
	}

	private JarResource makeBundle(String bsn) throws Exception {
		@SuppressWarnings("resource")
		Builder b = new Builder();
		b.setBundleSymbolicName(bsn);
		Jar jar = b.build();
		b.removeClose(jar);
		return new JarResource(jar);
	}

}
