/*
 * © 2017 AgNO3 Gmbh & Co. KG
 * 
 * This library 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 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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 library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package jcifs.tests;


import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.junit.Assert;
import org.junit.Assume;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jcifs.CIFSContext;
import jcifs.CIFSException;
import jcifs.CloseableIterator;
import jcifs.ResolverType;
import jcifs.SmbConstants;
import jcifs.SmbResource;
import jcifs.SmbTreeHandle;
import jcifs.config.DelegatingConfiguration;
import jcifs.context.CIFSContextWrapper;
import jcifs.netbios.NameServiceClientImpl;
import jcifs.smb.DosFileFilter;
import jcifs.smb.SmbException;
import jcifs.smb.SmbFile;
import jcifs.smb.SmbFilenameFilter;
import jcifs.smb.SmbUnsupportedOperationException;


/**
 * @author mbechler
 *
 */
@RunWith ( Parameterized.class )
@SuppressWarnings ( "javadoc" )
public class EnumTest extends BaseCIFSTest {

    private static final Logger log = LoggerFactory.getLogger(EnumTest.class);


    /**
     * @param name
     * @param properties
     */
    public EnumTest ( String name, Map<String, String> properties ) {
        super(name, properties);
    }


    @Parameters ( name = "{0}" )
    public static Collection<Object> configs () {
        return getConfigs("smb1", "noUnicode", "forceUnicode", "noNTStatus", "noNTSmbs", "smb2", "smb30", "smb31");
    }


    @Ignore ( "This causes a connection to whatever local master browser is available, config may be incompatible with it" )
    @Test
    public void testBrowse () throws MalformedURLException, CIFSException {
        CIFSContext ctx = withAnonymousCredentials();
        try ( SmbFile smbFile = new SmbFile("smb://", ctx) ) {
            try ( CloseableIterator<SmbResource> it = smbFile.children() ) {
                while ( it.hasNext() ) {
                    try ( SmbResource serv = it.next() ) {
                        System.err.println(serv.getName());
                    }
                }
            }
        }
        catch ( SmbUnsupportedOperationException e ) {
            Assume.assumeTrue("Browsing unsupported", false);
        }
    }


    @Test
    public void testBrowseDomain () throws MalformedURLException, CIFSException {
        CIFSContext ctx = withAnonymousCredentials();
        try ( SmbFile smbFile = new SmbFile("smb://" + getRequiredProperty(TestProperties.TEST_DOMAIN_SHORT), ctx) ) {
            // if domain is resolved through DNS this will be treated as a server and will enumerate shares instead
            Assume.assumeTrue("Not workgroup", SmbConstants.TYPE_WORKGROUP == smbFile.getType());
            try ( CloseableIterator<SmbResource> it = smbFile.children() ) {
                while ( it.hasNext() ) {
                    try ( SmbResource serv = it.next() ) {
                        System.err.println(serv.getName());
                        assertEquals(SmbConstants.TYPE_SERVER, serv.getType());
                        assertTrue(serv.isDirectory());
                    }
                }
            }
        }
        catch ( SmbUnsupportedOperationException e ) {
            Assume.assumeTrue("Browsing unsupported", false);
        }
    }


    @Test
    public void testBrowseDomainNetbios () throws MalformedURLException, CIFSException {

        // only do this if a WINS server is enabled
        getRequiredProperty("jcifs.netbios.wins");

        CIFSContext bctx = withAnonymousCredentials();

        // ensure that the domain name gets resolved through WINS so that
        // it gets the workgroup type.
        CIFSContext ctx = withConfig(bctx, new DelegatingConfiguration(bctx.getConfig()) {

            @Override
            public List<ResolverType> getResolveOrder () {
                return Arrays.asList(ResolverType.RESOLVER_WINS);
            }
        });

        // need to override NameServiceClient as it otherwise gets initialized with the original config
        final NameServiceClientImpl nsc = new NameServiceClientImpl(ctx);
        ctx = new CIFSContextWrapper(ctx) {

            @Override
            public jcifs.NameServiceClient getNameServiceClient () {
                return nsc;
            }
        };

        try ( SmbFile smbFile = new SmbFile("smb://" + getRequiredProperty(TestProperties.TEST_DOMAIN_SHORT), ctx) ) {
            // if domain is resolved through DNS this will be treated as a server and will enumerate shares instead
            Assume.assumeTrue("Not workgroup", SmbConstants.TYPE_WORKGROUP == smbFile.getType());
            try ( CloseableIterator<SmbResource> it = smbFile.children() ) {
                while ( it.hasNext() ) {
                    try ( SmbResource serv = it.next() ) {
                        System.err.println(serv.getName());
                        assertEquals(SmbConstants.TYPE_SERVER, serv.getType());
                        assertTrue(serv.isDirectory());
                    }
                }
            }
        }
        catch ( SmbUnsupportedOperationException e ) {
            Assume.assumeTrue("Browsing unsupported", false);
        }
    }


