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

import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;
import com.atlassian.bitbucket.util.PageUtils;
import com.google.common.collect.Iterables;

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

import static java.util.Comparator.comparing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
* Stub for testing interactions with RepositoryMappingDao
*/
public class StubRepositoryMappingDao implements RepositoryMappingDao {

    private final Map<String, AoRepositoryMapping> dataByExternalId = new HashMap<>();
    private final Map<Integer, AoRepositoryMapping> dataByLocalId = new HashMap<>();

    @Override
    public int countSyncedInProgress(@Nonnull String upstreamServerId) {
        // Note: This currently doesn't filter out the repos from non-IN_PROGRESS projects like its counterpart
        // AoRepositoryMappingDao.countAllInProgress().  No test currently relies on this.
        return (int) dataByLocalId.values()
                .stream()
                .filter(mapping -> mapping.getUpstreamId().equals(upstreamServerId) && mapping.getLastSyncDate() != null)
                .count();
    }

    @Override
    public int countAllInProgress(@Nonnull String upstreamServerId) {
        // Note: This currently doesn't filter out the repos from non-IN_PROGRESS projects like its counterpart
        // AoRepositoryMappingDao.countAllInProgress().  No test currently relies on this.
        return (int) dataByLocalId.values()
                .stream()
                .filter(mapping -> mapping.getUpstreamId().equals(upstreamServerId))
                .count();
    }

    @Nonnull
    @Override
    public AoRepositoryMapping create(int localId, @Nonnull String upstreamServerId, int localProjectId,
                                      @Nonnull String upstreamRepositoryId) {
        AoRepositoryMapping mapping = mock(AoRepositoryMapping.class);
        when(mapping.getLocalId()).thenReturn(localId);
        when(mapping.getUpstreamId()).thenReturn(upstreamServerId);
        when(mapping.getLocalProjectId()).thenReturn(localProjectId);
        when(mapping.getExternalId()).thenReturn(upstreamRepositoryId);

        dataByExternalId.put(mappingKey(upstreamServerId, upstreamRepositoryId), mapping);
        dataByLocalId.put(localId, mapping);

        return mapping;
    }

    public void add(AoRepositoryMapping mapping) {
        dataByExternalId.put(mappingKey(mapping.getUpstreamId(), mapping.getExternalId()), mapping);
        dataByLocalId.put(mapping.getLocalId(), mapping);
    }

    @Override
    public AoRepositoryMapping getByLocalId(int id) {
        return dataByLocalId.get(id);
    }

    @Override
    public AoRepositoryMapping getByUpstreamId(@Nonnull String upstreamServerId, @Nonnull String upstreamRepositoryId) {
        return dataByExternalId.get(mappingKey(upstreamServerId, upstreamRepositoryId));
    }

    @Nonnull
    @Override
    public Iterable<AoRepositoryMapping> getByUpstreamId(@Nonnull String upstreamServerId, @Nonnull Iterable<String> upstreamRepositoryIds) {
        ArrayList<AoRepositoryMapping> mappings = new ArrayList<>();
        for (String upstreamRepositoryId : upstreamRepositoryIds) {
            mappings.add(dataByExternalId.get(mappingKey(upstreamServerId, upstreamRepositoryId)));
        }
        return mappings;
    }

    @Nonnull
    @Override
    public Set<Integer> getLocalProjectIdsByUpstreamId(String upstreamId) {
        return dataByExternalId.values().stream().map(AoRepositoryMapping::getLocalProjectId).collect(Collectors.toSet());
    }

    @Nonnull
    @Override
    public Page<AoRepositoryMapping> findByUpstreamId(@Nonnull String upstreamServerId, @Nonnull PageRequest pageRequest) {
        List<AoRepositoryMapping> mappings =
                dataByExternalId.values()
                        .stream()
                        .filter(mapping -> mapping.getUpstreamId().equals(upstreamServerId))
                        .sorted(comparing(AoRepositoryMapping::getUpstreamId).thenComparing(AoRepositoryMapping::getExternalId))
                        .collect(Collectors.toList());
        return PageUtils.createPage(Iterables.skip(mappings, pageRequest.getStart()), pageRequest);
    }

    @Override
    public void delete(@Nonnull String upstreamServerId, @Nonnull String upstreamRepositoryId) {
        AoRepositoryMapping deleted = dataByExternalId.remove(mappingKey(upstreamServerId, upstreamRepositoryId));
        if (deleted != null) {
            dataByLocalId.remove(deleted.getLocalId());
        }
    }

    @Override
    public int incrementFailedSyncCount(int localId) {
        AoRepositoryMapping mapping = dataByLocalId.get(localId);
        if (mapping != null) {
            int failedSyncCount = mapping.getFailedSyncCount() + 1;
            mapping.setFailedSyncCount(failedSyncCount);
            return failedSyncCount;
        } else {
            return 0;
        }
    }

    @Override
    public AoRepositoryMapping updateProject(int localId, int localProjectId) {
        AoRepositoryMapping aoRepositoryMapping = dataByLocalId.get(localId);

        if (aoRepositoryMapping != null) {
            when(aoRepositoryMapping.getLocalProjectId()).thenReturn(localProjectId);
            dataByLocalId.put(localId, aoRepositoryMapping);
        }

        return aoRepositoryMapping;
    }

    @Override
    public AoRepositoryMapping updateSyncDate(int localId, @Nonnull Date date) {
        AoRepositoryMapping aoRepositoryMapping = dataByLocalId.get(localId);

        if (aoRepositoryMapping != null) {
            if (aoRepositoryMapping.getInitialSyncDate() == null) {
                when(aoRepositoryMapping.getInitialSyncDate()).thenReturn(date);
            }
            when(aoRepositoryMapping.getLastSyncDate()).thenReturn(date);
            dataByLocalId.put(localId, aoRepositoryMapping);
        }

        return aoRepositoryMapping;
    }

    private static String mappingKey(Object upstreamId, Object externalId) {
        return upstreamId + "::" + externalId;
    }
}
