package com.atlassian.stash.internal.rest.renderer;

import com.atlassian.bitbucket.rest.enrich.AvatarEnricher;
import com.atlassian.bitbucket.rest.enrich.LinkEnricher;
import com.atlassian.bitbucket.rest.enrich.MarkupEnricher;
import com.atlassian.bitbucket.avatar.AvatarRequest;
import com.atlassian.bitbucket.nav.NavBuilder;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.pull.PullRequestRef;
import com.atlassian.bitbucket.request.RequestContext;
import com.atlassian.bitbucket.request.RequestManager;
import com.atlassian.bitbucket.rest.annotation.JsonSurrogate;
import com.atlassian.bitbucket.rest.user.RestApplicationUser;
import com.atlassian.bitbucket.user.ApplicationUser;
import com.atlassian.bitbucket.user.Person;
import com.atlassian.bitbucket.user.UserType;
import com.google.common.collect.ImmutableMap;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;

import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class BaseSurrogateJsonRendererTest {

    @Mock
    private NavBuilder navBuilder;
    @Mock
    private RequestManager requestManager;
    @Mock
    private AvatarEnricher avatarEnricher;
    @Mock
    private MarkupEnricher markupEnricher;
    @Mock
    private LinkEnricher linkEnricher;

    private AggregateBuilder aggregateBuilder;

    @Before
    public void setUp() throws Exception {
        //Mocking would be preferable but using the concrete class is much much easier
        aggregateBuilder = new AggregateBuilder(navBuilder);
        aggregateBuilder.registerSurrogate(RestApplicationUser.class);
        aggregateBuilder.registerSurrogate(PullRequestSurrogate.class);
    }

    public BaseSurrogateJsonRenderer newTestRestJsonRenderer() {
        return new BaseSurrogateJsonRenderer(requestManager, aggregateBuilder, avatarEnricher, markupEnricher, linkEnricher) {};
    }

    @Test
    public void testRenderingNull() throws Exception {
        assertEquals(null, newTestRestJsonRenderer().render(null, noOptions()));
    }

    @Test
    public void testRenderingUnknownObject() throws Exception {
        assertEquals(null, newTestRestJsonRenderer().render(new Object(), noOptions()));
    }

    @Test
    public void testRenderingKnownObject() throws Exception {
        assertJsonEquals(userJson("foo"), newTestRestJsonRenderer().render(user("foo"), noOptions()));
    }

    @Test
    public void testEndTagEscaping() throws Exception {
        assertJsonEquals(userJson("<\\/script>", "<_script>"), newTestRestJsonRenderer().render(user("</script>"), noOptions()));
    }

    @Test
    public void testEndTagEscapingOff() throws Exception {
        assertJsonEquals(userJson("</script>", "<_script>"), newTestRestJsonRenderer().render(
                user("</script>"), ImmutableMap.of("noEndTagEscape", (Object) Boolean.TRUE)));
        assertJsonEquals(userJson("</script>", "<_script>"), newTestRestJsonRenderer().render(
                user("</script>"), ImmutableMap.of("noEndTagEscape", (Object) Boolean.TRUE.toString())));
    }

    @Test
    public void testAvatarsApplied() throws Exception {
        newTestRestJsonRenderer().render(user("foo"), noOptions());

        verify(avatarEnricher).enrich(any(Object.class), argThat(new BaseMatcher<AvatarRequest>() {
            @Override
            public boolean matches(Object o) {
                AvatarRequest request = (AvatarRequest) o;
                return request.isSecure() && request.getSize() == 64;
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("AvatarRequest should have default values");
            }
        }));
    }

    @Test
    public void testAvatarOptions() throws Exception {
        newTestRestJsonRenderer().render(
                user("foo"), ImmutableMap.of("avatarSize", (Object) 128, "avatarScheme", "https"));
        newTestRestJsonRenderer().render(
                user("foo"), ImmutableMap.of("avatarSize", (Object) "128", "avatarScheme", "https"));

        verify(avatarEnricher, times(2)).enrich(any(Object.class), argThat(new BaseMatcher<AvatarRequest>() {
            @Override
            public boolean matches(Object o) {
                AvatarRequest request = (AvatarRequest) o;
                return request.isSecure() && request.getSize() == 128;
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("AvatarRequest should be secure and for an image of size 128");
            }
        }));
    }

    @Test
    public void testAvatarSchemeDefaultMatchesHttpRequest() throws Exception {
        RequestContext requestContext = mock(RequestContext.class);
        when(requestManager.getRequestContext()).thenReturn(requestContext);
        when(requestContext.isSecure()).thenReturn(true);

        newTestRestJsonRenderer().render(user("foo"), noOptions());

        verify(avatarEnricher).enrich(any(Person.class), argThat(new BaseMatcher<AvatarRequest>() {
            @Override
            public boolean matches(Object o) {
                return ((AvatarRequest) o).isSecure();
            }

            @Override
            public void describeTo(Description description) {
                description.appendText("AvatarRequest should be secure since current request is secure");
            }
        }));
    }

    @Test
    public void testMarkupRenderingIsApplied() throws Exception {
        PullRequest pullRequest = mock(PullRequest.class);
        when(pullRequest.getFromRef()).thenReturn(mock(PullRequestRef.class));
        when(pullRequest.getToRef()).thenReturn(mock(PullRequestRef.class));
        newTestRestJsonRenderer().render(pullRequest, Collections.<String, Object>singletonMap("markup", ""));
        verify(markupEnricher).enrich(isA(PullRequestSurrogate.class));
    }

    @Test
    public void testRenderingCollections() throws Exception {
        assertJsonEquals("[]", newTestRestJsonRenderer().render(Collections.emptyList(), noOptions()));
        assertJsonEquals("[\"a\"]", newTestRestJsonRenderer().render(Collections.singleton("a"), noOptions()));
        assertJsonEquals("[\"a\",\"b\"]", newTestRestJsonRenderer().render(Arrays.asList("a", "b"), noOptions()));
        assertJsonEquals("[" + userJson("foo") + "]", newTestRestJsonRenderer().render(Collections.singleton(user("foo")), noOptions()));
        assertJsonEquals("[[" + userJson("foo") + "]]", newTestRestJsonRenderer().render(Collections.singleton(Collections.singleton(user("foo"))), noOptions()));
    }

    @Test
    public void testRenderingMaps() throws Exception {
        //Casting to (Object) on these calls is unnecessary for Java 7 but required for Java 8.
        //Please do not remove these casts! They're part of supporting building with both 7 and
        //8, to allow us to ensure upgrading to Java 8 for 4.0 doesn't require huge changes.
        assertJsonEquals("{}", newTestRestJsonRenderer().render(ImmutableMap.of(), noOptions()));
        assertJsonEquals("{\"a\":\"b\"}", newTestRestJsonRenderer().render(ImmutableMap.of("a", "b"), noOptions()));
        assertJsonEquals("{\"a\":" + userJson("foo") + "}", newTestRestJsonRenderer().render(ImmutableMap.of("a", user("foo")), noOptions()));
        assertJsonEquals("{\"a\":{\"b\":" + userJson("foo") + "}}", newTestRestJsonRenderer().render(ImmutableMap.of("a", ImmutableMap.of("b", user("foo"))), noOptions()));
    }

    private ApplicationUser user(String name) {
        ApplicationUser user = mock(ApplicationUser.class);
        when(user.getId()).thenReturn(1);
        when(user.getName()).thenReturn(name);
        when(user.getDisplayName()).thenReturn(name);
        when(user.getSlug()).thenReturn(name.replace('/', '_'));
        when(user.getEmailAddress()).thenReturn(name);
        when(user.isActive()).thenReturn(true);
        when(user.getType()).thenReturn(UserType.NORMAL);
        return user;
    }

    private String userJson(String name) {
        return userJson(name, name);
    }

    private String userJson(String name, String slug) {
        return "{\"name\":\"" + name + "\",\"id\":1,\"displayName\":\"" + name + "\",\"emailAddress\":\"" + name +
                "\",\"active\":true,\"slug\":\"" + slug + "\",\"type\":\"NORMAL\"}";
    }

    private Map<String, Object> noOptions() {
        return Collections.emptyMap();
    }

    private void assertJsonEquals(String expected, String actual) throws IOException {
        ObjectMapper mapper = new ObjectMapper();
        assertEquals(mapper.readTree(expected), mapper.readTree(actual));
    }

    @JsonSerialize
    @JsonSurrogate(PullRequest.class)
    private static class PullRequestSurrogate {
        @SuppressWarnings("unused") //This constructor is mandated by the JSON surrogate
        public PullRequestSurrogate(PullRequest pullRequest) {
        }
    }
}