    @Test
    public void testShareEnum () throws MalformedURLException, CIFSException {
        try ( SmbFile smbFile = new SmbFile("smb://" + getTestServer(), withTestNTLMCredentials(getContext())) ) {
            String[] list = smbFile.list();
            assertNotNull(list);
            assertTrue("No share found", list.length > 0);
            log.debug(Arrays.toString(list));
        }
    }


    @Test
    public void testDomainSeverEnum () throws MalformedURLException, CIFSException {
        try ( SmbFile smbFile = new SmbFile("smb://" + getTestDomain(), withTestNTLMCredentials(getContext())) ) {
            String[] list = smbFile.list();
            assertNotNull(list);
            log.debug(Arrays.toString(list));
        }
        catch ( SmbUnsupportedOperationException e ) {
            Assume.assumeTrue(false);
        }
    }


    @Test
    public void testDFSShareEnum () throws CIFSException, MalformedURLException {
        String dfsRoot = getDFSRootURL();
        try ( SmbFile smbFile = new SmbFile(dfsRoot, withTestNTLMCredentials(getContext())) ) {
            String[] list = smbFile.list();
            assertNotNull(list);
            assertTrue("No share found", list.length > 0);
            log.debug(Arrays.toString(list));

            String shareUrl = getTestShareURL();
            String link = shareUrl.substring(dfsRoot.length());

            Set<String> listLinks = new HashSet<>(Arrays.asList(list));
            int firstSep = link.indexOf('/');
            if ( firstSep == link.length() - 1 ) {
                // single level
                assertTrue("Link not found " + link, listLinks.contains(link));
            }
            else {
                link = link.substring(0, firstSep + 1);
                // single level
                assertTrue("First component of link not found" + link, listLinks.contains(link));
            }
        }
    }


    @Test
    public void testDirEnum () throws CIFSException, MalformedURLException, UnknownHostException {
        try ( SmbFile f = createTestDirectory() ) {
            try ( SmbFile a = new SmbFile(f, "a");
                  SmbFile b = new SmbFile(f, "b");
                  SmbFile c = new SmbFile(f, "c") ) {

                a.createNewFile();
                b.createNewFile();
                c.createNewFile();

                String[] names = f.list();
                assertNotNull(names);
                assertEquals(3, names.length);
                Arrays.sort(names);
                Assert.assertArrayEquals(new String[] {
                    "a", "b", "c"
                }, names);

                SmbFile[] files = f.listFiles();
                assertNotNull(files);
                assertEquals(3, files.length);

                for ( SmbFile cf : files ) {
                    assertTrue(cf.exists());
                    cf.close();
                }
            }
            finally {
                f.delete();
            }
        }
    }


    @Test
    public void testEnumDeepUnresolved () throws IOException {
        try ( SmbFile r = getDefaultShareRoot();
              SmbFile f = new SmbFile(r, "enum-test/a/b/") ) {

            try ( SmbResource c = f.resolve("c/") ) {
                if ( !c.exists() ) {
                    c.mkdirs();
                }
            }

            Set<String> names = new HashSet<>();
            try ( CloseableIterator<SmbResource> chld = f.children() ) {
                while ( chld.hasNext() ) {

                    try ( SmbResource next = chld.next() ) {
                        try ( CloseableIterator<SmbResource> children = next.children() ) {}
                        names.add(next.getName());
                    }
                }
            }

            assertTrue("Test directory  enum-test/a/b/c/ not found", names.contains("c/"));
        }

        try ( SmbFile r = getDefaultShareRoot();
              SmbFile f = new SmbFile(r, "enum-test/a/b/c/") ) {
            f.exists();
        }

    }


