package org.jboss.installer.core;

import org.apache.commons.lang3.RandomStringUtils;
import org.jboss.installer.dialogs.downloader.Download;
import org.jboss.installer.dialogs.downloader.DownloadHandler;
import org.jboss.installer.dialogs.downloader.DownloadManager;
import org.jboss.installer.validators.PathValidator;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

import java.net.URL;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@RunWith(MockitoJUnitRunner.class)
public class DownloadUtilsTest {

    @Mock
    private DownloadManager downloadManager;
    @Mock
    private DownloadUtils.DownloadUIFactory downloaderUIFactory;
    @Mock
    private DownloadHandler downloadHandler;
    @Mock
    private Consumer<Download> errorUI;
    @Captor
    private ArgumentCaptor<List<Download>> downloads;

    private DownloadUtils downloadUtils;

    @Before
    public void setUp() throws Exception {
        this.downloadUtils = new DownloadUtils(downloadManager, downloaderUIFactory, errorUI);
    }

    @Test
    public void emptyListProducesEmptyList() throws Exception {
        assertEquals(Collections.emptyList(), downloadUtils.downloadAll(Collections.emptyList()));
    }

    @Test
    public void nullStringProducesNullInList() throws Exception {
        final ArrayList<String> files = new ArrayList<>();
        files.add(null);
        assertEquals(null, downloadUtils.downloadAll(files).get(0));
    }

    @Test
    public void emptyStringProducesEmptyStringInList() throws Exception {
        assertThat(downloadUtils.downloadAll(List.of("")))
                .contains("");
    }

    @Test
    public void urlIsDownloadedAndResultIsAddedToTheList() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(true);
        when(downloaderUIFactory.prepareUI(downloads.capture(), eq(downloadManager))).thenReturn(mockUI(true));
        assertThat(downloadUtils.downloadAll(List.of("http://foo.bar/test.jar")))
                .noneMatch(s->s.startsWith("http"))
                .map(l-> Path.of(l))
                .map(p->p.getFileName().toString())
                .contains("test.jar");

