/*
 * 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 test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Hashtable;

import javax.crypto.Cipher;

import org.apache.felix.dm.Component;
import org.apache.felix.dm.DependencyManager;
import org.apache.felix.dm.shell.DMCommand;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;

/**
 * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
 */
public class DMCommandTest {
    /** System output just used to debug **/
    private final static PrintStream OUT =
        new PrintStream(new BufferedOutputStream(new FileOutputStream(FileDescriptor.out), 128));
    
    /** Setup a ByteArrayOutputStream to capture the system out printlines */
    private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
    private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
    
    private DependencyManager dm;
    @Spy @InjectMocks private DMCommand dme;
    @Mock private BundleContext m_bundleContext;

    @Before
    public void setUp() throws Exception {
        m_bundleContext = mock(BundleContext.class);
        Bundle bundle = mock(Bundle.class);
        when(m_bundleContext.getBundle()).thenReturn(bundle);
        System.setOut(new PrintStream(outContent));
        System.setErr(new PrintStream(errContent));
        dm = new DependencyManager(m_bundleContext);
        dme = new DMCommand(m_bundleContext);
    }

    @After
    public void cleanUp() {
        System.setOut(null);
        System.setErr(null);
    }

    @Test
    public void testWithoutAnyDependcyManagersShouldNotCrash() {
        OUT.println("testWithoutAnyDependcyManagersShouldNotCrash");
        setupEmptyBundles();
        
        dme.wtf();
        assertEquals("No unregistered components found\n", outContent.toString());
    }

    @Test
    public void testASingleComponentShouldNotRegisterAsFailure() {
        OUT.println("testASingleComponentShouldNotRegisterAsFailure");
        setupEmptyBundles();
        
        dm.add(dm.createComponent()
            .setImplementation(Object.class)
            .setInterface(Object.class.getName(), null)
            );
        dme.wtf();
        assertEquals("No unregistered components found\n", outContent.toString());
    }
    
    @Test
    public void testComponentThatDependsOnAOtheComponentShouldRegisterAsFailure() {
        OUT.println("testComponentThatDependsOnAOtheComponentShouldRegisterAsFailure");
        setupEmptyBundles();
        DependencyManager dm = new DependencyManager(m_bundleContext);
        DependencyManager.getDependencyManagers().add(dm);
        
        Component component = dm.createComponent()
            .setImplementation(Object.class)
            .setInterface(Object.class.getName(), null)
            .add(dm.createServiceDependency().setService(Math.class).setRequired(true));
        dm.add(component);
        
        dme.wtf();
        String output = outContent.toString();
        assertTrue(output.contains("1 unregistered"));
        assertTrue(output.contains("java.lang.Math"));
        
        // remove the mess
        dm.remove(component);
    }
    
    @Test
    public void testComponentThatHaveCycliclyDependencyOnAOtheComponentShouldRegisterAsFailure() {
        OUT.println("testComponentThatHaveCycliclyDependencyOnAOtheComponentShouldRegisterAsFailure");
        setupEmptyBundles();
        DependencyManager dm = new DependencyManager(m_bundleContext);
        DependencyManager.getDependencyManagers().add(dm);
        
        Component component1 = dm.createComponent()
            .setImplementation(Cipher.class)
            .setInterface(Cipher.class.getName(), null)
            .add(dm.createServiceDependency().setService(Math.class).setRequired(true));
        dm.add(component1);
        
        Component component2 = dm.createComponent()
            .setImplementation(Math.class)
            .setInterface(Math.class.getName(), null)
            .add(dm.createServiceDependency().setService(Cipher.class).setRequired(true));
        dm.add(component2);
        
        dme.wtf();
        String output = outContent.toString();
        assertTrue(output.contains("-> java.lang.Math -> javax.crypto.Cipher -> java.lang.Math") ||
        		output.contains("-> javax.crypto.Cipher -> java.lang.Math -> javax.crypto.Cipher"));
        
        // remove the mess
        dm.remove(component1);
        dm.remove(component2);
    }
    
    @Test
    public void testCanFindRootFailure() {
        OUT.println("testCanFindRootFailure");
        setupEmptyBundles();
        
        Component component1 = dm.createComponent()
            .setImplementation(Object.class)
            .setInterface(Object.class.getName(), null)
            .add(dm.createServiceDependency().setService(Math.class).setRequired(true));
        dm.add(component1);
        
        Component component2 = dm.createComponent()
            .setImplementation(Math.class)
            .setInterface(Math.class.getName(), null)
            .add(dm.createServiceDependency().setService(String.class).setRequired(true));
        dm.add(component2);
        
        dme.wtf();
        String output = outContent.toString();
        assertTrue(output.contains("2 unregistered"));
        assertTrue(output.contains("java.lang.String"));
        
        // remove the mess
        dm.remove(component1);
        dm.remove(component2);
    }
    
