package com.atlassian.bitbucket.internal.mirroring.mirror.dao;

import com.atlassian.activeobjects.internal.ActiveObjectsSqlException;
import com.atlassian.activeobjects.test.TestActiveObjects;
import com.atlassian.bitbucket.ao.AbstractAoDaoTest;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamServer;
import com.atlassian.bitbucket.internal.mirroring.mirror.InternalUpstreamServerMatcher;
import com.atlassian.bitbucket.internal.mirroring.mirror.SimpleUpstreamAccount;
import com.atlassian.bitbucket.internal.mirroring.mirror.UpstreamInstallationParameters;
import com.atlassian.bitbucket.mirroring.mirror.IntegrationState;
import com.atlassian.bitbucket.mirroring.mirror.MirroringMode;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServer;
import com.atlassian.bitbucket.mirroring.mirror.UpstreamServerType;
import com.google.common.collect.ImmutableList;
import net.java.ao.test.jdbc.Data;
import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import static com.atlassian.bitbucket.mirroring.mirror.IntegrationState.*;
import static com.atlassian.bitbucket.mirroring.mirror.UpstreamServerType.BITBUCKET_CLOUD;
import static com.atlassian.bitbucket.mirroring.mirror.UpstreamServerType.BITBUCKET_SERVER;
import static java.util.Optional.of;
import static org.junit.Assert.*;

@Data(AoDatabaseUpdater.class)
public class AoUpstreamServerDaoTest extends AbstractAoDaoTest {

    private static final String BASE_URL = "http://mirror.com/bitbucket";
    private static final String ID = "uuid-id";
    private static final String NAMESPACE = "bitbucket";

    private AoUpstreamServerDao dao;

    public AoUpstreamServerDaoTest() {
        super(AoUpstreamServer.class);
    }

    @Before
    public void setUp() {
        dao = new AoUpstreamServerDao(new TestActiveObjects(entityManager));
    }

    @Test
    public void testCreate() {
        assertCreate(ID, BASE_URL, BITBUCKET_SERVER, INITIALIZING);
    }

    @Test
    public void testCreateWithTrailingSlash() {
        assertCreate(ID, BASE_URL + "/", BITBUCKET_SERVER, PENDING);
    }

    @Test
    public void testCreateWithMultipleTrailingSlash() {
        assertCreate(ID, BASE_URL + "///", BITBUCKET_SERVER, INSTALLED);
    }

    @Test
    public void testCreateForCloud() {
        assertCreate(ID, BASE_URL + "///", BITBUCKET_CLOUD, PENDING);
    }

    @Test(expected = ActiveObjectsSqlException.class)
    public void testCreateWithDuplicateBaseUrlsIsPrevented() {
        assertUpstreamServer(createUpstreamServer(), INITIALIZING, BITBUCKET_SERVER);
        createUpstreamServer();
    }

    @Test
    public void testGetByClientKey() {
        AoUpstreamServer upstream = createUpstreamServer();
        upstream.setIssuerId("client-key");
        upstream.setSharedSecret("secret");
        upstream.save();

        AoUpstreamServer retrieved = dao.getByIssuerId("client-key");
        assertNotNull(retrieved);
        assertEquals(upstream.getId(), retrieved.getId());
        assertEquals(of("client-key"), retrieved.getIssuerId());
        assertEquals(of("secret"), retrieved.getSharedSecret());
    }

    @Test
    public void testGetByClientKeyNotFound() {
        assertNull(dao.getByIssuerId("no-such-client-key"));
    }

    @Test
    public void testGetById() {
        createUpstreamServer();
        assertUpstreamServer(dao.getById(ID), INITIALIZING, BITBUCKET_SERVER);
        assertNull(dao.getById("does-not-exist"));
    }

    @Test
    public void testGetByNamespace() {
        createUpstreamServer();
        assertUpstreamServer(dao.getByNamespace(NAMESPACE), INITIALIZING, BITBUCKET_SERVER);
        assertNull(dao.getByNamespace("does-not-exist"));
    }

