package com.larscheidschmitzhermes.nexus3.github.oauth.plugin;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.larscheidschmitzhermes.nexus3.github.oauth.plugin.api.GithubApiClient;
import com.larscheidschmitzhermes.nexus3.github.oauth.plugin.api.GithubOrg;
import com.larscheidschmitzhermes.nexus3.github.oauth.plugin.api.GithubTeam;
import com.larscheidschmitzhermes.nexus3.github.oauth.plugin.api.GithubUser;
import com.larscheidschmitzhermes.nexus3.github.oauth.plugin.configuration.GithubOauthConfiguration;
import com.larscheidschmitzhermes.nexus3.github.oauth.plugin.configuration.MockGithubOauthConfiguration;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.hamcrest.MatcherAssert;
import org.hamcrest.core.Is;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@RunWith(MockitoJUnitRunner.class)
public class GithubApiClientTest {
    private MockGithubOauthConfiguration config = new MockGithubOauthConfiguration(Duration.ofDays(1));
    private ObjectMapper mapper = new ObjectMapper();

    private List<GithubTeam> mockTeams() {
        GithubOrg org = new GithubOrg();
        org.setLogin("TEST-ORG");

        List<GithubTeam> teams = new ArrayList<>();

        GithubTeam team = new GithubTeam();
        team.setOrganization(org);
        team.setName("admin");
        teams.add(team);

        return teams;
    }

    private GithubUser mockUser(String username) {
        GithubUser user = new GithubUser();
        user.setName(username);
        user.setLogin("demo-user");
        return user;
    }

    private Set<GithubOrg> mockOrg(String orgname) {
        Set orgs = new HashSet();
        GithubOrg org = new GithubOrg();
        org.setLogin(orgname);
        orgs.add(org);
        return orgs;
    }

    private HttpResponse createMockResponse(Object entity) throws IOException {
        HttpResponse mockOrgRespone = Mockito.mock(HttpResponse.class, Mockito.RETURNS_DEEP_STUBS);
        Mockito.when(mockOrgRespone.getStatusLine().getStatusCode()).thenReturn(200);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        mapper.writeValue(baos, entity);
        byte[] data = baos.toByteArray();
        Mockito.when(mockOrgRespone.getEntity().getContent()).thenReturn(new ByteArrayInputStream(data));
        return mockOrgRespone;
    }

    private HttpClient fullyFunctionalMockClient() throws IOException {
        HttpClient mockClient = Mockito.mock(HttpClient.class);
        mockResponsesForGithubAuthRequest(mockClient);
        return mockClient;
    }

    private void mockResponsesForGithubAuthRequest(HttpClient mockClient) throws IOException {
        HttpResponse mockUserResponse = createMockResponse(mockUser("Hans Wurst"));
        Mockito.when(mockClient.execute(Mockito.any())).thenAnswer(invocationOnMock -> answerOnInvocation(invocationOnMock, mockUserResponse));
    }

    private HttpResponse answerOnInvocation(InvocationOnMock invocationOnMock, HttpResponse mockUserResponse) throws IOException {
        HttpResponse mockTeamResponse = createMockResponse(mockTeams());
        HttpResponse mockOrgsResponse = createMockResponse(mockOrg("TEST-ORG"));

        String uriString = ((HttpGet) invocationOnMock.getArguments()[0]).getURI().toString();
        if (uriString.equals(config.getGithubUserTeamsUri())) {
            return mockTeamResponse;
        } else if (uriString.equals(config.getGithubUserUri())) {
            return mockUserResponse;
        } else if (uriString.equals(config.getGithubUserOrgsUri())) {
            return mockOrgsResponse;
        }
        return null;
    }

    @Test
    public void shouldDoAuthzIfRequestStatusIs200() throws Exception {
        HttpClient mockClient = fullyFunctionalMockClient();

        GithubApiClient clientToTest = new GithubApiClient(mockClient, config);
        GithubPrincipal authorizedPrincipal = clientToTest.authz("demo-user", "DUMMY".toCharArray());

        MatcherAssert.assertThat(authorizedPrincipal.getRoles().size(), Is.is(1));
        MatcherAssert.assertThat(authorizedPrincipal.getRoles().iterator().next(), Is.is("TEST-ORG/admin"));
        MatcherAssert.assertThat(authorizedPrincipal.getUsername(), Is.is("demo-user"));

    }

    @Test(expected = GithubAuthenticationException.class)
    public void shouldNotAuthenticateIfRequestIsNot200() throws Exception {
        HttpClient mockClient = Mockito.mock(HttpClient.class);
        HttpResponse mockRespone = Mockito.mock(HttpResponse.class, Mockito.RETURNS_DEEP_STUBS);
        Mockito.when(mockRespone.getStatusLine().getStatusCode()).thenReturn(403);
        Mockito.when(mockClient.execute(Mockito.any())).thenReturn(mockRespone);

        GithubApiClient clientToTest = new GithubApiClient(mockClient, new MockGithubOauthConfiguration(Duration.ofDays(1)));

        clientToTest.authz("demo-user", "DUMMY".toCharArray());
    }