    @Test
    public void testEnumDeepVirgin () throws IOException {

        try ( SmbFile r = getDefaultShareRoot();
              SmbFile f = new SmbFile(r, "x-test/") ) {
            if ( !f.exists() ) {
                f.mkdirs();
            }
        }

        // fresh connection
        CIFSContext ctx = getNewContext();
        try ( SmbFile r = new SmbFile(getTestShareURL(), withTestNTLMCredentials(ctx));
              SmbFile f = new SmbFile(r, "x-test/") ) {

            Set<String> names = new HashSet<>();
            try ( CloseableIterator<SmbResource> chld = f.children() ) {
                while ( chld.hasNext() ) {

                    try ( SmbResource next = chld.next() ) {
                        try ( CloseableIterator<SmbResource> children = next.children() ) {}
                        names.add(next.getName());
                    }
                }
            }
        }

    }


    @Test
    public void testEnumDeepUnresolvedCasing () throws IOException {

        String testShareURL = getTestShareURL().toUpperCase(Locale.ROOT);

        try ( SmbFile r = new SmbFile(testShareURL, withTestNTLMCredentials(getContext()));
              SmbFile f = new SmbFile(r, "enum-test/a/b/") ) {

            try ( SmbResource c = f.resolve("c/") ) {
                if ( !c.exists() ) {
                    c.mkdirs();
                }
            }

            Set<String> names = new HashSet<>();
            try ( CloseableIterator<SmbResource> chld = f.children() ) {
                while ( chld.hasNext() ) {

                    try ( SmbResource next = chld.next() ) {
                        try ( CloseableIterator<SmbResource> children = next.children() ) {}
                        names.add(next.getName());
                    }
                }
            }

            assertTrue("Test directory  enum-test/a/b/c/ not found", names.contains("c/"));
        }

        try ( SmbFile r = getDefaultShareRoot();
              SmbFile f = new SmbFile(r, "enum-test/a/b/c/") ) {
            f.exists();
        }

    }


    @Test
    public void testDirFilenameFilterEnum () throws CIFSException, MalformedURLException, UnknownHostException {
        try ( SmbFile f = createTestDirectory() ) {
            try ( SmbFile a = new SmbFile(f, "a.txt");
                  SmbFile b = new SmbFile(f, "b.txt");
                  SmbFile c = new SmbFile(f, "c.bar") ) {

                a.createNewFile();
                b.createNewFile();
                c.createNewFile();

                String[] names = f.list(new SmbFilenameFilter() {

                    @Override
                    public boolean accept ( SmbFile dir, String name ) throws SmbException {
                        return name.endsWith(".txt");
                    }
                });
                assertNotNull(names);
                assertEquals(2, names.length);
                Arrays.sort(names);
                Assert.assertArrayEquals(new String[] {
                    "a.txt", "b.txt"
                }, names);

                SmbFile[] files = f.listFiles(new SmbFilenameFilter() {

                    @Override
                    public boolean accept ( SmbFile dir, String name ) throws SmbException {
                        return name.equals("c.bar");
                    }
                });
                assertNotNull(files);
                assertEquals(1, files.length);

                for ( SmbFile cf : files ) {
                    assertTrue(cf.exists());
                    cf.close();
                }
            }
            finally {
                f.delete();
            }
        }
    }


    @Test
    public void testDirDosFilterEnum () throws CIFSException, MalformedURLException, UnknownHostException {
        try ( SmbFile f = createTestDirectory() ) {
            try ( SmbFile a = new SmbFile(f, "a.txt");
                  SmbFile b = new SmbFile(f, "b.txt");
                  SmbFile c = new SmbFile(f, "c.bar") ) {

                a.createNewFile();
                b.createNewFile();
                c.createNewFile();

                SmbFile[] files = f.listFiles(new DosFileFilter("*.txt", -1));
                assertNotNull(files);
                assertEquals(2, files.length);
                for ( SmbFile cf : files ) {
                    assertTrue(cf.exists());
                    cf.close();
                }
            }
            finally {
                f.delete();
            }
        }
    }