    @Test
    public void testGetMaster() {
        assertNull(dao.getUpstream());

        createUpstreamServer();
        UpstreamServer master = dao.getUpstream();
        assertNotNull(master);
        assertEquals(ID, master.getId());
        assertEquals(BASE_URL, master.getBaseUrl());
    }

    @Test
    public void testGetSettingsById() {
        createUpstreamServer();

        AoUpstreamSettings settings = dao.getSettingsById(ID);
        assertMirrorSettings(settings, ID, AoUpstreamServerDao.DEFAULT_MODE);
    }

    @Test
    public void testGetSettingsByIdReturnsNullIfNotFound() {
        assertNull(dao.getSettingsById("does-not-exist"));
    }

    @Test
    public void testTransitionState() {
        createUpstreamServer();
        assertUpstreamServer(dao.transitionState(ID, IntegrationState.INSTALLED), IntegrationState.INSTALLED, BITBUCKET_SERVER);
    }

    @Test
    public void testTransitionStateNonExistent() {
        assertNull(dao.transitionState("does-not-exist", IntegrationState.INSTALLED));
    }

    @Test
    public void testSetAccountName() throws Exception {
        AoUpstreamServer upstream = createUpstreamServer();
        AoUpstreamServer updated = dao.setAccountName(upstream.getId(), "foobar");
        assertEquals("foobar", updated.getAccountName());
    }

    @Test
    public void testSetInstalledParametersAll() {
        String apiUrl = "api";
        String clientKey = "client:30";
        String secret = "secret";
        String teamId = "teamId";
        String teamName = "teamName";

        AoUpstreamServer upstream = createUpstreamServer();
        UpstreamInstallationParameters install = UpstreamInstallationParameters.builder()
                .apiBaseUrl(apiUrl + "////")
                .issuerId(clientKey)
                .secret(secret)
                .team(teamId, teamName)
                .upstreamId(ID)
                .build();

        AoUpstreamServer updated = dao.setInstalledParameters(install);
        assertThat(updated, isUpstream(upstream)
                .apiBaseUrl(apiUrl)
                .issuerId(clientKey)
                .secret(secret)
                .account(new SimpleUpstreamAccount(teamId, teamName))
                .id(ID));
    }

    @Test
    public void testSetInstalledParametersRequired() {
        String clientKey = "client:30";
        String secret = "secret";

        AoUpstreamServer upstream = createUpstreamServer();
        UpstreamInstallationParameters install = UpstreamInstallationParameters.builder()
                .issuerId(clientKey)
                .secret(secret)
                .upstreamId(ID)
                .build();

        AoUpstreamServer updated = dao.setInstalledParameters(install);
        assertThat(updated, isUpstream(upstream)
                .apiBaseUrl(upstream.getBaseUrl())
                .issuerId(clientKey)
                .secret(secret)
                .account(null)
                .id(ID));
    }

    @Test
    public void testSetInstalledParametersWhenUpstreamDoesNotExist() {
        assertNull(dao.setInstalledParameters(UpstreamInstallationParameters.builder()
                .upstreamId(ID)
                .issuerId("issuer")
                .secret("secret")
                .build()));
    }

    @Test
    public void testSetNamespace() throws Exception {
        AoUpstreamServer upstream = createUpstreamServer();
        dao.setNamespace(upstream.getId(), "foobar");
        assertNull(dao.getByNamespace(NAMESPACE));
        assertThat(upstream, isUpstream()
                .id(ID)
                .state(INITIALIZING)
                .baseUrl(BASE_URL)
                .apiBaseUrl(BASE_URL)
                .namespace("foobar")
                .issuerId(null)
                .secret(null)
                .type(BITBUCKET_SERVER));
    }

    @Test
    public void testUpdateSettingsReturnsNullIfNotFound() {
        assertNull(dao.updateSettings("does-not-exist", MirroringMode.ALL_PROJECTS, Collections.emptyList()));
    }

