package com.limegroup.gnutella.downloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.limewire.collection.Interval;
import org.limewire.util.PrivilegedAccessor;

import junit.framework.Test;

import com.limegroup.gnutella.FileDesc;
import com.limegroup.gnutella.FileManager;
import com.limegroup.gnutella.IncompleteFileDesc;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.settings.SharingSettings;
import com.limegroup.gnutella.stubs.ActivityCallbackStub;

@SuppressWarnings( { "unchecked", "cast" } )
public class IncompleteFileManagerTest extends com.limegroup.gnutella.util.LimeTestCase {
    private IncompleteFileManager ifm;
    private RemoteFileDesc rfd1, rfd2;
    private FileManager fm;
    
    public IncompleteFileManagerTest(String name) {
        super(name);
    }
    
    public static Test suite() {
        return buildTestSuite(IncompleteFileManagerTest.class);
    }
    
    public static void globalSetUp() {
        new RouterService(new ActivityCallbackStub());
    }
    
    public void setUp() {
        ifm=new IncompleteFileManager();
        fm = RouterService.getFileManager();
    }

    /** @param urn a SHA1 urn, or null */
    public static RemoteFileDesc newRFD(String name, int size, String urn) {
       try {
           Set urns=new HashSet(1);
           if (urn!=null) 
               urns.add(URN.createSHA1Urn(urn));
           return new RemoteFileDesc(
               "18.239.0.144", 6346, 13l,
               name, size,
               new byte[16], 56, false, 4, true, null,
               urns, false, false,"",null, -1);
       } catch (IOException e) {
           fail("Invalid URN", e);
           return null;
       }
    }

    /////////////////////////////////////////////////////////////

	public void testLegacy() throws Throwable {
        File file=new File(getSaveDirectory(), "T-748-test.txt");
        IncompleteFileManager ifm=new IncompleteFileManager();
        Iterator iter=null;
        VerifyingFile vf = new VerifyingFile(748);
        //0 blocks
        assertNull(ifm.getEntry(file));
        assertEquals(0, ifm.getBlockSize(file));
        //1 block
        vf.addInterval(new Interval(0,10));
        ifm.addEntry(file,vf);
        assertEquals(11, ifm.getBlockSize(file));//full inclusive now
        iter=ifm.getEntry(file).getBlocks().iterator();
        assertEquals(new Interval(0, 10), iter.next());
        assertTrue(! iter.hasNext());
        
        SharingSettings.INCOMPLETE_PURGE_TIME.setValue(26);
        File young=new FakeTimedFile(25);
        File old=new FakeTimedFile(27);
        assertTrue(!isOld(young));
        assertTrue(isOld(old));
    }

    public void testTempName() throws Throwable {
        assertEquals("T-748-abc def",
                     tempName("abc def", 748, 0));
        assertEquals("T-748-abc def (2)",
                     tempName("abc def", 748, 2));
        assertEquals("T-748-abc.txt",
                     tempName("abc.txt", 748, 1));
        assertEquals("T-748-abc (2).txt",
                     tempName("abc.txt", 748, 2));
        assertEquals("T-748-.txt",
                     tempName(".txt", 748, 1));
        assertEquals("T-748- (2).txt",
                     tempName(".txt", 748, 2));
    }
    

    /** Different name or size ==> different temp file */
    public void testGetFile_DifferentSize() throws Throwable {
        rfd1=newRFD("some file name", 1839, null);
        rfd2=newRFD("some file name", 1223, null);
        assertTrue(! IncompleteFileManager.same(rfd1, rfd2));
        File tmp1=ifm.getFile(rfd1);
        File tmp2=ifm.getFile(rfd2);
        assertNotEquals(tmp2, tmp1);
    }