    @Test
    public void testPatternEnum () throws CIFSException, MalformedURLException, UnknownHostException {
        try ( SmbFile f = createTestDirectory() ) {
            try ( SmbFile a = new SmbFile(f, "a.txt");
                  SmbFile b = new SmbFile(f, "b.txt");
                  SmbFile c = new SmbFile(f, "c.bar") ) {

                a.createNewFile();
                b.createNewFile();
                c.createNewFile();

                SmbFile[] files = f.listFiles("*.txt");
                assertNotNull(files);
                assertEquals(2, files.length);
                for ( SmbFile cf : files ) {
                    assertTrue(cf.exists());
                    cf.close();
                }

                int n = 0;
                try ( CloseableIterator<SmbResource> children = f.children("*.txt") ) {
                    while ( children.hasNext() ) {
                        try ( SmbResource r = children.next() ) {
                            assertTrue(r.exists());
                            n++;
                        }
                    }
                }
                assertEquals(2, n);
            }
            finally {
                f.delete();
            }
        }
    }


    @Test
    public void testAttributeEnum () throws CIFSException, MalformedURLException, UnknownHostException {

        try ( SmbFile f = createTestDirectory() ) {
            try ( SmbFile a = new SmbFile(f, "a/");
                  SmbFile b = new SmbFile(f, "b.txt");
                  SmbFile c = new SmbFile(f, "c.bar") ) {

                a.mkdir();

                b.createNewFile();
                boolean haveHidden = false;
                try {
                    b.setAttributes(SmbConstants.ATTR_HIDDEN);
                    haveHidden = true;
                }
                catch ( SmbUnsupportedOperationException e ) {}

                c.createNewFile();
                boolean haveArchive = false;
                try {
                    c.setAttributes(SmbConstants.ATTR_ARCHIVE);
                    haveArchive = true;
                }
                catch ( SmbUnsupportedOperationException e ) {}

                SmbFile[] dirs = f.listFiles(new DosFileFilter("*", SmbConstants.ATTR_DIRECTORY));
                assertNotNull(dirs);
                assertEquals(1, dirs.length);

                if ( haveHidden ) {
                    SmbFile[] hidden = f.listFiles(new DosFileFilter("*", SmbConstants.ATTR_HIDDEN));
                    assertNotNull(hidden);
                    assertEquals(1, hidden.length);
                }

                if ( haveArchive ) {
                    SmbFile[] archive = f.listFiles(new DosFileFilter("*", SmbConstants.ATTR_ARCHIVE));
                    assertNotNull(archive);
                    assertEquals(1, archive.length);
                }
            }
            finally {
                f.delete();
            }
        }
    }


    @Test
    public void testEmptyEnum () throws CIFSException, MalformedURLException, UnknownHostException {
        try ( SmbFile f = createTestDirectory() ) {
            try {
                SmbFile[] files = f.listFiles(new DosFileFilter("*.txt", 0));
                assertNotNull(files);
                assertEquals(0, files.length);

                files = f.listFiles();
                assertNotNull(files);
                assertEquals(0, files.length);
            }
            finally {
                f.delete();
            }
        }
    }


