package com.atlassian.stash.internal.pull;

import com.atlassian.bitbucket.annotation.Dao;
import com.atlassian.bitbucket.annotation.DataSet;
import com.atlassian.bitbucket.repository.*;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageUtils;
import com.atlassian.stash.internal.AbstractDaoTest;
import com.atlassian.stash.internal.DataSets;
import com.atlassian.stash.internal.repository.HibernateRepositoryDao;
import com.atlassian.stash.internal.repository.InternalRepository;
import com.atlassian.stash.internal.repository.RepositoryDao;
import com.atlassian.stash.internal.user.HibernateStashUserDao;
import com.atlassian.stash.internal.user.InternalApplicationUser;
import com.atlassian.stash.internal.user.StashUserDao;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.junit.Test;

import javax.annotation.Nonnull;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@DataSet({DataSets.USERS, DataSets.PROJECTS, DataSets.REPOSITORIES, DataSets.RESCOPE_REQUESTS})
public class HibernateRescopeRequestDaoTest extends AbstractDaoTest {

    private static Collection<RefChange> REQ_1_REF_CHANGES = ImmutableList.of(
            refUpdate("refs/heads/master", "master", "ebc2923017e627cff6103404e7c4c66f6d5d66cd",
                    "f959ad838544e450ad6a2be17b82db04d5e4290d"));
    private static Collection<RefChange> REQ_2_REF_CHANGES = ImmutableList.of(
            refUpdate("refs/heads/1.2", "1.2", "4e7a17ac1428c5e5ac388a7deb8b0cc2fb7ccbc3",
                    "625ec307cb48ab285fd304ef7e97b153c200286b"),
            refUpdate("refs/heads/1.3", "1.3", "f2e33aaf4252acbbe02a0ae0ce52eb0d4dc7c9e4",
                    "c955806cc882d7cf7b627ac6aa92a0855114fadc"));

    @Dao
    private HibernateRescopeRequestDao dao;
    @Dao(HibernateRepositoryDao.class)
    private RepositoryDao repositoryDao;
    @Dao(HibernateStashUserDao.class)
    private StashUserDao userDao;

    @Test
    public void testCreate() {
        int repositoryId = 2;
        int userId = 3;
        String hash1 = "157517e346389e68b9044dea7829041c69ec636e";
        String hash2 = "04c5168928172b444afdd3f222ca30882e374e59";

        InternalApplicationUser user = userDao.getById(userId);
        InternalRepository repository = repositoryDao.getById(repositoryId);
        List<RefChange> changes = ImmutableList.of(refUpdate("refs/heads/test", "test", hash1, hash2));
        InternalRescopeRequest created = dao.create(
                new InternalRescopeRequest.Builder(repository, user)
                        .changes(changes)
                        .build());

        assertNotNull(created);

        InternalRescopeRequest retrieved = dao.getById(created.getId());
        assertRequest(retrieved, created.getId(), repositoryId, userId, changes);
    }

    @Test
    public void testCreateWithoutRefChanges() {
        int repositoryId = 2;
        int userId = 3;

        InternalApplicationUser user = userDao.getById(userId);
        InternalRepository repository = repositoryDao.getById(repositoryId);
        InternalRescopeRequest created = dao.create(
                new InternalRescopeRequest.Builder(repository, user)
                        .build());

        assertNotNull(created);

        InternalRescopeRequest retrieved = dao.getById(created.getId());
        assertRequest(retrieved, created.getId(), repositoryId, userId, Collections.emptySet());
    }

    @Test
    public void testCreateWithShortAndMissingHashes() {
        int repositoryId = 2;
        int userId = 3;
        String hash1 = "157517e346389";
        String hash2 = "04c5168928172";

        InternalApplicationUser user = userDao.getById(userId);
        InternalRepository repository = repositoryDao.getById(repositoryId);
        List<RefChange> changes = ImmutableList.of(
                refUpdate("refs/heads/short", "short", hash1, hash2),
                refUpdate("refs/heads/missing", "missing", null, null));
        InternalRescopeRequest created = dao.create(
                new InternalRescopeRequest.Builder(repository, user)
                        .changes(changes)
                        .build());

        assertNotNull(created);

        InternalRescopeRequest retrieved = dao.getById(created.getId());
        assertRequest(retrieved, created.getId(), repositoryId, userId, changes);
    }

    @Test
    public void testDelete() {
        InternalRescopeRequest request = dao.getById(2L);

        assertNotNull(request);
        dao.delete(request);

        assertNull(dao.getById(2L));

        Page<InternalRescopeRequest> page = dao.findAll(PageUtils.newRequest(0, 10));
        assertEquals(2, page.getSize());
        assertTrue(page.getIsLastPage());

        List<InternalRescopeRequest> requests = ImmutableList.copyOf(page.getValues());
        assertRequest(requests.get(0), 1L, 1, 1, REQ_1_REF_CHANGES);
        assertRequest(requests.get(1), 3L, 2, 4, Collections.emptySet());
    }

    @Test
    public void testDeleteByIds() {
        dao.deleteByIds(ImmutableList.of(2L, 3L, 9L)); // 9 does not exist

        flush();

        Page<InternalRescopeRequest> page = dao.findAll(PageUtils.newRequest(0, 10));
        assertNotNull(page);
        assertEquals(1, page.getSize());
        assertTrue(page.getIsLastPage());
        assertRequest(Iterables.getFirst(page.getValues(), null), 1L, 1, 1, REQ_1_REF_CHANGES);
    }

