/*
 * 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.netbeans.core.output2;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.EnumSet;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;
import org.netbeans.junit.NbTestCase;
import org.netbeans.junit.RandomlyFails;
import org.openide.util.Exceptions;
import org.openide.windows.IOContainer;
import org.openide.windows.IOSelect;
import org.openide.windows.OutputWriter;

/**
 *
 * @author tim
 */
@RandomlyFails // timed out in NB-Core-Build #3589; no thread dump despite timeOut override
public class LifecycleTest extends NbTestCase {

    public LifecycleTest(String testName) {
        super(testName);
    }

    protected @Override int timeOut() {
        return 300000;
    }

    private IOContainer container;
    private NbIO io;
    private NbIOProvider ioProvider;
    JFrame jf = null;

    OutputTab tab = null;
    OutputPane pane = null;
    @Override
    protected void setUp() throws java.lang.Exception {
        SwingUtilities.invokeAndWait(new Runnable() {

            public void run() {
                container = IOContainer.getDefault();
                jf = new JFrame();
                jf.getContentPane().setLayout(new BorderLayout());
                jf.getContentPane().add(getIOWindow(), BorderLayout.CENTER);
                jf.setBounds(20, 20, 700, 300);
                jf.setVisible(true);
                ioProvider = new NbIOProvider();
                io = (NbIO) ioProvider.getIO("Test", false);
                io.select();
                tab = (OutputTab) container.getSelected();
                pane = (OutputPane) tab.getOutputPane();
            }
        });
        if (tab == null) {
            fail("Failed in setup - selected tab was null");
        }
    }
    
    @Override
    protected void tearDown() {
        tab = null;
        pane = null;
        if (jf != null) {
            jf.dispose();
        }
        jf = null;
        if (io != null) {
            NbIOProvider.dispose(io);
        }
        io.closeInputOutput();
        io = null;
        ioProvider = null;
        container = null;
        sleep();
    }
    
    private final void sleep() {
        dosleep();
        dosleep();
        dosleep();
        int ct = 0;
        while (IOEvent.pendingCount > 0) {
            dosleep();
            ct++;
            if (ct > 1000) {
                fail ("After 1000 cycles on the event queue, there is still some IOEvent which was not processed");
            }
        }
    }
    
    private final void dosleep() {
        try {
            Thread.sleep(200);
            SwingUtilities.invokeAndWait (new Runnable() {
                public void run() {
                    System.currentTimeMillis();
                }
            });
            Thread.sleep(200);
        } catch (Exception e) {
            fail (e.getMessage());
        }
    }

    public void testGetErr() throws Exception {
        System.out.println("testGetOut");
        ErrWriter err = io.writer().err();
        assertNull ("Error output should not be created yet", err);
        err = io.writer().getErr();
        assertNotNull ("Error output should never be null from getErr()", err);
        assertTrue ("Error output should initially be closed", err.isClosed());
        err.println ("Hello");
        assertFalse ("Error output should not be closed after writing to it", err.isClosed());
        err.close();
        assertTrue ("Error output be closed after calling close()", err.isClosed());
    }