        assertThat(downloads.getValue())
                .map(Download::getUrl)
                .contains(new URL("http://foo.bar/test.jar"));
        verify(downloadManager).start(downloads.getValue(), downloadHandler);
    }

    private DownloadUtils.DownloadUI mockUI(boolean res) {
        return new DownloadUtils.DownloadUI() {
            @Override
            public boolean display() {
                return res;
            }

            @Override
            public DownloadHandler getDownloadHandler() {
                return downloadHandler;
            }
        };
    }

    @Test
    public void errorIsDisplayedIfValidationFails() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(false);

        assertNull(downloadUtils.downloadAll(List.of("http://foo.bar/test.jar")));

        verify(errorUI).accept(any(Download.class));
    }

    @Test
    public void nullIsReturnedIfDownloadIsCanceled() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(true);
        when(downloaderUIFactory.prepareUI(downloads.capture(), eq(downloadManager))).thenReturn(mockUI(false));

        assertNull(downloadUtils.downloadAll(List.of("http://foo.bar/test.jar")));
    }

    @Test
    public void duplicatedFileNamesResultInDistinctNames() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(true);
        when(downloaderUIFactory.prepareUI(downloads.capture(), eq(downloadManager))).thenReturn(mockUI(true));

        final List<String> resolved = downloadUtils.downloadAll(List.of("http://foo.bar/test.jar", "http://foo.bar2/test.jar"));
        assertEquals("test.jar", Path.of(resolved.get(0)).getFileName().toString());
        assertThat(Path.of(resolved.get(1)).getFileName().toString())
                .matches("test\\-.*\\.jar");

        assertThat(downloads.getValue())
                .map(Download::getUrl)
                .contains(new URL("http://foo.bar/test.jar"),new URL("http://foo.bar2/test.jar"));
        verify(downloadManager).start(downloads.getValue(), downloadHandler);
    }

    @Test
    public void duplicatedFileNamesWithoutSuffixResultInDistinctNames() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(true);
        when(downloaderUIFactory.prepareUI(downloads.capture(), eq(downloadManager))).thenReturn(mockUI(true));

        final List<String> resolved = downloadUtils.downloadAll(List.of("http://foo.bar/test", "http://foo.bar2/test"));
        assertEquals("test", Path.of(resolved.get(0)).getFileName().toString());
        assertThat(Path.of(resolved.get(1)).getFileName().toString())
                .matches("test\\-.*");

        assertThat(downloads.getValue())
                .map(Download::getUrl)
                .contains(new URL("http://foo.bar/test"),new URL("http://foo.bar2/test"));
        verify(downloadManager).start(downloads.getValue(), downloadHandler);
    }

    @Test
    public void duplicatedFileNamesInTwoCallsResultInDistinctNames() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(true);
        when(downloaderUIFactory.prepareUI(downloads.capture(), eq(downloadManager))).thenReturn(mockUI(true));

        final List<String> resolved = downloadUtils.downloadAll(List.of("http://foo.bar/test"));
        assertEquals("test", Path.of(resolved.get(0)).getFileName().toString());
        final List<String> resolved2 = downloadUtils.downloadAll(List.of("http://foo.bar2/test"));
        assertNotEquals(Path.of(resolved.get(0)), Path.of(resolved2.get(0)));

        verify(downloadManager, times(2)).start(any(), eq(downloadHandler));
    }

    @Test
    public void tooLongFileNameInUrlIsTruncatedInTheResultingFile() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(true);
        when(downloaderUIFactory.prepareUI(downloads.capture(), eq(downloadManager))).thenReturn(mockUI(true));

        final String path = RandomStringUtils.randomAlphabetic(300);
        final String url = "http://foo.bar/" + path;
        final List<String> resolved = downloadUtils.downloadAll(List.of(url));
        final String resolvedFilename = Path.of(resolved.get(0)).getFileName().toString();
        assertTrue(String.format("Expected generated filename %s to be substring of %s", resolvedFilename, path),
                path.startsWith(resolvedFilename));
        assertTrue(String.format("Expected resolvedFilename length to be less then limit (%d), but was %d",
                        PathValidator.FILENAME_LENGTH_LIMIT, resolvedFilename.length()),
                resolvedFilename.length() < PathValidator.FILENAME_LENGTH_LIMIT);


        assertThat(downloads.getValue())
                .map(Download::getUrl)
                .contains(new URL(url));
        verify(downloadManager).start(downloads.getValue(), downloadHandler);
    }

    @Test
    public void dontDownloadTheSameUrlTwice() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(true);
        when(downloaderUIFactory.prepareUI(downloads.capture(), eq(downloadManager))).thenReturn(mockUI(true));

        final List<String> resolved = downloadUtils.downloadAll(List.of("http://foo.bar/test", "http://foo.bar/test"));
        // verify returns the same file
        assertEquals("test", Path.of(resolved.get(0)).getFileName().toString());
        assertEquals("test", Path.of(resolved.get(1)).getFileName().toString());
        assertEquals(resolved.get(0), resolved.get(1));

        assertThat(downloads.getValue())
                .map(Download::getUrl)
                .contains(new URL("http://foo.bar/test"));
        verify(downloadManager).start(downloads.getValue(), downloadHandler);
    }

    @Test
    public void dontDownloadAlreadyDownloadedUrl() throws Exception {
        when(downloadManager.verify(any(Download.class))).thenReturn(true);
        when(downloaderUIFactory.prepareUI(any(), eq(downloadManager))).thenReturn(mockUI(true));

        final List<String> resolved = downloadUtils.downloadAll(List.of("http://foo.bar/test"));
        assertEquals("test", Path.of(resolved.get(0)).getFileName().toString());

        final List<String> resolved2 = downloadUtils.downloadAll(List.of("http://foo.bar/test"));
        assertEquals("test", Path.of(resolved2.get(0)).getFileName().toString());
        assertEquals(resolved.get(0), resolved2.get(0));

        verify(downloadManager, Mockito.times(1)).start(any(), eq(downloadHandler));
    }

}