    @Test
    public void testFindAll() {
        Page<InternalRescopeRequest> page = dao.findAll(PageUtils.newRequest(0, 10));
        assertNotNull(page);
        assertEquals(3, page.getSize());
        assertTrue(page.getIsLastPage());

        List<InternalRescopeRequest> requests = ImmutableList.copyOf(page.getValues());
        assertRequest(requests.get(0), 1L, 1, 1, REQ_1_REF_CHANGES);
        assertRequest(requests.get(1), 2L, 3, 4, REQ_2_REF_CHANGES);
        assertRequest(requests.get(2), 3L, 2, 4, Collections.emptySet());
    }

    @Test
    public void testFindByRepository() {
        List<InternalRescopeRequest> requests = dao.findByRepository(1);
        assertNotNull(requests);
        assertEquals(1, requests.size());
        assertRequest(requests.get(0), 1L, 1, 1, REQ_1_REF_CHANGES);

        requests = dao.findByRepository(9); // There are no requests for repository 9
        assertNotNull(requests);
        assertTrue(requests.isEmpty());
    }

    @Test
    public void testFindByMultipleRepositories() {
        List<InternalRescopeRequest> requests = dao.findByRepositories(Lists.newArrayList(1, 3));
        assertNotNull(requests);
        assertEquals(2, requests.size());
        assertRequest(requests.get(0), 1L, 1, 1, REQ_1_REF_CHANGES);
        assertRequest(requests.get(1), 2L, 3, 4, REQ_2_REF_CHANGES);
    }

    @Test
    public void testFindByManyManyRepositories() {
        // create a long list of ids, with existing ids at the beginning and end of the list so we can verify the
        // paging behaviour
        List<Integer> ids = IntStream.range(3, 833).boxed().collect(Collectors.toList());
        ids.add(1);

        List<InternalRescopeRequest> requests = dao.findByRepositories(ids);
        assertNotNull(requests);
        assertEquals(2, requests.size());
        assertRequest(requests.get(0), 1L, 1, 1, REQ_1_REF_CHANGES);
        assertRequest(requests.get(1), 2L, 3, 4, REQ_2_REF_CHANGES);
    }

    @Test
    public void testFindPendingRepositories() {
        List<Integer> repositoryIds = dao.findPendingRepositoryIds();
        assertNotNull(repositoryIds);
        assertEquals(3, repositoryIds.size());
        assertEquals(1, repositoryIds.get(0).intValue());
        assertEquals(2, repositoryIds.get(1).intValue());
        assertEquals(3, repositoryIds.get(2).intValue());
    }

    @Test
    public void testGetById() {
        assertRequest(dao.getById(2L), 2L, 3, 4, REQ_2_REF_CHANGES);
        assertNull(dao.getById(99L));
    }

    @Test
    public void testGetByIds() {
        List<InternalRescopeRequest> requests = dao.getByIds(ImmutableList.of(3L, 2L, 9L)); // 9 does not exist

        assertEquals(2, requests.size());
        assertRequest(requests.get(0), 2L, 3, 4, REQ_2_REF_CHANGES);
        assertRequest(requests.get(1), 3L, 2, 4, Collections.emptySet());
    }

    private static void assertRequest(InternalRescopeRequest actual, long id, int repositoryId,
                                      int userId, Collection<RefChange> changes) {
        assertNotNull(actual);
        assertEquals("id", id, actual.getId());
        assertEquals("repositoryId", repositoryId, actual.getRepository().getId());
        assertEquals("userId", userId, actual.getUser().getId());

        assertEquals(toSimpleRefChange(changes), toSimpleRefChange(actual.getChanges()));
    }

    private static Set<RefChange> toSimpleRefChange(Collection<RefChange> changes) {
        return changes.stream()
                .map(c -> new UnsafeRefChange(c.getRef(), c.getType(), c.getFromHash(), c.getToHash()))
                .collect(Collectors.toSet());
    }

    private static RefChange refUpdate(String refId, String displayId, String fromHash, String toHash) {
        MinimalRef ref = new SimpleMinimalRef.Builder()
                .type(StandardRefType.BRANCH)
                .id(refId)
                .displayId(displayId)
                .build();
        return new UnsafeRefChange(ref, RefChangeType.UPDATE, fromHash, toHash);
    }

    private static class UnsafeRefChange implements RefChange {
        private final String fromHash;
        private final MinimalRef ref;
        private final String toHash;
        private final RefChangeType type;

        public UnsafeRefChange(MinimalRef ref, RefChangeType type, String from, String to) {
            this.fromHash = from;
            this.ref = ref;
            this.toHash = to;
            this.type = type;
        }

        @Nonnull
        @Override
        public String getFromHash() {
            return fromHash;
        }

        @Nonnull
        @Override
        public MinimalRef getRef() {
            return ref;
        }

        @Nonnull
        @Override
        public String getRefId() {
            return getRef().getId();
        }

        @Nonnull
        @Override
        public String getToHash() {
            return toHash;
        }

        @Nonnull
        @Override
        public RefChangeType getType() {
            return type;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }
            UnsafeRefChange that = (UnsafeRefChange) o;
            return Objects.equals(fromHash, that.fromHash) &&
                    Objects.equals(ref, that.ref) &&
                    Objects.equals(toHash, that.toHash) &&
                    Objects.equals(type, that.type);
        }

        @Override
        public int hashCode() {
            return Objects.hash(fromHash, ref, toHash, type);
        }

        public String toString() {
            return "{ref: " + ref.getId() + ", type: " + type + ", from: " + fromHash + ", to: " + toHash + "}";
        }
    }
}
