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

import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

import junit.extensions.TestSetup;
import junit.framework.Protectable;
import junit.framework.Test;
import junit.framework.TestResult;
import junit.framework.TestSuite;

import org.apache.commons.io.FileUtils;

import org.apache.commons.AbstractVfsTestCase;
import org.apache.commons.vfs2.FileName;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.impl.DefaultFileReplicator;
import org.apache.commons.vfs2.impl.DefaultFileSystemManager;
import org.apache.commons.vfs2.impl.PrivilegedFileReplicator;
import org.apache.commons.vfs2.provider.local.DefaultLocalFileProvider;

/**
 * The suite of tests for a file system.
 */
public abstract class AbstractTestSuite
    extends TestSetup
{
    private final ProviderTestConfig providerConfig;
    private final String prefix;
    private TestSuite testSuite;

    private FileObject baseFolder;
    private FileObject readFolder;
    private FileObject writeFolder;
    private DefaultFileSystemManager manager;
    private File tempDir;

    private Thread[] startThreadSnapshot;
    private Thread[] endThreadSnapshot;
    private final boolean addEmptyDir;

    /**
     * Adds the tests for a file system to this suite.
     */
    public AbstractTestSuite(final ProviderTestConfig providerConfig) throws Exception
    {
        this(providerConfig, "", false, false);
    }

    protected AbstractTestSuite(final ProviderTestConfig providerConfig,
                                final String prefix,
                                final boolean nested) throws Exception
    {
        this(providerConfig, prefix, nested, false);
    }


    protected AbstractTestSuite(final ProviderTestConfig providerConfig,
                                final String prefix,
                                final boolean nested,
                                final boolean addEmptyDir)
        throws Exception
    {
        super(new TestSuite());
        testSuite = (TestSuite) fTest;
        this.providerConfig = providerConfig;
        this.prefix = prefix;
        this.addEmptyDir = addEmptyDir;
        addBaseTests();
        if (!nested)
        {
            // Add nested tests
            // TODO - move nested jar and zip tests here
            // TODO - enable this again
            //testSuite.addTest( new ProviderTestSuite( new JunctionProviderConfig( providerConfig ), "junction.", true ));
        }
    }

    /**
     * Adds base tests - excludes the nested test cases.
     */
    protected void addBaseTests() throws Exception
    {
    }

    /**
     * Adds the tests from a class to this suite.  The supplied class must be
     * a subclass of {@link AbstractProviderTestCase} and have a public a
     * no-args constructor.  This method creates an instance of the supplied
     * class for each public 'testNnnn' method provided by the class.
     */
    public void addTests(final Class<?> testClass) throws Exception
    {
        // Verify the class
        if (!AbstractProviderTestCase.class.isAssignableFrom(testClass))
        {
            throw new Exception("Test class " + testClass.getName() + " is not assignable to " + AbstractProviderTestCase.class.getName());
        }

        // Locate the test methods
        final Method[] methods = testClass.getMethods();
        for (final Method method2 : methods)
        {
            final Method method = method2;
            if (!method.getName().startsWith("test")
                || Modifier.isStatic(method.getModifiers())
                || method.getReturnType() != Void.TYPE
                || method.getParameterTypes().length != 0)
            {
                continue;
            }

            // Create instance
            final AbstractProviderTestCase testCase = (AbstractProviderTestCase) testClass.newInstance();
            testCase.setMethod(method);
            testCase.setName(prefix + method.getName());
            testCase.addEmptyDir(this.addEmptyDir);
            testSuite.addTest(testCase);
        }
    }

    @Override
    public void run(final TestResult result) {
        final Protectable p = new Protectable() {
            @Override
            public void protect() throws Exception {
                setUp();
                basicRun(result);
                tearDown();
                validateThreadSnapshot();
            }
        };
        result.runProtected(this, p);
    }

    @Override
    protected void setUp() throws Exception
    {
        startThreadSnapshot = createThreadSnapshot();

        // Locate the temp directory, and clean it up
        tempDir = AbstractVfsTestCase.getTestDirectory("temp");
        FileUtils.cleanDirectory(tempDir);
        checkTempDir("Temp dir not empty before test");

        // Create the file system manager
        manager = providerConfig.getDefaultFileSystemManager();
        manager.setFilesCache(providerConfig.getFilesCache());

        final DefaultFileReplicator replicator = new DefaultFileReplicator(tempDir);
        manager.setReplicator(new PrivilegedFileReplicator(replicator));
        manager.setTemporaryFileStore(replicator);

        providerConfig.prepare(manager);

        if (!manager.hasProvider("file"))
        {
            manager.addProvider("file", new DefaultLocalFileProvider());
        }

        manager.init();

        // Locate the base folders
        baseFolder = providerConfig.getBaseTestFolder(manager);
        readFolder = baseFolder.resolveFile("read-tests");
        writeFolder = baseFolder.resolveFile("write-tests");

        // Make some assumptions about the read folder
        assertTrue("Folder does not exist: " + readFolder, readFolder.exists());
        assertFalse(readFolder.getName().getPath().equals(FileName.ROOT_PATH));

        // Configure the tests
        final Enumeration<Test> tests = testSuite.tests();
        while (tests.hasMoreElements())
        {
            final Test test = tests.nextElement();
            if (test instanceof AbstractProviderTestCase)
            {
                final AbstractProviderTestCase providerTestCase = (AbstractProviderTestCase) test;
                providerTestCase.setConfig(manager, providerConfig, baseFolder, readFolder, writeFolder);
            }
        }
    }

    @Override
    protected void tearDown() throws Exception
    {
        readFolder.close();
        writeFolder.close();
        baseFolder.close();

        readFolder = null;
        writeFolder = null;
        baseFolder = null;
        testSuite = null;

        // force the SoftRefFilesChache to free all files
        System.gc();
        Thread.sleep(1000);
        System.gc();
        Thread.sleep(1000);
        System.gc();
        Thread.sleep(1000);
        System.gc();
        Thread.sleep(1000);

        manager.freeUnusedResources();
        manager.close();

        // Make sure temp directory is empty or gone
        checkTempDir("Temp dir not empty after test");
    }

    private void validateThreadSnapshot()
    {
        endThreadSnapshot = createThreadSnapshot();

        final Thread[] diffThreadSnapshot = diffThreadSnapshot(startThreadSnapshot, endThreadSnapshot);
        if (diffThreadSnapshot.length > 0)
        {
            final String message = dumpThreadSnapshot(diffThreadSnapshot);
            /*
            if (providerConfig.checkCleanThreadState())
            {
                // close the manager to do a "not thread safe" release of all resources
                // and allow the vm to shutdown
                manager.close();
                fail(message);
            }
            else
            {
            */
            System.out.println(message);
            // }
        }
        // System.in.read();
    }

    /**
     * Asserts that the temp dir is empty or gone.
     */
    private void checkTempDir(final String assertMsg)
    {
        if (tempDir.exists())
        {
            assertTrue(assertMsg + " (" + tempDir.getAbsolutePath() + ")", tempDir.isDirectory() && tempDir.list().length == 0);
        }
    }

    private String dumpThreadSnapshot(final Thread[] threadSnapshot)
    {
        final StringBuffer sb = new StringBuffer(256);
        sb.append("created threads still running:\n");

        Field threadTargetField = null;
        try
        {
            threadTargetField = Thread.class.getDeclaredField("target");
            threadTargetField.setAccessible(true);
        }
        catch (final NoSuchFieldException e)
        {
            // ignored
        }

        for (int iter = 0; iter < threadSnapshot.length; iter++)
        {
            final Thread thread = threadSnapshot[iter];
            if (thread == null || !thread.isAlive())
            {
                continue;
            }

            sb.append("#");
            sb.append(iter + 1);
            sb.append(": ");
            sb.append(thread.getThreadGroup() != null ?
                thread.getThreadGroup().getName() : "(null)");
            sb.append("\t");
            sb.append(thread.getName());
            sb.append("\t");
            sb.append(thread.getState());
            sb.append("\t");
            if (thread.isDaemon())
            {
                sb.append("daemon");
            }
            else
            {
                sb.append("not_a_daemon");
            }

            if (threadTargetField != null)
            {
                sb.append("\t");
                try
                {
                    final Object threadTarget = threadTargetField.get(thread);
                    if (threadTarget != null)
                    {
                        sb.append(threadTarget.getClass());
                    }
                    else
                    {
                        sb.append("null");
                    }
                }
                catch (final IllegalAccessException e)
                {
                    sb.append("unknown class");
                }
            }

            sb.append("\n");
        }

        return sb.toString();
    }

    private Thread[] diffThreadSnapshot(final Thread[] startThreadSnapshot, final Thread[] endThreadSnapshot)
    {
        final List<Thread> diff = new ArrayList<Thread>(10);

        nextEnd: for (int iterEnd = 0; iterEnd < endThreadSnapshot.length; iterEnd++)
        {
            for (int iterStart = 0; iterStart < startThreadSnapshot.length; iterStart++)
            {
                if (startThreadSnapshot[iterStart] == endThreadSnapshot[iterEnd])
                {
                    continue nextEnd;
                }
            }

            diff.add(endThreadSnapshot[iterEnd]);
        }

        final Thread ret[] = new Thread[diff.size()];
        diff.toArray(ret);
        return ret;
    }

    private Thread[] createThreadSnapshot()
    {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        while (tg.getParent() != null)
        {
            tg = tg.getParent();
        }

        final Thread snapshot[] = new Thread[200];
        tg.enumerate(snapshot, true);

        return snapshot;
    }
}
