package com.atlassian.stash.internal.web.markup;

import com.atlassian.markup.renderer.RenderContext;
import com.atlassian.markup.renderer.RenderTransform;
import com.atlassian.markup.renderer.Replacement;
import com.atlassian.bitbucket.content.Path;
import com.atlassian.bitbucket.hamcrest.SimpleTypeSafeMatcher;
import com.atlassian.bitbucket.i18n.I18nService;
import com.atlassian.bitbucket.i18n.SimpleI18nService;
import com.atlassian.bitbucket.idx.CommitIndex;
import com.atlassian.bitbucket.idx.IndexSearchRequest;
import com.atlassian.bitbucket.idx.IndexedCommit;
import com.atlassian.stash.internal.project.InternalNormalProject;
import com.atlassian.stash.internal.project.InternalProject;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.repository.Repository;
import com.atlassian.bitbucket.transaction.SimpleSpringTransactionTemplate;
import com.atlassian.bitbucket.util.PageRequest;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.transaction.support.TransactionTemplate;

import javax.annotation.Nonnull;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import static com.atlassian.bitbucket.mockito.PageAnswer.withEmptyPageOf;
import static com.atlassian.bitbucket.mockito.PageAnswer.withLastPageOf;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

@RunWith(Parameterized.class)
public class CommitMarkupRendererTest {

    private final I18nService i18nService = new SimpleI18nService(SimpleI18nService.Mode.RETURN_KEYS_WITH_ARGUMENTS);
    private final TransactionTemplate transactionTemplate = new SimpleSpringTransactionTemplate();

    private CommitMarkupRenderer renderer;

    @Parameterized.Parameters(name = "markup = \"{0}\"")
    public static Collection<Object[]> testCases() {
        return Arrays.asList(new Object[][]{
                {"lorem 123abcd ipsum 456ef78 111aaad", new String[]{"456ef78"}},
                {"lorem 123abcd ipsum 456ef78 789fedc", new String[]{"456ef78", "789fedc"}},
                {"loremx123abcd ipsumx456ef78 789fedc", new String[]{"789fedc"}},
                {"loremx123abcd ipsumx456ef78", new String[]{}},
                {"lorem 456ef78", new String[]{"456ef78"}},
                {"456ef78 lorem", new String[]{"456ef78"}},
                {"456Ef78 789FEDC", new String[]{"456ef78", "789fedc"}},
                {"456ef78456ef78456ef78456ef78456ef78456ef 456ef78", new String[]{"456ef78456ef78456ef78456ef78456ef78456ef", "456ef78"}},
                {"https://localhost/projects/FOO/repos/foo/commits/456ef784", new String[]{}},
                {"https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa", new String[]{}},
                {"https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa lorem", new String[]{}},
                {"https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa 789fedc", new String[]{"789fedc"}},
                {"[a link](https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa)", new String[]{}},
                {"[a link](https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa) lorem", new String[]{}},
                {"[a link](https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa) 789fedc", new String[]{"789fedc"}},
                {"[456ef784](https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa)", new String[]{}},
                {"[456ef784](https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa) lorem", new String[]{}},
                {"[456ef784](https://localhost/projects/FOO/repos/foo/commits/456ef78456ef78456ef78456ef78456ef78456efaaa) 789fedc", new String[]{"789fedc"}},
                {"456ef78456ef78456ef78456ef78456ef78456efaaa 456ef78", new String[]{"456ef78"}},
                {"456ef7 456", new String[]{}},
                {" ", new String[]{}},
                {"", new String[]{}},
        });
    }

    private final String markup;
    private final String[] commitIds;

    public CommitMarkupRendererTest(String markup, String[] commitIds) {
        this.markup = markup;
        this.commitIds = commitIds;
    }