    public void testClose() throws Exception {
        System.out.println("testClose");
        NbWriter writer = (NbWriter) io.getOut();
        ErrWriter err = writer.getErr();
        OutWriter out = writer.out();

        writer.reset();
        sleep();

        err.println ("hello");
        sleep();
        writer.println ("world");
        sleep();

//        assertTrue("Text in container not correct:\"" + pane.getTextView().getText() +"\"",
//            pane.getTextView().getText().equals ("hello\nworld\n\n\n"));

        assertFalse ("Err should not be closed", err.isClosed());
        assertFalse ("Writer should not be closed", writer.isClosed());
//        assertFalse ("Out should not be closed", out.isClosed());

        err.close();
        sleep();
        assertFalse ("Out is open, err is closed, writer should return false from isClosed()", writer.isClosed());

        writer.close();
        sleep();
        assertTrue ("Out should be closed after calling close() on it", out.isClosed());
        assertTrue ("Out and err are closed, but writer says it is not", writer.isClosed());

        assertTrue ("Output's storage is not closed", writer.out().getStorage().isClosed());

        writer.reset();
        sleep();

        assertTrue ("After reset, err should be closed", err.isClosed());
        assertTrue ("After reset, writer should be closed", writer.isClosed());
        assertTrue ("After reset, out should be closed", out.isClosed());

        err.println ("goodbye");
        writer.println ("world");
        sleep();

        assertFalse ("Err should not be closed", err.isClosed());
        assertFalse ("Writer should not be closed", writer.isClosed());
//        assertFalse ("Out should not be closed", out.isClosed());

        //Now close err & out in the opposite order
        writer.close();
        sleep();
        assertTrue ("Out should  be closed after calling close() on it", writer.isClosed());

        err.close();
        sleep();
        assertTrue ("Out is closed, err is closed, writer should return true from isClosed()", writer.isClosed());
        assertTrue ("Out and err are closed, but writer says it is not", writer.isClosed());

        assertTrue ("Output's storage is not closed", writer.out().getStorage().isClosed());

        err.println("I should be reopened now");
        sleep();

        assertFalse ("Err should be open", err.isClosed());
    }

    public void testReset() throws Exception {
        System.out.println("testReset");
        ErrWriter err = io.writer().getErr();
        OutWriter out = io.writer().out();
        NbWriter writer = io.writer();

        OutputDocument doc = (OutputDocument) pane.getDocument();
        assertNotNull ("Document should not be null", doc);

        err.println ("hello");
        writer.println ("world");
        sleep();
        writer.reset();
        sleep();

        assertTrue ("Same writer object should be used after a reset", io.writer() == writer);
        assertTrue ("Same err object should be used after a reset", io.writer().err() == err);
        assertTrue ("Different output should be used afer a reset", out != io.writer().out());

        assertNull ("Old document's Lines object not disposed - that means neither was its writer", doc.getLines());

        Exception e = null;
        try {
            out.getStorage();
        } catch (Exception exc) {
            e = exc;
        }
        assertNotNull ("OutWriter should have thrown an exception on trying to " +
            "fetch its storage after it was disposed.  It appears it wasn't disposed.", e);
    }

    /**
     * org.netbeans.api.io.InputOutput uses InputOutput.reset(),
     * NOT writer.reset().[NETBEANS-4203]
     * <p>
     * Identical to testReset, expcept use ioProvider.resetIO(io) not writer.reset()
     * </p>
     * @throws Exception 
     */
    public void testNbIOReset() throws Exception {
        System.out.println("testNbIOReset");
        ErrWriter err = io.writer().getErr();
        OutWriter out = io.writer().out();
        NbWriter writer = io.writer();

        OutputDocument doc = (OutputDocument) pane.getDocument();
        assertNotNull ("Document should not be null", doc);

        err.println ("hello");
        writer.println ("world");
        sleep();
        ioProvider.resetIO(io);
        sleep();

        assertTrue ("Same writer object should be used after a reset", io.writer() == writer);
        assertTrue ("Same err object should be used after a reset", io.writer().err() == err);
        assertTrue ("Different output should be used afer a reset", out != io.writer().out());

        assertNull ("Old document's Lines object not disposed - that means neither was its writer", doc.getLines());

        Exception e = null;
        try {
            out.getStorage();
        } catch (Exception exc) {
            e = exc;
        }
        assertNotNull ("OutWriter should have thrown an exception on trying to " +
            "fetch its storage after it was disposed.  It appears it wasn't disposed.", e);
    }

    public void testCloseInputOutput() throws Exception {

        System.out.println("testCloseInputOutput");
        ErrWriter err = io.writer().getErr();
        OutWriter out = io.writer().out();
        NbWriter writer = io.writer();

        err.println ("joy to the world");
        writer.println ("all the boys and girls");
        err.close();
        sleep();
        writer.close();
        sleep();
        io.closeInputOutput();
        sleep();
        assertNull ("Should be no selected tab after closeInputOutput", getSelectedTab());
    }