    @Test
    public void testUpdateSettings() {
        createUpstreamServer();

        AoUpstreamSettings updated = dao.updateSettings(ID, MirroringMode.SELECTED_PROJECTS,
                ImmutableList.of("12", "13", "25", "12"));
        assertMirrorSettings(updated, ID, MirroringMode.SELECTED_PROJECTS, "12", "13", "25");

        updated = dao.updateSettings(ID, MirroringMode.SELECTED_PROJECTS,
                ImmutableList.of("25", "99", "12", "77"));
        assertMirrorSettings(updated, ID, MirroringMode.SELECTED_PROJECTS, "12", "25", "77", "99");
    }

    @Test
    public void testUpdateSettingsAddRemove() {
        createUpstreamServer();

        AoUpstreamSettings updated = dao.updateSettings(ID, MirroringMode.SELECTED_PROJECTS,
                ImmutableList.of("12", "13", "25", "12"), ImmutableList.of("12"));

        assertMirrorSettings(updated, ID, MirroringMode.SELECTED_PROJECTS, "13", "25");

        // add/remove some more
        updated = dao.updateSettings(ID, MirroringMode.SELECTED_PROJECTS,
                ImmutableList.of("13", "2", "90"), ImmutableList.of("25", "99"));
        assertMirrorSettings(updated, ID, MirroringMode.SELECTED_PROJECTS, "2", "13", "90");

    }

    @Test
    public void testUpdateSettingsAddRemoveReturnsNullIfNotFound() {
        assertNull(dao.updateSettings("does-not-exist", MirroringMode.ALL_PROJECTS,
                Collections.emptyList(), Collections.emptyList()));
    }

    private void assertMirrorSettings(AoUpstreamSettings settings, String id, MirroringMode mode, String...projectIds) {
        assertNotNull(settings);
        assertEquals(id, settings.getUpstreamId());
        assertEquals(mode.getId(), settings.getMode());

        Set<String> expected = new HashSet<>(Arrays.asList(projectIds));
        Set<String> notExpectedButFound = new HashSet<>();
        for (AoMirroredProject mirroredProject : settings.getMirroredProjects()) {
            if (!expected.remove(mirroredProject.getExternalId())) {
                notExpectedButFound.add(mirroredProject.getExternalId());
            }
        }
        assertEquals("Expected but not found", new HashSet<String>(), expected);
        assertEquals("Not expected but found", new HashSet<String>(), notExpectedButFound);
    }

    private void assertUpstreamServer(AoUpstreamServer upstream, IntegrationState state, UpstreamServerType type) {
        assertUpstreamServer(upstream, ID, state, null, null, type);
    }

    private void assertUpstreamServer(AoUpstreamServer upstream, String id, IntegrationState state, String issuerId,
                                      String secret, UpstreamServerType type) {
        assertThat(upstream, isUpstream()
                .id(id)
                .state(state)
                .baseUrl(BASE_URL)
                .apiBaseUrl(BASE_URL)
                .namespace(NAMESPACE)
                .issuerId(issuerId)
                .secret(secret)
                .type(type));
    }

    private AoUpstreamServer createUpstreamServer() {
        return createUpstreamServer(ID, BASE_URL, BITBUCKET_SERVER, INITIALIZING);
    }

    private AoUpstreamServer createUpstreamServer(String id, String baseUrl, UpstreamServerType type, IntegrationState state) {
        return dao.create(new CreateUpstreamServerRequest.Builder()
                .id(id)
                .baseUrl(baseUrl)
                .upstreamType(type)
                .state(state)
                .namespace(NAMESPACE)
                .build());
    }

    private void assertCreate(String id, String baseUrl, UpstreamServerType type, IntegrationState state) {
        AoUpstreamServer upstream = createUpstreamServer(id, baseUrl, type, state);

        assertUpstreamServer(upstream, state, type);
        AoUpstreamSettings settings = dao.getSettingsById(id);
        assertMirrorSettings(settings, id, MirroringMode.SELECTED_PROJECTS);
    }

    private static InternalUpstreamServerMatcher isUpstream() {
        return new InternalUpstreamServerMatcher();
    }

    private static InternalUpstreamServerMatcher isUpstream(InternalUpstreamServer server) {
        return new InternalUpstreamServerMatcher(server);
    }
}