    @Before
    public void setup() throws Exception {
        InternalProject project1 = new InternalNormalProject.Builder()
                .id(1)
                .key("SCRIPT")
                .name("<script>alert(1);</script>")
                .build();

        InternalRepository repository1 = new InternalRepository.Builder().id(1)
                .name("repo").hierarchyId("42").project(project1).build();
        InternalRepository repository2 = new InternalRepository.Builder().id(2)
                .name("repo").hierarchyId("42").project(project1).build();

        IndexedCommit commit123abcd = mock(IndexedCommit.class);
        when(commit123abcd.getRepositories()).thenReturn(ImmutableSet.<Repository>of());

        IndexedCommit commit456ef78 = mock(IndexedCommit.class);
        when(commit456ef78.getRepositories()).thenReturn(ImmutableSet.<Repository>of(repository1));

        IndexedCommit commit789fedc = mock(IndexedCommit.class);
        when(commit789fedc.getRepositories()).thenReturn(ImmutableSet.<Repository>of(repository1, repository2));

        NavBuilder navBuilder = mock(NavBuilder.class, RETURNS_DEEP_STUBS);
        when(navBuilder.repo(notNull(Repository.class)).commit(notNull(String.class))).thenAnswer(new CommitAnswer("https://example.com", "/stash", "/commits"));

        CommitIndex commitIndex = mock(CommitIndex.class);
        doAnswer(withEmptyPageOf(IndexedCommit.class)).when(commitIndex).search(any(IndexSearchRequest.class), any(PageRequest.class));
        doAnswer(withLastPageOf(commit456ef78)).when(commitIndex).search(searchRequest("456ef78"), any(PageRequest.class));
        doAnswer(withLastPageOf(commit789fedc)).when(commitIndex).search(searchRequest("789fedc"), any(PageRequest.class));

        renderer = new CommitMarkupRenderer(commitIndex, i18nService, navBuilder, transactionTemplate);
        new DirectFieldAccessor(renderer).setPropertyValue("lookupTimeout", 5000);
    }

    @Test
    public void testRender() throws Exception {
        RenderTransform transform = renderer.process(markup, new RenderContext("https://example.com/stash", ImmutableMap.<String, Object>of()));

        List<Replacement> replacements = MarkupTestUtils.sort(transform.getReplacements());
        assertThat("mismatch on: " + markup, replacements, hasSize(commitIds.length));
        for (int i = 0; i < commitIds.length; i++) {
            Replacement replacement = replacements.get(i);
            assertThat(replacement.getValue().toString(), allOf(
                    startsWith("<a"),
                    containsString("\"https://example.com/stash/commits/" + commitIds[i] + "\""),
                    containsString(">" + commitIds[i] + "<"),
                    containsString("class=\"commitid\""),
                    not(containsString("<script>alert(1);</script>")),
                    containsString("&lt;script&gt;alert(1);&lt;/script&gt;"),
                    endsWith("</a>")));
        }
    }

    private static IndexSearchRequest searchRequest(final String commitid) {
        return argThat(new SimpleTypeSafeMatcher<IndexSearchRequest>() {

            @Override
            protected boolean matchesSafely(IndexSearchRequest request) {
                return StringUtils.startsWith(request.getFilter(), commitid);
            }
        });
    }

    private static class CommitAnswer implements Answer<NavBuilder.Commit> {

        private final String baseUrl;
        private final String contextPath;
        private final String relativePath;

        private CommitAnswer(String baseUrl, String contextPath, String relativePath) {
            this.baseUrl = baseUrl;
            this.contextPath = contextPath;
            this.relativePath = relativePath;
        }

        @Override
        public NavBuilder.Commit answer(final InvocationOnMock invocation) {
            return new NavBuilder.Commit() {

                @Override
                public String buildAbsolute() {
                    return baseUrl + buildRelative();
                }

                @Override
                public String buildRelative() {
                    return contextPath + buildRelNoContext();
                }

                @Override
                public String buildRelNoContext() {
                    return relativePath + '/' + invocation.getArguments()[0];
                }

                @Override
                public String buildConfigured() {
                    throw new UnsupportedOperationException();
                }

                @Nonnull
                @Override
                public NavBuilder.Commit change(@Nonnull Path path) {
                    throw new UnsupportedOperationException();
                }

                @Nonnull
                @Override
                public NavBuilder.Commit change(@Nonnull String path) {
                    throw new UnsupportedOperationException();
                }

                @Nonnull
                @Override
                public NavBuilder.CommitComment comment(long commentId) {
                    throw new UnsupportedOperationException();
                }

                @Override
                public NavBuilder.PathBuilder<NavBuilder.Commit> path(@Nonnull String... path) {
                    throw new UnsupportedOperationException();
                }

                @Override
                public NavBuilder.PathBuilder<NavBuilder.Commit> path(@Nonnull Path path) {
                    throw new UnsupportedOperationException();
                }

                @Override
                public NavBuilder.Commit withParam(@Nonnull String name, String value) {
                    throw new UnsupportedOperationException();
                }

                @Nonnull
                @Override
                public NavBuilder.Commit unwatch() {
                    throw new UnsupportedOperationException();
                }
            };
        }
    }
}