    public void testDisplayOfBackspace() throws Exception {

        OutWriter out = io.writer().out();
        out.println("Enter something:");
        out.print(">");
        sleep();
        out.println("\bPrompt removed.");
        sleep();
        out.println("xx");
        sleep();
        io.closeInputOutput();
    }

    public void testFilesCleanedUp() throws Exception {
        System.out.println("testFilesCleanedUp");
        NbWriter writer = io.writer();
        ErrWriter err = writer.getErr();
        OutWriter out = writer.out();

        err.println ("hello");
        writer.println ("world");
        sleep();

        assertTrue ("Output should not have changed - was " + out + " now " + io.writer().out(), io.writer().out() == out);
        FileMapStorage storage = (FileMapStorage) writer.out().getStorage();
        String fname = storage.toString();
        assertTrue ("FileMapStorage should be returning a file name", fname.indexOf("[") == -1);
        assertTrue ("FileMapStorage should be pointing to an existing file", new File(fname).exists());

        err.close();
        sleep();
        writer.close();
        sleep();
        io.closeInputOutput();
        sleep();

        assertTrue (out.isDisposed());
        sleep();
//        assertFalse ("FileMapStorage's file should have been deleted", new File(fname).exists());
    }

    public void testFastResets() throws IOException, InterruptedException {
        System.out.println("testFastResets");
        OutputWriter out = io.getOut();
        for (int i = 0; i < 100; i++) {
            for (int k = 0; k < 10; k++) {
                out.println(i + " " + k);
            }
            Thread.sleep(10);
            out.close();
            out.reset();
        }
    }

    public void testMultipleResetsAreHarmless() throws Exception {
        System.out.println("testMultipleResetsAreHarmless");
        NbWriter writer = io.writer();
        ErrWriter err = writer.getErr();
        OutWriter out = writer.out();

        assertTrue ("Before any writes, out should be empty", out.isEmpty());

        writer.reset();
        sleep();
        assertTrue ("Reset on an unused writer should not replace its output", writer.out() == out);

        writer.reset();
        writer.reset();
        writer.reset();
        sleep();
        assertTrue ("Reset on an unused writer should not replace its output", writer.out() == out);

        writer.println ("Now there is data");
        writer.reset();
        sleep();

        assertFalse ("Reset on a used writer should replace its underlying output", writer.out() == out);

    }

    // Not really a Lifecycle feature but requires a bonafide TC container
    // which this Test provides.
    public void testFineSelect() {
        System.out.println("testFineSelect");

	EnumSet<IOSelect.AdditionalOperation> extraOps;

	extraOps = null;
	boolean sawException = false;
	try {
	    IOSelect.select(io, extraOps);
	} catch (NullPointerException ex) {
	    // extraOps is NonNull
	    sawException = true;
	}
	assertTrue("null pointer not caught", sawException);
	sleep();

	extraOps = EnumSet.noneOf(IOSelect.AdditionalOperation.class);
	IOSelect.select(io, extraOps);
	sleep();

	extraOps = EnumSet.of(IOSelect.AdditionalOperation.OPEN);
	IOSelect.select(io, extraOps);
	sleep();

	extraOps = EnumSet.of(IOSelect.AdditionalOperation.REQUEST_ACTIVE);
	IOSelect.select(io, extraOps);
	sleep();

	extraOps = EnumSet.of(IOSelect.AdditionalOperation.REQUEST_VISIBLE);
	IOSelect.select(io, extraOps);
	sleep();

	extraOps = EnumSet.of(IOSelect.AdditionalOperation.OPEN,
		              IOSelect.AdditionalOperation.REQUEST_VISIBLE);
	IOSelect.select(io, extraOps);
	sleep();

	extraOps = EnumSet.of(IOSelect.AdditionalOperation.OPEN,
		              IOSelect.AdditionalOperation.REQUEST_VISIBLE,
			      IOSelect.AdditionalOperation.REQUEST_ACTIVE);
	IOSelect.select(io, extraOps);
	sleep();
    }

