/**
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.commons.file.util;

import com.mucommander.commons.file.AbstractFile;
import com.mucommander.commons.file.FileFactory;
import com.mucommander.commons.file.PermissionAccess;
import com.mucommander.commons.file.PermissionType;
import com.mucommander.commons.io.RandomAccessOutputStream;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.Test;

import java.io.IOException;

/**
 * A test case for the {@link com.mucommander.commons.file.util.FileMonitor} class.
 *
 * @author Maxence Bernard
 */
public class FileMonitorTest implements FileMonitorConstants {

    /** Temporary file used by the current test */
    private AbstractFile file;
    /** FileMonitor used by the current test */
    private FileMonitor fileMonitor;
    /** FileChangeTracker used by the current test */
    private FileChangeTracker fileChangeTracker;

    /** Poll period used by the FileMonitor (in milliseconds) */
    private final static int POLL_PERIOD = 10;

    /** Number of milliseconds to wait for an attribute change before timing out */
    private final static int TIMEOUT = 5000;

    
    /**
     * Validates that FileMonitor properly reports {@link FileMonitor#DATE_ATTRIBUTE} changes when a file's date changes.
     *
     * @throws IOException should not normally happen
     */
    @Test
    public void testDateAttribute() throws IOException {
        setUp(DATE_ATTRIBUTE);

        file.changeDate(file.getDate()-2000);
        assert hasAttributeChanged(DATE_ATTRIBUTE);

        file.changeDate(file.getDate()+2000);
        assert hasAttributeChanged(DATE_ATTRIBUTE);
    }

    /**
     * Validates that FileMonitor properly reports {@link FileMonitor#SIZE_ATTRIBUTE} changes when a file's size changes.
     *
     * @throws IOException should not normally happen
     */
    @Test
    public void testSizeAttribute() throws IOException {
        setUp(SIZE_ATTRIBUTE);

        RandomAccessOutputStream raos = file.getRandomAccessOutputStream();
        try {
            raos.setLength(10);

            assert hasAttributeChanged(SIZE_ATTRIBUTE);

            raos.setLength(0);
        }
        finally {
            if(raos!=null)
                raos.close();
        }
    }

    /**
     * Validates that FileMonitor properly reports {@link FileMonitor#PERMISSIONS_ATTRIBUTE} changes when a file's
     * permissions change.
     *
     * @throws IOException should not normally happen
     */
    @Test
    public void testPermissionsAttribute() throws IOException {
        setUp(PERMISSIONS_ATTRIBUTE);

        file.changePermission(PermissionAccess.USER, PermissionType.WRITE, !file.getPermissions().getBitValue(PermissionAccess.USER, PermissionType.WRITE));
        assert hasAttributeChanged(PERMISSIONS_ATTRIBUTE);
    }

    /**
     * Validates that FileMonitor properly reports {@link FileMonitor#IS_DIRECTORY_ATTRIBUTE} changes when a file
     * becomes a directory and vice-versa.
     *
     * @throws IOException should not normally happen
     */
    @Test
    public void testIsDirectoryAttribute() throws IOException {
        setUp(IS_DIRECTORY_ATTRIBUTE);

        file.delete();
        file.mkdir();
        assert hasAttributeChanged(IS_DIRECTORY_ATTRIBUTE);

        file.delete();
        file.mkfile();
        assert hasAttributeChanged(IS_DIRECTORY_ATTRIBUTE);
    }

    /**
     * Validates that FileMonitor properly reports {@link FileMonitor#EXISTS_ATTRIBUTE} changes when an existing file or
     * directory is deleted, or when a non-existing file or directory is created. 
     *
     * @throws IOException should not normally happen
     */
    @Test
    public void testExistsAttribute() throws IOException {
        setUp(EXISTS_ATTRIBUTE);

        file.delete();
        assert hasAttributeChanged(EXISTS_ATTRIBUTE);

        file.mkdir();
        assert hasAttributeChanged(EXISTS_ATTRIBUTE);

        file.delete();
        assert hasAttributeChanged(EXISTS_ATTRIBUTE);

        file.mkfile();
        assert hasAttributeChanged(EXISTS_ATTRIBUTE);
    }

    /**
     * Called after each test, stops monitoring file changes.
     */
    @AfterMethod
    protected void tearDown() {
        fileMonitor.stopMonitoring();
    }


    /////////////////////////////////
    // Support methods and classes //
    /////////////////////////////////

    /**
     * Sets everything up for a test: retrieves a temporary file instance, create the file, waits until the file exists,
     * create a <code>FileMonitor</code>, register a {@link FileChangeTracker} and finally start monitoring file changes.
     *
     * @param attribute the attribute to monitor
     * @throws IOException should not normally happen
     */
    private void setUp(int attribute) throws IOException {
        // Retrieve a temporary AbstractFile instance that will be deleted on VM shutdown
        file = FileFactory.getTemporaryFile(getClass().getName(), true);
        // Create the file
        file.mkfile();

        // Waits until the file truly exists (I/O are usually asynchroneous)
        while(!file.exists()) {
            try { Thread.sleep(POLL_PERIOD); }
            catch(InterruptedException e) {}
        }

        // Create the monitor, change listener and start monitoring file changes
        fileMonitor = new FileMonitor(file, attribute, POLL_PERIOD);

        fileChangeTracker = new FileChangeTracker();
        fileMonitor.addFileChangeListener(fileChangeTracker);

        fileMonitor.startMonitoring();
    }


    /**
     * Returns <code>true</code> if the current <code>FileMonitor</code> reported a change on the specified attribute.
     * This method will wait up to {@link #TIMEOUT} milliseconds for the attribute to change and if it hasn't changed,
     * will return <code>false</code>.
     *
     * @param attribute the attribute to test against
     * @return true if the current <code>FileMonitor</code> reported a change on the specified attribute
     */
    private boolean hasAttributeChanged(int attribute) {
        boolean hasAttributeChanged = false;

        try {
            synchronized(fileChangeTracker) {
                hasAttributeChanged = (attribute&fileChangeTracker.getChangedAttributes())!=0;

                if(!hasAttributeChanged) {
                    // Waits until FileChangeTracker calls notify to report an attribute change; give up after
                    // TIMEOUT milliseconds
                    fileChangeTracker.wait(TIMEOUT);
                }

                hasAttributeChanged = (attribute&fileChangeTracker.getChangedAttributes())!=0;
            }
        }
        catch(InterruptedException e) {}

        // Resets FileChangeTracker to be ready to detect the next attribute change
        fileChangeTracker.reset();

        return hasAttributeChanged;
    }

    /**
     * This {@link FileChangeListener} keeps track of the attributes that changed, as reported by
     * {@link #fileChanged(com.mucommander.commons.file.AbstractFile, int)}.
     */
    private static class FileChangeTracker implements FileChangeListener {

        /** Bit mask that describes the attributes that have changed */
        private int changedAttributes;

        /**
         * Returns a bit mask that describes the attributes that have changed.
         *
         * @return a bit mask that describes the attributes that have changed
         */
        private int getChangedAttributes() {
            return changedAttributes;
        }

        /**
         * Resets the changed attributes bit mask to zero.
         */
        private void reset() {
            this.changedAttributes = 0;
        }

        ///////////////////////////////////////
        // FileChangeListener implementation //
        ///////////////////////////////////////

        public void fileChanged(AbstractFile file, int changedAttributes) {
            synchronized(this) {
                this.changedAttributes |= changedAttributes;

                notify();   // Notify that hasAttributeChanged(int) method that an attribute has changed 
            }
        }
    }

}