    /** 
     * You should be able to resume to a file with same hash but different name.
     */
    public void testGetFile_DifferentNameSameHash() throws Throwable {
        rfd1=newRFD("some file name", 1839, 
                    "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        rfd2=newRFD("another file name", 1839, 
                    "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        assertTrue(IncompleteFileManager.same(rfd1, rfd2));
        File tmp1=ifm.getFile(rfd1);
        File tmp2=ifm.getFile(rfd2);
        assertEquals(tmp1, tmp2);
    }

    /** 
     * You should NOT be able to resume to file w/ same name but different hash.
     */
    public void testGetFile_SameNameDifferentHash() throws Throwable {
        rfd1=newRFD("some file name", 1839, 
                    "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        rfd2=newRFD("some file name", 1839, 
                    "urn:sha1:LSTHIPQGSGSZTS5FJPAKPZWUGYQYPFBU");
        assertTrue(! IncompleteFileManager.same(rfd1, rfd2));
        File tmp1=ifm.getFile(rfd1);
        File tmp2=ifm.getFile(rfd2);
        assertNotEquals(tmp2, tmp1);
    }

    /** Risky resumes are allowed: no hashes */
    public void testGetFile_NoHash() throws Throwable {
        rfd1=newRFD("some file name", 1839, null);
        rfd2=newRFD("some file name", 1839, 
                    "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        assertTrue(IncompleteFileManager.same(rfd1, rfd2));
        File tmp1=ifm.getFile(rfd1);
        File tmp2=ifm.getFile(rfd2);
        assertEquals(tmp1, tmp2);
    }

    /** Checks that removeEntry clears blocks AND hashes. */
    public void testRemoveEntry() throws Throwable {       
        //Populate IFM with a hash.
        rfd1=newRFD("some file name", 1839, 
                    "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB"); 
        File tmp1=ifm.getFile(rfd1);
        VerifyingFile vf=new VerifyingFile(1839);
        ifm.addEntry(tmp1, vf);

        //After deleting entry there should be no more blocks...
        ifm.removeEntry(tmp1);      
        assertNull(ifm.getEntry(tmp1));

        //...and same temp file should be used for different hash. [sic]
        rfd2=newRFD("some file name", 1839, 
                    "urn:sha1:LSTHIPQGSGSZTS5FJPAKPZWUGYQYPFBU");
        File tmp2=ifm.getFile(rfd2);
        assertEquals(tmp1, tmp2);
        assertEquals(tmp2, ifm.getFile(newRFD("some file name", 1839, null)));
    }
    
    /**
     * Checks that addEntry & removeEntry notify the FileManager of the
     * added / removed incomplete file.
     */
    public void testFileManagerIsNotified() throws Exception {
        assertEquals(0, fm.getNumIncompleteFiles()); // begin with 0 shared.
        
        //Populate IFM with a hash.
        rfd1=newRFD("some file name", 1839, 
                    "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        File tmp1=ifm.getFile(rfd1);
        VerifyingFile vf=new VerifyingFile(1839);
        ifm.addEntry(tmp1, vf);
        
        assertEquals(1, fm.getNumIncompleteFiles()); // 1 added.
        
        // make sure it's associated with a URN.
        URN urn = URN.createSHA1Urn(    
            "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        FileDesc fd = fm.getFileDescForUrn(urn);
        assertNotNull(urn);
        assertInstanceof(IncompleteFileDesc.class, fd);
        
        ifm.removeEntry(tmp1);
        
        assertEquals(0, fm.getNumIncompleteFiles()); // back to 0 shared.
    }   

    public void testCompletedHash_NotFound() throws Throwable{
        File tmp2=new File("T-1839-some file name");
        assertNull(ifm.getCompletedHash(tmp2));
    }

    public void testCompletedHash_Found() throws Throwable {
        rfd1=newRFD("some file name", 1839, 
                    "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        File tmp1=ifm.getFile(rfd1);
        try {
            URN urn=URN.createSHA1Urn( 
                "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
            assertEquals(urn, ifm.getCompletedHash(tmp1));
        } catch (IOException e) {
            fail("Couldn't make URN", e);
        }
    }

    public void testCompletedName() throws Throwable {
        File tmp1=new File("T-1839-some file.txt");
        assertEquals("some file.txt", IncompleteFileManager.getCompletedName(tmp1));
    }

    public void testCompletedName_IllegalArgument() throws Throwable {
        try {
            IncompleteFileManager.getCompletedName(new File("no dash.txt"));
            fail("Accepted bad file");
        } catch (IllegalArgumentException pass) { }

        try {
            IncompleteFileManager.getCompletedName(new File("T-one dash.txt"));
            fail("Accepted bad file");
        } catch (IllegalArgumentException pass) { }

        try {
            IncompleteFileManager.getCompletedName(new File("T-123-"));
            fail("Accepted bad file");
        } catch (IllegalArgumentException pass) { }
    }

    public void testCompletedSize() throws Throwable {
        File tmp1=new File("T-1839-some file.txt");
        assertEquals(1839, IncompleteFileManager.getCompletedSize(tmp1));
    }

    public void testCompletedSize_IllegalArgument() throws Throwable {
        try {
            IncompleteFileManager.getCompletedSize(new File("no dash.txt"));
            fail("Accepted bad file");
        } catch (IllegalArgumentException pass) { }

        try {
            IncompleteFileManager.getCompletedSize(new File("T-one dash.txt"));
            fail("Accepted bad file");
        } catch (IllegalArgumentException pass) { }

        try {
            IncompleteFileManager.getCompletedSize(new File("T--no number.txt"));
            fail("Accepted bad file");
        } catch (IllegalArgumentException pass) { }

        try {
            IncompleteFileManager.getCompletedSize(new File("T-x-bad number.txt"));
            fail("Accepted bad file");
        } catch (IllegalArgumentException pass) { }
    }

    /** Tests that hash information is purged when calling purge(true). */
    public void testPurgeHash_Yes() throws Throwable {
        //These files have the same hash, but no blocks have been written.
        RemoteFileDesc rfd1=newRFD("file name.txt", 1839, 
                                   "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        RemoteFileDesc rfd1b=newRFD("other file.txt", 1839, 
                                   "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        File file1=ifm.getFile(rfd1);
        file1.delete(); // getFile will create it, we don't want it created.
        File file1b=ifm.getFile(rfd1b);
        file1b.delete();
        assertEquals(file1, file1b);

        //These files have the same hash, but blocks have been written to disk.
        RemoteFileDesc rfd2=newRFD("another name.txt", 1839, 
                                   "urn:sha1:LSTHIPQGSSZGTS5FJUPAKPZWUGYQYPFB");
        RemoteFileDesc rfd2b=newRFD("yet another file.txt", 1839, 
                                   "urn:sha1:LSTHIPQGSSZGTS5FJUPAKPZWUGYQYPFB");
        File file2=ifm.getFile(rfd2);
        file2.delete();
        File file2b=ifm.getFile(rfd2b);
        file2b.delete();
        assertEquals(file2, file2b);
        try {
            file2.createNewFile();
        } catch (IOException e) {
            fail("Couldn't create "+file2, e);
        }

        //After purging, only hashes associated with files that exists remain.
        ifm.initialPurge(Collections.EMPTY_LIST);
        File file1c=ifm.getFile(rfd1b);
        assertNotEquals(file1b, file1c);
        File file2c=ifm.getFile(rfd2b);
        assertEquals(file2b ,file2c);
        file2.delete();
    }

    /** Tests that hash information is not purged when calling purge(false). */
    public void testPurgeHash_No() throws Throwable {
        RemoteFileDesc rfd1=newRFD("file name.txt", 1839, 
                                   "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        RemoteFileDesc rfd2=newRFD("other file.txt", 1839, 
                                   "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
        File file1=ifm.getFile(rfd1);
        File file2=ifm.getFile(rfd2);
        assertEquals(file1, file2);
        ifm.purge();             //Does nothing
        File file2b=ifm.getFile(rfd2);
        assertEquals(file2, file2b);
    }

    /** Serializes, then deserializes. */
    public void testSerialize() throws Exception {
        File tmp=null;
        try {
            //Create an IFM with one entry, with hash.
            IncompleteFileManager ifm1=new IncompleteFileManager();
            RemoteFileDesc rfd=newRFD("file name.txt", 1839, 
                "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB");
            File incomplete=ifm1.getFile(rfd);
            VerifyingFile vf=new VerifyingFile(1839);
            vf.addInterval(new Interval(10, 100));  //inclusive
            ifm1.addEntry(incomplete, vf);

            //Write to disk.
            tmp=File.createTempFile("IncompleteFileManagerTest", ".dat");
            ObjectOutputStream out=new ObjectOutputStream(
                                       new FileOutputStream(tmp));
            out.writeObject(ifm1);
            out.close();
            ifm1=null;
            
            //Read IFM from disk.
            ObjectInputStream in=new ObjectInputStream(
                                       new FileInputStream(tmp));
            IncompleteFileManager ifm2=(IncompleteFileManager)in.readObject();
            in.close();
            
            //Make sure it's the same.
            File incomp = null;
            File inDir = null;
            inDir = SharingSettings.INCOMPLETE_DIRECTORY.getValue();
            if( inDir == null ||
               !inDir.isDirectory() ||
               !inDir.exists() ||
               !inDir.canWrite() ) {
                fail("unable to set up test-cannot find incomplete directory");
            }
            incomp =  new File(inDir, "T-1839-file name.txt");
            VerifyingFile vf2=(VerifyingFile)ifm2.getEntry(incomp);
            assertTrue(vf2!=null);
            Iterator /* of Interval */ iter=vf2.getBlocks().iterator();
            assertTrue(iter.hasNext());
            assertEquals(new Interval(10, 100), iter.next());
            assertTrue(!iter.hasNext());
            assertEquals(new File(inDir, "T-1839-file name.txt"),
                ifm2.getFile(newRFD("different name.txt", 1839, 
                                    "urn:sha1:GLSTHIPQGSSZTS5FJUPAKPZWUGYQYPFB")));
        } finally {
            if (tmp!=null)
                tmp.delete();
        }
    }

	private static String tempName(String s, int one, int two) throws Throwable {
	    try {
            return (String)PrivilegedAccessor.invokeMethod(
                IncompleteFileManager.class, "tempName", 
                new Object[] {s, new Integer(one), new Integer(two)},
                new Class[] {String.class, Integer.TYPE, Integer.TYPE});
        } catch(Exception e) {
            if ( e.getCause() != null ) 
                throw e.getCause();
            else throw e;
        }
    }
    
	private static boolean isOld(File f) throws Throwable {
	    try {
            return ((Boolean)PrivilegedAccessor.invokeMethod(
                IncompleteFileManager.class, "isOld", 
                new Object[] {f},
                new Class[] {File.class} )).booleanValue();
        } catch(Exception e) {
            if ( e.getCause() != null ) 
                throw e.getCause();
            else throw e;
        }
    }    
    
    static class FakeTimedFile extends File {
        private long days;
        FakeTimedFile(int days) {
            super("whatever.txt");
            this.days=days;
        }

        public long lastModified() {
            //30 days ago
            return System.currentTimeMillis()-days*24l*60l*60l*1000l;
        }
    }       
}