    @Test
    public void testCanFindRootFailureWithSecondair() {
        OUT.println("testCanFindRootFailureWithSecondair");
        setupEmptyBundles();
        
        Component component1 = dm.createComponent()
            .setImplementation(Object.class)
            .setInterface(Object.class.getName(), null)
            .add(dm.createServiceDependency().setService(Math.class).setRequired(true));
        dm.add(component1);
        
        Component component2 = dm.createComponent()
            .setImplementation(Math.class)
            .setInterface(Math.class.getName(), null)
            .add(dm.createServiceDependency().setService(Float.class).setRequired(true));
        dm.add(component2);
        
        Component component3 = dm.createComponent()
            .setImplementation(Object.class)
            .setInterface(new String[] {Object.class.getName(), Float.class.getName()}, null)
            .add(dm.createServiceDependency().setService(String.class).setRequired(true));
        dm.add(component3);
        
        dme.wtf();
        String output = outContent.toString();
        assertTrue(output.contains("3 unregistered"));
        assertTrue(output.contains("java.lang.String"));
        assertFalse(output.contains("java.lang.Float"));
        
        // remove the mess
        dm.remove(component1);
        dm.remove(component2);
        dm.remove(component3);
    }
    
    @Test
    public void testCanFindRootFailureWithTwoFailures() {
        OUT.println("testCanFindRootFailureWithTwoFailures");
        setupEmptyBundles();
        
        Component component1 = dm.createComponent()
            .setImplementation(Object.class)
            .setInterface(Object.class.getName(), null)
            .add(dm.createServiceDependency().setService(Math.class).setRequired(true))
            .add(dm.createServiceDependency().setService(Long.class).setRequired(true));
        dm.add(component1);
        
        
        dme.wtf();
        String output = outContent.toString();
        assertTrue(output.contains("1 unregistered"));
        assertTrue(output.contains("java.lang.Math"));
        assertTrue(output.contains("java.lang.Long"));
        
        // remove the mess
        dm.remove(component1);
    }
    
    @Test
    public void testInstalledBundleListing() {
        OUT.println("testInstalledBundleListing");
        Bundle bundle1 = mock(Bundle.class);
        when(bundle1.getState()).thenReturn(Bundle.INSTALLED);
        when(bundle1.getSymbolicName()).thenReturn("BadBundle");
        
        setupBundles(bundle1);
        
        dme.wtf();
        String output = outContent.toString();
        assertTrue(output.contains("following bundles are in the INSTALLED"));
        assertTrue(output.contains("[0] BadBundle"));
        // Will print null if it gets bundle 2, that should not happen
        assertFalse(output.contains("null"));
    }
    
    @Test
    public void testResolvedBundleListing() {
        OUT.println("testResolvedBundleListing");
        Bundle bundle1 = mock(Bundle.class);
        when(bundle1.getState()).thenReturn(Bundle.RESOLVED);
        when(bundle1.getSymbolicName()).thenReturn("BadBundle");
        Hashtable<String, String> headers = new Hashtable<>();
        when(bundle1.getHeaders()).thenReturn(headers);
        
        setupBundles(bundle1);
        
        dme.wtf();
        String output = outContent.toString();
        assertTrue(output.contains("following bundles are in the RESOLVED"));
        assertTrue(output.contains("[0] BadBundle"));
        assertFalse(output.contains("null"));
    }
    
    @Test
    public void testResolvedBundleListingButNoFragements() {
        OUT.println("testResolvedBundleListingButNoFragements");
        Bundle bundle1 = mock(Bundle.class);
        when(bundle1.getState()).thenReturn(Bundle.RESOLVED);
        when(bundle1.getSymbolicName()).thenReturn("BadBundle");
        Hashtable<String, String> headers = new Hashtable<>();
        headers.put("Fragment-Host", "some value");
        when(bundle1.getHeaders()).thenReturn(headers);
        setupBundles(bundle1);
        
        dme.wtf();
        String output = outContent.toString();
        assertFalse(output.contains("following bundles are in the RESOLVED"));
        // Will print null if it gets bundle 2, that should not happen
        assertFalse(output.contains("null"));
    }

    private void setupBundles( Bundle bundle1) {
        Bundle bundle2 = mock(Bundle.class);
        when(bundle2.getState()).thenReturn(Bundle.ACTIVE);
        
        when(m_bundleContext.getBundles()).thenReturn(new Bundle[] { bundle1, bundle2});
    }
    
    /** Sets up the bundle context without any bundles */
    private void setupEmptyBundles() {
        when(m_bundleContext.getBundles()).thenReturn(new Bundle[] {});
    }

}