    /**
     * Test for bug 239194 - NPE at
     * org.netbeans.core.output2.FoldingSideBar.paintComponent.
     *
     * @throws java.lang.InterruptedException
     */
    public void testBug239194() throws InterruptedException {
        final Semaphore s = new Semaphore(0);
        final Exception[] excRef = new Exception[1];
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                NbIO io = (NbIO) NbIOProvider.getDefault().getIO("bug239194",
                        true);
                try {
                    FoldingSideBar fsb = pane.getFoldingSideBar();
                    JTextComponent jtc = pane.getTextView();
                    jtc.setVisible(false);
                    jtc.setMinimumSize(new Dimension(-1, -1));
                    jtc.setSize(new Dimension(-1, -1));
                    Graphics g = new BufferedImage(1, 1,
                            BufferedImage.TYPE_INT_RGB).getGraphics();
                    g.setClip(0,0,1,1);
                    fsb.paintComponent(g);
                } catch (Exception e) {
                    Exceptions.printStackTrace(e);
                    excRef[0] = e;
                } finally {
                    io.closeInputOutput();
                    io.dispose();
                }
                s.release();
            }
        });
        assertTrue("Timout", s.tryAcquire(30, TimeUnit.MINUTES));
        assertNull("No exception should be thrown", excRef[0]);
    }


    /**
     * Test for bug 242979.
     *
     * @throws java.lang.InterruptedException
     * @throws java.lang.reflect.InvocationTargetException
     */
    public void testDoubleReset() throws InterruptedException, InvocationTargetException {

        io.getOut().println("First line");
        final Object lock = new Object();

        class OutputResetter implements Runnable {

            @Override
            public void run() {
                try {
                    final OutputWriter pw = io.getOut();
                    synchronized (lock) {
                        pw.reset();
                        pw.println("test println");
                    }
                } catch (IOException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        }

        final Semaphore s = new Semaphore(0);
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(100); // block EDT for a while
                    s.release();
                } catch (InterruptedException ex) {
                    Exceptions.printStackTrace(ex);
                }
            }
        });
        Thread t1 = new Thread(new OutputResetter(), "Thread 1");
        Thread t2 = new Thread(new OutputResetter(), "Thread 2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();
        s.tryAcquire(100, TimeUnit.MILLISECONDS);

        EventQueue.invokeAndWait(new Runnable() {

            @Override
            public void run() {
                // process scheduled events
            }
        });

        assertFalse(((NbWriter) io.getOut()).out().isDisposed());

        io.closeInputOutput();
    }

    static JComponent getIOWindow() {
        IOContainer container = IOContainer.getDefault();
        JComponent comp = null;
        try {
            Field f = container.getClass().getDeclaredField("provider");
            f.setAccessible(true);
            IOContainer.Provider prov = (IOContainer.Provider) f.get(container);
            Method m = prov.getClass().getDeclaredMethod("impl", new Class<?>[0]);
            m.setAccessible(true);
            comp = (JComponent) m.invoke(prov);
        } catch (InvocationTargetException | NoSuchMethodException | IllegalArgumentException
                | IllegalAccessException | NoSuchFieldException | SecurityException ex) {
            Exceptions.printStackTrace(ex);
        }
        return comp;
    }

    JComponent getSelectedTab() {
        class R implements Runnable {
            JComponent tab;
            public void run() {
                tab = container.getSelected();
            }
        }
        R r = new R();
        try {
            SwingUtilities.invokeAndWait(r);
        } catch (InterruptedException ex) {
            Exceptions.printStackTrace(ex);
        } catch (InvocationTargetException ex) {
            Exceptions.printStackTrace(ex);
        }
        return r.tab;
    }
    
}