    @Test
    // BUG #15
    // this cannot be reproduced against samba, samba always subtracts
    // 8 bytes from the output buffer length, probably to mitigate
    // against this issue
    public void testEnumBufferSize () throws IOException {
        CIFSContext ctx = getContext();
        int origBufferSize = ctx.getConfig().getMaximumBufferSize();
        // odd buffer size that does match the alignment
        int tryBufferSize = 1023;
        final int bufSize[] = new int[] {
            origBufferSize
        };
        ctx = withConfig(ctx, new DelegatingConfiguration(ctx.getConfig()) {

            @Override
            public int getMaximumBufferSize () {
                return bufSize[ 0 ];
            }
        });
        ctx = withTestNTLMCredentials(ctx);
        try ( SmbResource root = ctx.get(getTestShareURL());
              SmbResource f = root.resolve(makeRandomDirectoryName()) ) {

            try ( SmbTreeHandle treeHandle = ( (SmbFile) root ).getTreeHandle() ) {
                Assume.assumeTrue("Not SMB2", treeHandle.isSMB2());
            }

            f.mkdir();
            try {

                for ( int i = 0; i < 5; i++ ) {
                    // each entry 94 byte + 2 * name length
                    // = 128 byte per entry
                    try ( SmbResource r = f.resolve(String.format("%04x%s", i, repeat('X', 13))) ) {
                        r.createNewFile();
                    }
                }
                // == 5*128 = 640

                // . and .. entries = 200 byte (includes alignment)

                // + 64 byte header
                // + 8 byte query response overhead
                // + 110 bytes entry
                // -> 1022 predicted message size <= 1023 maximum buffer size
                // 112 bytes to alignment
                // -> aligned to 1024 > 1023 maximum buffer size

                // 110 byte entry = 16 byte name = 8 char length
                try ( SmbResource r = f.resolve(repeat('Y', 8)) ) {
                    r.createNewFile();
                }

                bufSize[ 0 ] = tryBufferSize;

                try ( CloseableIterator<SmbResource> chld = f.children() ) {
                    while ( chld.hasNext() ) {
                        chld.next();
                    }
                }
                finally {
                    bufSize[ 0 ] = origBufferSize;
                }
            }
            finally {
                f.delete();
            }
        }
    }


    @Test
    // BUG #16
    public void testListCountRollover () throws IOException {
        testListCount(5, 4); // 4 + 2 (.,..) files
    }


    @Test
    // BUG #16
    public void testListCountExact () throws IOException {
        testListCount(5, 3); // 3 + 2 (.,..) files
    }


    @Test
    // BUG #16
    public void testListCountMoreThanTwo () throws IOException {
        testListCount(5, 10); // 10 + 2 (.,..) files
    }


    private void testListCount ( final int pageSize, int numFiles ) throws CIFSException {
        CIFSContext ctx = getContext();
        ctx = withConfig(ctx, new DelegatingConfiguration(ctx.getConfig()) {

            @Override
            public int getListCount () {
                return pageSize;
            }
        });
        ctx = withTestNTLMCredentials(ctx);
        try ( SmbResource root = ctx.get(getTestShareURL());
              SmbResource f = root.resolve(makeRandomDirectoryName()) ) {
            f.mkdir();
            try {
                for ( int i = 0; i < numFiles; i++ ) {
                    try ( SmbResource r = f.resolve(String.format("%04x", i)) ) {
                        r.createNewFile();
                    }
                }

                int cnt = 0;
                try ( CloseableIterator<SmbResource> chld = f.children() ) {
                    while ( chld.hasNext() ) {
                        try ( SmbResource next = chld.next() ) {
                            cnt++;
                        }
                    }
                }

                assertEquals(numFiles, cnt);
            }
            finally {
                f.delete();
            }
        }
    }


    @Test
    // BUG #61
    public void testListTrailingSlash () throws MalformedURLException, UnknownHostException, CIFSException {
        CIFSContext ctx = getContext();
        try ( SmbFile f = createTestDirectory() ) {
            try ( SmbFile a = new SmbFile(f, "a/");
                  SmbFile b = new SmbFile(a, "b.txt");
                  SmbFile c = new SmbFile(a, "c.txt") ) {

                a.mkdir();
                b.createNewFile();
                c.createNewFile();

                CIFSContext tc = withTestNTLMCredentials(ctx);

                String url = getTestShareURL() + f.getName() + "a/";

                try ( SmbFile f2 = new SmbFile(url, tc) ) {
                    f2.list();
                }
            }
            finally {
                f.delete();
            }
        }

    }


    private static String repeat ( char c, int n ) {
        char chs[] = new char[n];
        for ( int i = 0; i < n; i++ ) {
            chs[ i ] = c;
        }
        return new String(chs);
    }

}