    @Test(expected = GithubAuthenticationException.class)
    public void shouldNotAuthenticateIfUsernameDoesntMatch() throws Exception {
        HttpClient mockClient = fullyFunctionalMockClient();

        GithubApiClient clientToTest = new GithubApiClient(mockClient, config);
        clientToTest.authz("not-the-demo-user", "DUMMY".toCharArray());
    }

    @Test(expected = GithubAuthenticationException.class)
    public void shouldNotAuthenticateIfUserNotInOrg() throws Exception {
        HttpClient mockClient = fullyFunctionalMockClient();
        config.setGithubOrg("OTHER-ORG");
        GithubApiClient clientToTest = new GithubApiClient(mockClient, config);
        clientToTest.authz("demo-user", "DUMMY".toCharArray());
    }

    @Test
    public void cachedPrincipalReturnsIfNotExpired() throws Exception {
        HttpClient mockClient = fullyFunctionalMockClient();

        GithubApiClient clientToTest = new GithubApiClient(mockClient, config);
        String login = "demo-user";
        char[] token = "DUMMY".toCharArray();
        clientToTest.authz(login, token);

        // We make 2 calls to Github for a single auth check
        Mockito.verify(mockClient, Mockito.times(3)).execute(Mockito.any(HttpGet.class));
        Mockito.verifyNoMoreInteractions(mockClient);

        // This invocation should hit the cache and should not use the client
        clientToTest.authz(login, token);
        Mockito.verifyNoMoreInteractions(mockClient);
    }

    @Test
    public void shouldNotCheckOrgDuringAuthentication() throws Exception {
        HttpClient mockClient = fullyFunctionalMockClient();
        config.setGithubOrg(null);

        GithubApiClient clientToTest = new GithubApiClient(mockClient, config);
        String login = "demo-user";
        char[] token = "DUMMY".toCharArray();
        clientToTest.authz(login, token);

        // We make 2 calls to Github for a single auth check
        Mockito.verify(mockClient, Mockito.times(2)).execute(Mockito.any(HttpGet.class));
        Mockito.verifyNoMoreInteractions(mockClient);

    }

    @Test
    public void principalCacheHonorsTtl() throws Exception {
        HttpClient mockClient = fullyFunctionalMockClient();

        GithubOauthConfiguration configWithShortCacheTtl = new MockGithubOauthConfiguration(Duration.ofMillis(1));
        GithubApiClient clientToTest = new GithubApiClient(mockClient, configWithShortCacheTtl);
        char[] token = "DUMMY".toCharArray();

        clientToTest.authz("demo-user", token);
        // We make 2 calls to Github for a single auth check
        Mockito.verify(mockClient, Mockito.times(3)).execute(Mockito.any(HttpGet.class));
        Mockito.verifyNoMoreInteractions(mockClient);

        // Wait a bit for the cache to become invalidated
        Thread.sleep(10);

        // Mock the responses again so a second auth attempt works
        Mockito.reset(mockClient);
        mockResponsesForGithubAuthRequest(mockClient);
        // This should also hit Github because the cache TTL has elapsed
        clientToTest.authz("demo-user", token);
        // We make 3 calls to Github for a single auth check
        Mockito.verify(mockClient, Mockito.times(3)).execute(Mockito.any(HttpGet.class));
        Mockito.verifyNoMoreInteractions(mockClient);
    }

    @Test
    public void shouldAcceptOrgAnywhereInList() throws Exception {
        HttpClient mockClient = fullyFunctionalMockClient();
        config.setGithubOrg("TEST-ORG,TEST-ORG2");
        GithubApiClient clientToTest = new GithubApiClient(mockClient, config);
        GithubPrincipal authorizedPrincipal = clientToTest.authz("demo-user", "DUMMY".toCharArray());
        MatcherAssert.assertThat(authorizedPrincipal.getRoles().iterator().next(), Is.is("TEST-ORG/admin"));

        HttpClient mockClient2 = fullyFunctionalMockClient();
        config.setGithubOrg("TEST-ORG2,TEST-ORG");
        GithubApiClient clientToTest2 = new GithubApiClient(mockClient2, config);
        GithubPrincipal authorizedPrincipal2 = clientToTest2.authz("demo-user", "DUMMY".toCharArray());
        MatcherAssert.assertThat(authorizedPrincipal2.getRoles().iterator().next(), Is.is("TEST-ORG/admin"));
    }
}