@Test
public void shouldBuildEvenIfKeyGeneratorIsNotDefined() {
    final String query = "info_hash={infohash}&peer_id={peerid}&supportcrypto=1&port={port}&azudp={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&event={event}&numwant={numwant}&no_peer_id=1&compact=1&azver=3";
    final List<BitTorrentClientConfig.HttpHeader> requestHeaders = Arrays.asList(new HttpHeader("User-Agent", "Azureus 5.7.5.0;{os};1.8.0_66"), new HttpHeader("Connection", "close"), new HttpHeader("Accept-Encoding", "gzip"), new HttpHeader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
    final BitTorrentClientConfig config = new BitTorrentClientConfig(defaultPeerIdGenerator, query, null, defaultUrlEncoder, requestHeaders, 200, 0);
    assertThat(provider.createClient(config).getKey(mock(InfoHash.class), RequestEvent.STARTED)).isEmpty();
}

@Test
public void shouldBuildEvenIfKeyGeneratorIsNotDefined() {
    final String query = "info_hash={infohash}&peer_id={peerid}&supportcrypto=1&port={port}&azudp={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&event={event}&numwant={numwant}&no_peer_id=1&compact=1&azver=3";
    final List<BitTorrentClientConfig.HttpHeader> requestHeaders = Arrays.asList(new HttpHeader("User-Agent", "Azureus 5.7.5.0;{os};1.8.0_66"), new HttpHeader("Connection", "close"), new HttpHeader("Accept-Encoding", "gzip"), new HttpHeader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
    final BitTorrentClientConfig config = new BitTorrentClientConfig(defaultPeerIdGenerator, query, null, defaultUrlEncoder, requestHeaders, 200, 0);
    assertThat(config.createClient().getKey(Mockito.mock(InfoHash.class), RequestEvent.STARTED)).isEmpty();
}

@Test
public void shouldBuildClient() {
    final String query = "info_hash={infohash}&peer_id={peerid}&supportcrypto=1&port={port}&azudp={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&event={event}&numwant={numwant}&no_peer_id=1&compact=1&key={key}&azver=3";
    final List<BitTorrentClientConfig.HttpHeader> requestHeaders = Arrays.asList(new HttpHeader("User-Agent", "Azureus 5.7.5.0;{os};1.8.0_66"), new HttpHeader("Connection", "close"), new HttpHeader("Accept-Encoding", "gzip"), new HttpHeader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
    final PeerIdGenerator peerIdGenerator = PeerIdGeneratorTest.createDefault();
    final KeyGenerator keyGenerator = KeyGeneratorTest.createDefault();
    final BitTorrentClientConfig config = new BitTorrentClientConfig(peerIdGenerator, query, keyGenerator, defaultUrlEncoder, requestHeaders, 200, 0);
    final BitTorrentClient client = provider.createClient(config);
    assertThat(client.getHeaders()).isEqualTo(requestHeaders.stream().map(header -> new AbstractMap.SimpleEntry<>(header.getName(), header.getValue())).collect(Collectors.toList()));
    assertThat(client.getQuery()).isEqualTo(query);
    assertThat(client.getKey(null, RequestEvent.STARTED).get()).isEqualTo(keyGenerator.getKey(null, RequestEvent.STARTED));
    assertThat(client.getPeerId(null, RequestEvent.STARTED)).isEqualTo(peerIdGenerator.getPeerId(null, RequestEvent.STARTED));
    assertThat(client.getNumwant(RequestEvent.STARTED)).isEqualTo(200);
}

@Test
public void shouldBuildClient() {
    final String query = "info_hash={infohash}&peer_id={peerid}&supportcrypto=1&port={port}&azudp={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&event={event}&numwant={numwant}&no_peer_id=1&compact=1&key={key}&azver=3";
    final List<BitTorrentClientConfig.HttpHeader> requestHeaders = Arrays.asList(new HttpHeader("User-Agent", "Azureus 5.7.5.0;{os};1.8.0_66"), new HttpHeader("Connection", "close"), new HttpHeader("Accept-Encoding", "gzip"), new HttpHeader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
    final PeerIdGenerator peerIdGenerator = PeerIdGeneratorTest.createDefault();
    final KeyGenerator keyGenerator = KeyGeneratorTest.createDefault();
    final BitTorrentClientConfig config = new BitTorrentClientConfig(peerIdGenerator, query, keyGenerator, defaultUrlEncoder, requestHeaders, 200, 0);
    final BitTorrentClient client = config.createClient();
    assertThat(client.getHeaders()).isEqualTo(requestHeaders.stream().map(header -> new AbstractMap.SimpleEntry<>(header.getName(), header.getValue())).collect(Collectors.toList()));
    assertThat(client.getQuery()).isEqualTo(query);
    assertThat(client.getKey(null, RequestEvent.STARTED).get()).isEqualTo(keyGenerator.getKey(null, RequestEvent.STARTED));
    assertThat(client.getPeerId(null, RequestEvent.STARTED)).isEqualTo(peerIdGenerator.getPeerId(null, RequestEvent.STARTED));
    assertThat(client.getNumwant(RequestEvent.STARTED)).isEqualTo(200);
}


@Test
public void shouldDeserialize() throws IOException {
    final AppConfiguration config = mapper.readValue("{\"minUploadRate\":180,\"maxUploadRate\":190,\"simultaneousSeed\":2,\"client\":\"azureus.client\",\"keepTorrentWithZeroLeechers\":false}", AppConfiguration.class);
    assertThat(config.getMinUploadRate()).isEqualTo(180);
    assertThat(config.getMaxUploadRate()).isEqualTo(190);
    assertThat(config.getSimultaneousSeed()).isEqualTo(2);
    assertThat(config.getClient()).isEqualTo("azureus.client");
    assertThat(config.isKeepTorrentWithZeroLeechers()).isEqualTo(false);
}

@Test
public void shouldDeserialize() throws IOException {
    final AppConfiguration config = mapper.readValue("{\"minUploadRate\":180,\"maxUploadRate\":190,\"simultaneousSeed\":2,\"client\":\"azureus.client\",\"keepTorrentWithZeroLeechers\":false}", AppConfiguration.class);
    assertThat(config.getMinUploadRate()).isEqualTo(180);
    assertThat(config.getMaxUploadRate()).isEqualTo(190);
    assertThat(config.getSimultaneousSeed()).isEqualTo(2);
    assertThat(config.getClientFileName()).isEqualTo("azureus.client");
    assertThat(config.shouldKeepTorrentWithZeroLeechers()).isEqualTo(false);
}

@JsonProperty("keepTorrentWithZeroLeechers")
public boolean shouldKeepTorrentWithZeroLeechers() {
    return keepTorrentWithZeroLeechers;
}

@Test
public void shouldIgnoreUnknownProperties() throws IOException {
    final AppConfiguration config = mapper.readValue("{\"minUploadRate\":180,\"maxUploadRate\":190,\"simultaneousSeed\":2,\"client\":\"azureus.client\",\"keepTorrentWithZeroLeechers\":false,\"qddlqjdqlskdjlqk\":\"qdqdqsdd\"}", AppConfiguration.class);
    assertThat(config.getMinUploadRate()).isEqualTo(180);
    assertThat(config.getMaxUploadRate()).isEqualTo(190);
    assertThat(config.getSimultaneousSeed()).isEqualTo(2);
    assertThat(config.getClient()).isEqualTo("azureus.client");
}

@Test
public void shouldIgnoreUnknownProperties() throws IOException {
    final AppConfiguration config = mapper.readValue("{\"minUploadRate\":180,\"maxUploadRate\":190,\"simultaneousSeed\":2,\"client\":\"azureus.client\",\"keepTorrentWithZeroLeechers\":false,\"qddlqjdqlskdjlqk\":\"qdqdqsdd\"}", AppConfiguration.class);
    assertThat(config.getMinUploadRate()).isEqualTo(180);
    assertThat(config.getMaxUploadRate()).isEqualTo(190);
    assertThat(config.getSimultaneousSeed()).isEqualTo(2);
    assertThat(config.getClientFileName()).isEqualTo("azureus.client");
}

@JsonProperty("client")
public String getClientFileName() {
    return client;
}


@Test
public void shouldBuild() {
    final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
    assertThat(config.getMinUploadRate()).isEqualTo(180);
    assertThat(config.getMaxUploadRate()).isEqualTo(190);
    assertThat(config.getSimultaneousSeed()).isEqualTo(2);
    assertThat(config.getClient()).isEqualTo("azureus.client");
}

@Test
public void shouldBuild() {
    final AppConfiguration config = new AppConfiguration(180L, 190L, 2, "azureus.client", false);
    assertThat(config.getMinUploadRate()).isEqualTo(180);
    assertThat(config.getMaxUploadRate()).isEqualTo(190);
    assertThat(config.getSimultaneousSeed()).isEqualTo(2);
    assertThat(config.getClientFileName()).isEqualTo("azureus.client");
}


@Test
public void shouldFailIfJsonFileIsNotPresent() {
    final SeedManager.JoalFoldersPath fakeFolders = new SeedManager.JoalFoldersPath(Paths.get("nop"));
    assertThatThrownBy(() -> new JoalConfigProvider(new ObjectMapper(), fakeFolders, Mockito.mock(ApplicationEventPublisher.class))).isInstanceOf(FileNotFoundException.class).hasMessageContaining("App configuration file [" + fakeFolders.getConfPath() + File.separator + "config.json] not found");
}

@Test
public void shouldFailIfJsonFileIsNotPresent() {
    final SeedManager.JoalFoldersPath fakeFolders = new SeedManager.JoalFoldersPath(Paths.get("nop"));
    assertThatThrownBy(() -> new JoalConfigProvider(new ObjectMapper(), fakeFolders, Mockito.mock(ApplicationEventPublisher.class))).isInstanceOf(FileNotFoundException.class).hasMessageContaining("App configuration file '" + fakeFolders.getConfPath() + File.separator + "config.json' not found.");
}

public Path getConfPath() {
    return confPath;
}


@Test
public void shouldBuild() {
    final Map<InfoHash, Speed> speeds = new HashMap<>();
    final SeedingSpeedsHasChangedEvent event = new SeedingSpeedsHasChangedEvent(speeds);
    assertThat(event.getSpeeds()).isEqualTo(speeds);
}

@Test
public void shouldBuild() {
    final Map<InfoHash, Speed> speeds = Maps.newHashMap();
    final SeedingSpeedsHasChangedEvent event = new SeedingSpeedsHasChangedEvent(speeds);
    assertThat(event.getSpeeds()).isEqualTo(speeds);
}

public Map<InfoHash, Speed> getSpeeds() {
    return speeds;
}


@Test
public void shouldRemoveNonHumanReadableChars() {
    final InfoHash infoHash = new InfoHash("a\u0001b\u0001\u0001cc".getBytes());
    assertThat(infoHash.getHumanReadable()).isEqualTo("abcc");
}

@Test
public void shouldRemoveNonHumanReadableChars() {
    final InfoHash infoHash = new InfoHash("a\u0001".getBytes());
    assertThat(infoHash.humanReadableValue()).isEqualTo("a");
}

public String humanReadableValue() {
    return infoHash.replaceAll("\\p{C}", "");
}


@SuppressWarnings("unchecked")
@Test
public void shouldDeleteTorrentIfNoMorePeersOnlyIfConfigValueShouldKeepInactiveTorrentIsFalse() {
    final AppConfiguration appConfiguration = this.createMockedConf();
    doReturn(false).when(appConfiguration).isKeepTorrentWithZeroLeechers();
    final TorrentFileProvider torrentFileProvider = mock(TorrentFileProvider.class);
    final Client client = (Client) ClientBuilder.builder().withAnnouncerFactory(mock(AnnouncerFactory.class)).withBandwidthDispatcher(mock(BandwidthDispatcher.class)).withAppConfiguration(appConfiguration).withTorrentFileProvider(torrentFileProvider).withEventPublisher(mock(ApplicationEventPublisher.class)).withDelayQueue(mock(DelayQueue.class)).build();
    final InfoHash infoHash = InfoHashTest.createOne("abcd");
    client.onNoMorePeers(infoHash);
    verify(torrentFileProvider, times(1)).moveToArchiveFolder(eq(infoHash));
    final InfoHash infoHash2 = InfoHashTest.createOne("abcd");
    doReturn(true).when(appConfiguration).isKeepTorrentWithZeroLeechers();
    client.onNoMorePeers(infoHash);
    verify(torrentFileProvider, times(1)).moveToArchiveFolder(eq(infoHash2));
}

@SuppressWarnings("unchecked")
@Test
public void shouldDeleteTorrentIfNoMorePeersOnlyIfConfigValueShouldKeepInactiveTorrentIsFalse() {
    final AppConfiguration appConfiguration = this.createMockedConf();
    doReturn(false).when(appConfiguration).shouldKeepTorrentWithZeroLeechers();
    final TorrentFileProvider torrentFileProvider = mock(TorrentFileProvider.class);
    final Client client = (Client) ClientBuilder.builder().withAnnouncerFactory(mock(AnnouncerFactory.class)).withBandwidthDispatcher(mock(BandwidthDispatcher.class)).withAppConfiguration(appConfiguration).withTorrentFileProvider(torrentFileProvider).withEventPublisher(mock(ApplicationEventPublisher.class)).withDelayQueue(mock(DelayQueue.class)).build();
    final InfoHash infoHash = InfoHashTest.createOne("abcd");
    client.onNoMorePeers(infoHash);
    verify(torrentFileProvider, times(1)).moveToArchiveFolder(eq(infoHash));
    final InfoHash infoHash2 = InfoHashTest.createOne("abcd");
    doReturn(true).when(appConfiguration).shouldKeepTorrentWithZeroLeechers();
    client.onNoMorePeers(infoHash);
    verify(torrentFileProvider, times(1)).moveToArchiveFolder(eq(infoHash2));
}

public void onNoMorePeers(final InfoHash infoHash) {
    if (!this.appConfiguration.isKeepTorrentWithZeroLeechers()) {
        this.torrentFileProvider.moveToArchiveFolder(infoHash);
    }
}

public void onNoMorePeers(final InfoHash infoHash) {
    if (!this.appConfiguration.shouldKeepTorrentWithZeroLeechers()) {
        this.torrentFileProvider.moveToArchiveFolder(infoHash);
    }
}


@Test
public void shouldSaveConfiguration() {
    final SeedManager seedManager = mock(SeedManager.class);
    final JoalMessageSendingTemplate sendingTemplate = mock(JoalMessageSendingTemplate.class);
    final WebSocketController controller = new WebSocketController(seedManager, sendingTemplate);
    final AppConfiguration config = AppConfigurationTest.createOne();
    final ConfigIncomingMessage configIncomingMessage = mock(ConfigIncomingMessage.class);
    doReturn(config.getMinUploadRate()).when(configIncomingMessage).getMinUploadRate();
    doReturn(config.getMaxUploadRate()).when(configIncomingMessage).getMaxUploadRate();
    doReturn(config.getSimultaneousSeed()).when(configIncomingMessage).getSimultaneousSeed();
    doReturn(config.getClient()).when(configIncomingMessage).getClient();
    doReturn(config.isKeepTorrentWithZeroLeechers()).when(configIncomingMessage).isKeepTorrentWithZeroLeechers();
    doReturn(config).when(configIncomingMessage).toAppConfiguration();
    controller.saveNewConf(configIncomingMessage);
    Mockito.verify(seedManager, times(1)).saveNewConfiguration(eq(config));
}

@Test
public void shouldSaveConfiguration() {
    final SeedManager seedManager = mock(SeedManager.class);
    final JoalMessageSendingTemplate sendingTemplate = mock(JoalMessageSendingTemplate.class);
    final WebSocketController controller = new WebSocketController(seedManager, sendingTemplate);
    final AppConfiguration config = AppConfigurationTest.createOne();
    final ConfigIncomingMessage configIncomingMessage = mock(ConfigIncomingMessage.class);
    doReturn(config.getMinUploadRate()).when(configIncomingMessage).getMinUploadRate();
    doReturn(config.getMaxUploadRate()).when(configIncomingMessage).getMaxUploadRate();
    doReturn(config.getSimultaneousSeed()).when(configIncomingMessage).getSimultaneousSeed();
    doReturn(config.getClientFileName()).when(configIncomingMessage).getClient();
    doReturn(config.shouldKeepTorrentWithZeroLeechers()).when(configIncomingMessage).shouldKeepTorrentWithZeroLeechers();
    doReturn(config).when(configIncomingMessage).toAppConfiguration();
    controller.saveNewConf(configIncomingMessage);
    Mockito.verify(seedManager, times(1)).saveNewConfiguration(eq(config));
}

@MessageMapping("/config/save")
public void saveNewConf(final ConfigIncomingMessage message) {
    if (log.isDebugEnabled()) {
        log.debug("Client ask to save new conf {}", message.toString());
    }
    try {
        seedManager.saveNewConfiguration(message.toAppConfiguration());
    } catch (final Exception e) {
        log.warn("Failed to save conf {}", message.toString(), e);
        messageSendingTemplate.convertAndSend("/config", new InvalidConfigPayload(e));
    }
}

@MessageMapping("/config/save")
public void saveNewConf(final ConfigIncomingMessage message) {
    if (logger.isDebugEnabled()) {
        logger.debug("Client ask to save new conf {}", message.toString());
    }
    try {
        seedManager.saveNewConfiguration(message.toAppConfiguration());
    } catch (final Exception e) {
        logger.warn("Failed to save conf {}", message.toString(), e);
        messageSendingTemplate.convertAndSend("/config", new InvalidConfigPayload(e));
    }
}


@Test
public void shouldCreate() {
    final AnnounceDataAccessor announceDataAccessor = mock(AnnounceDataAccessor.class);
    final AnnouncerFactory announcerFactory = new AnnouncerFactory(announceDataAccessor, Mockito.mock(HttpClient.class), mock(AppConfiguration.class));
    final MockedTorrent torrent = mock(MockedTorrent.class);
    given(torrent.getAnnounceList()).willReturn(list(list(URI.create("http://localhost"))));
    final Announcer announcer = announcerFactory.create(torrent);
    assertThat(announcer.getTorrent()).isEqualTo(torrent);
}

@Test
public void shouldCreate() {
    final AnnounceDataAccessor announceDataAccessor = mock(AnnounceDataAccessor.class);
    final AnnouncerFactory announcerFactory = new AnnouncerFactory(announceDataAccessor, Mockito.mock(HttpClient.class));
    final MockedTorrent torrent = mock(MockedTorrent.class);
    given(torrent.getAnnounceList()).willReturn(list(list(URI.create("http://localhost"))));
    final Announcer announcer = announcerFactory.create(torrent);
    assertThat(announcer.getTorrent()).isEqualTo(torrent);
}

public Announcer create(final MockedTorrent torrent) {
    return new Announcer(torrent, this.announceDataAccessor, httpClient, appConfiguration.getUploadRatioTarget());
}

public Announcer create(final MockedTorrent torrent) {
    return new Announcer(torrent, this.announceDataAccessor, httpClient);
}


@Test
public void shouldThrowTooManyFailsExceptionIfFailsFiveTimesInARow() throws AnnounceException {
    final MockedTorrent torrent = MockedTorrentTest.createOneMock("abcd");
    final TrackerClient trackerClient = mock(TrackerClient.class);
    doThrow(new AnnounceException("yeah ! :)")).when(trackerClient).announce(anyString(), any());
    final AnnounceDataAccessor dataAccessor = mock(AnnounceDataAccessor.class);
    doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
    doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
    final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
    announcer.setTrackerClient(trackerClient);
    for (int i = 0; i < 4; ++i) {
        try {
            announcer.announce(RequestEvent.STARTED);
            fail("Should have thrown AnnounceException");
        } catch (final AnnounceException ignore) {
        } catch (final TooManyAnnouncesFailedInARowException e) {
            fail("should not have thrown TooManyAnnouncesFailedInARowException already");
        }
    }
    assertThatThrownBy(() -> announcer.announce(RequestEvent.STARTED)).isInstanceOf(TooManyAnnouncesFailedInARowException.class);
}

@Test
public void shouldThrowTooManyFailsExceptionIfFailsFiveTimesInARow() throws AnnounceException {
    final MockedTorrent torrent = MockedTorrentTest.createOneMock("abcd");
    final TrackerClient trackerClient = mock(TrackerClient.class);
    doThrow(new AnnounceException("yeah ! :)")).when(trackerClient).announce(anyString(), any());
    final AnnounceDataAccessor dataAccessor = mock(AnnounceDataAccessor.class);
    doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
    doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
    final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
    announcer.setTrackerClient(trackerClient);
    for (int i = 0; i < 4; ++i) {
        try {
            announcer.announce(RequestEvent.STARTED);
            fail("Should have thrown AnnounceException");
        } catch (final AnnounceException ignore) {
        } catch (final TooManyAnnouncesFailedInARowException e) {
            fail("should not have thrown TooManyAnnouncesFailedInARowException already");
        }
    }
    assertThatThrownBy(() -> announcer.announce(RequestEvent.STARTED)).isInstanceOf(TooManyAnnouncesFailedInARowException.class);
}

public SuccessAnnounceResponse announce(final RequestEvent event) throws AnnounceException, TooManyAnnouncesFailedInARowException {
    log.debug("Attempt to announce {} for {}", event.getEventName(), this.torrent.getTorrentInfoHash().getHumanReadable());
    try {
        this.lastAnnouncedAt = LocalDateTime.now();
        final SuccessAnnounceResponse responseMessage = this.trackerClient.announce(this.announceDataAccessor.getHttpRequestQueryForTorrent(this.torrent.getTorrentInfoHash(), event), this.announceDataAccessor.getHttpHeadersForTorrent());
        log.info("{} has announced successfully. Response: {} seeders, {} leechers, {}s interval", this.torrent.getTorrentInfoHash().getHumanReadable(), responseMessage.getSeeders(), responseMessage.getLeechers(), responseMessage.getInterval());
        this.reportedUploadBytes = announceDataAccessor.getUploaded(this.torrent.getTorrentInfoHash());
        this.lastKnownInterval = responseMessage.getInterval();
        this.lastKnownLeechers = responseMessage.getLeechers();
        this.lastKnownSeeders = responseMessage.getSeeders();
        this.consecutiveFails = 0;
        return responseMessage;
    } catch (final Exception e) {
        this.consecutiveFails++;
        if (this.consecutiveFails >= 5) {
            log.warn("[{}] has failed to announce {} times in a row", this.torrent.getTorrentInfoHash().getHumanReadable(), this.consecutiveFails);
            throw new TooManyAnnouncesFailedInARowException(torrent);
        } else {
            log.info("[{}] has failed to announce {}. time", this.torrent.getTorrentInfoHash().getHumanReadable(), this.consecutiveFails);
        }
        throw e;
    }
}

public SuccessAnnounceResponse announce(final RequestEvent event) throws AnnounceException, TooManyAnnouncesFailedInARowException {
    log.debug("Attempt to announce {} for {}", event.getEventName(), this.torrent.getTorrentInfoHash().getHumanReadable());
    try {
        this.lastAnnouncedAt = LocalDateTime.now();
        final SuccessAnnounceResponse responseMessage = this.trackerClient.announce(this.announceDataAccessor.getHttpRequestQueryForTorrent(this.torrent.getTorrentInfoHash(), event), this.announceDataAccessor.getHttpHeadersForTorrent());
        log.info("{} has announced successfully. Response: {} seeders, {} leechers, {}s interval", this.torrent.getTorrentInfoHash().getHumanReadable(), responseMessage.getSeeders(), responseMessage.getLeechers(), responseMessage.getInterval());
        this.lastKnownInterval = responseMessage.getInterval();
        this.lastKnownLeechers = responseMessage.getLeechers();
        this.lastKnownSeeders = responseMessage.getSeeders();
        this.consecutiveFails = 0;
        return responseMessage;
    } catch (final Exception e) {
        this.consecutiveFails++;
        if (this.consecutiveFails >= 5) {
            log.warn("[{}] has failed to announce {} times in a row", this.torrent.getTorrentInfoHash().getHumanReadable(), this.consecutiveFails);
            throw new TooManyAnnouncesFailedInARowException(torrent);
        } else {
            log.info("[{}] has failed to announce {}. time", this.torrent.getTorrentInfoHash().getHumanReadable(), this.consecutiveFails);
        }
        throw e;
    }
}

@Test
public void shouldUpdateAnnounceDateOnEachAnnounce() throws AnnounceException, InterruptedException {
    final MockedTorrent torrent = MockedTorrentTest.createOneMock("abcd");
    final TrackerClient trackerClient = mock(TrackerClient.class);
    doThrow(new AnnounceException("yeah ! :)")).doReturn(SuccessAnnounceResponseTest.createOne()).when(trackerClient).announce(anyString(), any());
    final AnnounceDataAccessor dataAccessor = mock(AnnounceDataAccessor.class);
    doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
    doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
    final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
    announcer.setTrackerClient(trackerClient);
    assertThat(announcer.getLastAnnouncedAt()).isEmpty();
    try {
        announcer.announce(RequestEvent.STARTED);
        fail("should have failed");
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
    }
    final LocalDateTime lastDateTime = announcer.getLastAnnouncedAt().get();
    assertThat(lastDateTime).isNotNull();
    Thread.sleep(50);
    try {
        announcer.announce(RequestEvent.STARTED);
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
        fail("should not have failed");
    }
    assertThat(announcer.getLastAnnouncedAt().get()).isAfter(lastDateTime);
}

@Test
public void shouldUpdateAnnounceDateOnEachAnnounce() throws AnnounceException, InterruptedException {
    final MockedTorrent torrent = MockedTorrentTest.createOneMock("abcd");
    final TrackerClient trackerClient = mock(TrackerClient.class);
    doThrow(new AnnounceException("yeah ! :)")).doReturn(SuccessAnnounceResponseTest.createOne()).when(trackerClient).announce(anyString(), any());
    final AnnounceDataAccessor dataAccessor = mock(AnnounceDataAccessor.class);
    doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
    doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
    final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
    announcer.setTrackerClient(trackerClient);
    assertThat(announcer.getLastAnnouncedAt()).isEmpty();
    try {
        announcer.announce(RequestEvent.STARTED);
        fail("should have failed");
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
    }
    final LocalDateTime lastDateTime = announcer.getLastAnnouncedAt().get();
    assertThat(lastDateTime).isNotNull();
    Thread.sleep(50);
    try {
        announcer.announce(RequestEvent.STARTED);
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
        fail("should not have failed");
    }
    assertThat(announcer.getLastAnnouncedAt().get()).isAfter(lastDateTime);
}

@Test
public void shouldUpdateLastPeersStatsAndIntevalOnEachAnnounce() throws AnnounceException {
    final MockedTorrent torrent = MockedTorrentTest.createOneMock("abcd");
    final TrackerClient trackerClient = mock(TrackerClient.class);
    doReturn(new SuccessAnnounceResponse(1800, 164, 12)).doThrow(new AnnounceException("yeah ! :)")).doReturn(new SuccessAnnounceResponse(900, 180, 30)).when(trackerClient).announce(anyString(), any());
    final AnnounceDataAccessor dataAccessor = mock(AnnounceDataAccessor.class);
    doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
    doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
    final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
    announcer.setTrackerClient(trackerClient);
    assertThat(announcer.getLastKnownInterval()).isEqualTo(5);
    assertThat(announcer.getLastKnownSeeders()).isEmpty();
    assertThat(announcer.getLastKnownLeechers()).isEmpty();
    try {
        announcer.announce(RequestEvent.STARTED);
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
        fail("should not have failed");
    }
    assertThat(announcer.getLastKnownInterval()).isEqualTo(1800);
    assertThat(announcer.getLastKnownSeeders()).contains(164);
    assertThat(announcer.getLastKnownLeechers()).contains(12);
    try {
        announcer.announce(RequestEvent.STARTED);
        fail("should have failed");
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
    }
    assertThat(announcer.getLastKnownInterval()).isEqualTo(1800);
    assertThat(announcer.getLastKnownSeeders()).contains(164);
    assertThat(announcer.getLastKnownLeechers()).contains(12);
    try {
        announcer.announce(RequestEvent.STARTED);
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
        fail("should not have failed");
    }
    assertThat(announcer.getLastKnownInterval()).isEqualTo(900);
    assertThat(announcer.getLastKnownSeeders()).contains(180);
    assertThat(announcer.getLastKnownLeechers()).contains(30);
}

@Test
public void shouldUpdateLastPeersStatsAndIntevalOnEachAnnounce() throws AnnounceException {
    final MockedTorrent torrent = MockedTorrentTest.createOneMock("abcd");
    final TrackerClient trackerClient = mock(TrackerClient.class);
    doReturn(new SuccessAnnounceResponse(1800, 164, 12)).doThrow(new AnnounceException("yeah ! :)")).doReturn(new SuccessAnnounceResponse(900, 180, 30)).when(trackerClient).announce(anyString(), any());
    final AnnounceDataAccessor dataAccessor = mock(AnnounceDataAccessor.class);
    doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
    doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
    final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
    announcer.setTrackerClient(trackerClient);
    assertThat(announcer.getLastKnownInterval()).isEqualTo(5);
    assertThat(announcer.getLastKnownSeeders()).isEmpty();
    assertThat(announcer.getLastKnownLeechers()).isEmpty();
    try {
        announcer.announce(RequestEvent.STARTED);
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
        fail("should not have failed");
    }
    assertThat(announcer.getLastKnownInterval()).isEqualTo(1800);
    assertThat(announcer.getLastKnownSeeders()).contains(164);
    assertThat(announcer.getLastKnownLeechers()).contains(12);
    try {
        announcer.announce(RequestEvent.STARTED);
        fail("should have failed");
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
    }
    assertThat(announcer.getLastKnownInterval()).isEqualTo(1800);
    assertThat(announcer.getLastKnownSeeders()).contains(164);
    assertThat(announcer.getLastKnownLeechers()).contains(12);
    try {
        announcer.announce(RequestEvent.STARTED);
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException ignore) {
        fail("should not have failed");
    }
    assertThat(announcer.getLastKnownInterval()).isEqualTo(900);
    assertThat(announcer.getLastKnownSeeders()).contains(180);
    assertThat(announcer.getLastKnownLeechers()).contains(30);
}

@Test
public void shouldResetConsecutiveFailsOnAnnounceSuccess() throws AnnounceException {
    final MockedTorrent torrent = MockedTorrentTest.createOneMock("abcd");
    final TrackerClient trackerClient = mock(TrackerClient.class);
    doThrow(new AnnounceException("yeah ! :)")).doThrow(new AnnounceException("yeah ! :)")).doThrow(new AnnounceException("yeah ! :)")).doThrow(new AnnounceException("yeah ! :)")).doReturn(SuccessAnnounceResponseTest.createOne()).when(trackerClient).announce(anyString(), any());
    final AnnounceDataAccessor dataAccessor = mock(AnnounceDataAccessor.class);
    doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
    doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
    final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class), 1f);
    announcer.setTrackerClient(trackerClient);
    for (int i = 0; i < 4; ++i) {
        try {
            announcer.announce(RequestEvent.STARTED);
            fail("Should have thrown AnnounceException");
        } catch (final AnnounceException ignore) {
        } catch (final TooManyAnnouncesFailedInARowException e) {
            fail("should not have thrown TooManyAnnouncesFailedInARowException already");
        }
    }
    assertThat(announcer.getConsecutiveFails()).isEqualTo(4);
    try {
        announcer.announce(RequestEvent.STARTED);
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException e) {
        fail("should not have failed");
    }
    assertThat(announcer.getConsecutiveFails()).isEqualTo(0);
}

@Test
public void shouldResetConsecutiveFailsOnAnnounceSuccess() throws AnnounceException {
    final MockedTorrent torrent = MockedTorrentTest.createOneMock("abcd");
    final TrackerClient trackerClient = mock(TrackerClient.class);
    doThrow(new AnnounceException("yeah ! :)")).doThrow(new AnnounceException("yeah ! :)")).doThrow(new AnnounceException("yeah ! :)")).doThrow(new AnnounceException("yeah ! :)")).doReturn(SuccessAnnounceResponseTest.createOne()).when(trackerClient).announce(anyString(), any());
    final AnnounceDataAccessor dataAccessor = mock(AnnounceDataAccessor.class);
    doReturn("dd=ff&qq=d").when(dataAccessor).getHttpRequestQueryForTorrent(any(InfoHash.class), eq(RequestEvent.STARTED));
    doReturn(new HashSet<>()).when(dataAccessor).getHttpHeadersForTorrent();
    final Announcer announcer = new Announcer(torrent, dataAccessor, Mockito.mock(HttpClient.class));
    announcer.setTrackerClient(trackerClient);
    for (int i = 0; i < 4; ++i) {
        try {
            announcer.announce(RequestEvent.STARTED);
            fail("Should have thrown AnnounceException");
        } catch (final AnnounceException ignore) {
        } catch (final TooManyAnnouncesFailedInARowException e) {
            fail("should not have thrown TooManyAnnouncesFailedInARowException already");
        }
    }
    assertThat(announcer.getConsecutiveFails()).isEqualTo(4);
    try {
        announcer.announce(RequestEvent.STARTED);
    } catch (final TooManyAnnouncesFailedInARowException | AnnounceException e) {
        fail("should not have failed");
    }
    assertThat(announcer.getConsecutiveFails()).isEqualTo(0);
}


@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldRemoveUnregisteredTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getInBytesPerSeconds();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    bandwidthDispatcher.start();
    Thread.sleep(20);
    final long uploadedBeforeUnregistering = bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded();
    assertThat(uploadedBeforeUnregistering).isGreaterThan(1);
    bandwidthDispatcher.unregisterTorrent(infoHash);
    assertThat(bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded()).isEqualTo(0);
    bandwidthDispatcher.stop();
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldRemoveUnregisteredTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000_000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    bandwidthDispatcher.start();
    Thread.sleep(20);
    final long uploadedBeforeUnregistering = bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded();
    assertThat(uploadedBeforeUnregistering).isGreaterThan(1);
    bandwidthDispatcher.unregisterTorrent(infoHash);
    assertThat(bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded()).isEqualTo(0);
    bandwidthDispatcher.stop();
}

public void unregisterTorrent(final InfoHash infoHash) {
    if (logger.isDebugEnabled()) {
        logger.debug("{} has been removed from bandwidth dispatcher.", infoHash.humanReadableValue());
    }
    this.lock.writeLock().lock();
    try {
        this.weightHolder.remove(infoHash);
        this.torrentsSeedStats.remove(infoHash);
        this.speedMap.remove(infoHash);
        this.recomputeSpeeds();
    } finally {
        this.lock.writeLock().unlock();
    }
}

public void unregisterTorrent(final InfoHash infoHash) {
    log.debug("{} has been removed from bandwidth dispatcher.", infoHash.getHumanReadable());
    this.lock.writeLock().lock();
    try {
        this.weightHolder.remove(infoHash);
        this.torrentsSeedStats.remove(infoHash);
        this.speedMap.remove(infoHash);
        this.recomputeSpeeds();
    } finally {
        this.lock.writeLock().unlock();
    }
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldBeSafeToUpdateTorrentSeedsStatsWhileRegisteringTorrents() throws ExecutionException, InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getInBytesPerSeconds();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(1, speedProvider);
    bandwidthDispatcher.start();
    final ExecutorService executorService = Executors.newFixedThreadPool(5);
    final Collection<Future<?>> futures = new ArrayList<>();
    for (int i = 0; i < 25; ++i) {
        final InfoHash infoHash = new InfoHash((i + "").getBytes());
        futures.add(executorService.submit(() -> {
            bandwidthDispatcher.registerTorrent(infoHash);
            bandwidthDispatcher.updateTorrentPeers(infoHash, 20, 50);
        }));
    }
    for (final Future<?> future : futures) {
        future.get();
    }
    bandwidthDispatcher.stop();
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldBeSafeToUpdateTorrentSeedsStatsWhileRegisteringTorrents() throws ExecutionException, InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000_000L).when(speedProvider).getCurrentSpeed();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(1, speedProvider);
    bandwidthDispatcher.start();
    final ExecutorService executorService = Executors.newFixedThreadPool(5);
    final Collection<Future<?>> futures = new ArrayList<>();
    for (int i = 0; i < 25; ++i) {
        final InfoHash infoHash = new InfoHash((String.valueOf(i)).getBytes());
        futures.add(executorService.submit(() -> {
            bandwidthDispatcher.registerTorrent(infoHash);
            bandwidthDispatcher.updateTorrentPeers(infoHash, 20, 50);
        }));
    }
    for (final Future<?> future : futures) {
        future.get();
    }
    bandwidthDispatcher.stop();
}

public void registerTorrent(final InfoHash infoHash) {
    if (logger.isDebugEnabled()) {
        logger.debug("{} has been added to bandwidth dispatcher.", infoHash.humanReadableValue());
    }
    this.lock.writeLock().lock();
    try {
        this.torrentsSeedStats.put(infoHash, new TorrentSeedStats());
        this.speedMap.put(infoHash, new Speed(0));
    } finally {
        this.lock.writeLock().unlock();
    }
}

public void registerTorrent(final InfoHash infoHash) {
    log.debug("{} has been added to bandwidth dispatcher.", infoHash.getHumanReadable());
    this.lock.writeLock().lock();
    try {
        this.torrentsSeedStats.put(infoHash, new TorrentSeedStats());
        this.speedMap.put(infoHash, new Speed(0));
    } finally {
        this.lock.writeLock().unlock();
    }
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldRefreshRandomSpeedProviderAndRecomputeSpeed() {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getInBytesPerSeconds();
    final BandwidthDispatcher bandwidthDispatcher = spy(new BandwidthDispatcher(2, speedProvider));
    bandwidthDispatcher.refreshCurrentBandwidth();
    Mockito.verify(speedProvider, Mockito.times(1)).refresh();
    Mockito.verify(bandwidthDispatcher, times(1)).recomputeSpeeds();
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldRefreshRandomSpeedProviderAndRecomputeSpeed() {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000_000L).when(speedProvider).getCurrentSpeed();
    final BandwidthDispatcher bandwidthDispatcher = spy(new BandwidthDispatcher(2, speedProvider));
    bandwidthDispatcher.refreshCurrentBandwidth();
    Mockito.verify(speedProvider, Mockito.times(1)).refresh();
    Mockito.verify(bandwidthDispatcher, times(1)).recomputeSpeeds();
}

@VisibleForTesting
void refreshCurrentBandwidth() {
    if (logger.isDebugEnabled()) {
        logger.debug("Refreshing global bandwidth");
    }
    this.lock.writeLock().lock();
    try {
        this.randomSpeedProvider.refresh();
        this.recomputeSpeeds();
        if (logger.isDebugEnabled()) {
            logger.debug("Global bandwidth refreshed, new value is {}", FileUtils.byteCountToDisplaySize(this.randomSpeedProvider.getInBytesPerSeconds()));
        }
    } finally {
        this.lock.writeLock().unlock();
    }
}

@VisibleForTesting
void refreshCurrentBandwidth() {
    log.debug("Refreshing global bandwidth");
    this.lock.writeLock().lock();
    try {
        this.randomSpeedProvider.refresh();
        this.recomputeSpeeds();
        if (log.isDebugEnabled()) {
            log.debug("Global bandwidth refreshed, new value is {}", FileUtils.byteCountToDisplaySize(this.randomSpeedProvider.getCurrentSpeed()));
        }
    } finally {
        this.lock.writeLock().unlock();
    }
}

@SuppressWarnings({ "ResultOfMethodCallIgnored", "unchecked" })
@Test
public void shouldNotifyThatSpeedHasChangedAfterRegisteringTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(10000L).when(speedProvider).getInBytesPerSeconds();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.start();
    final SpeedChangedListener speedListener = spy(new VoidSpeedChangedListener());
    bandwidthDispatcher.setSpeedListener(speedListener);
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    Thread.sleep(30);
    bandwidthDispatcher.stop();
    Mockito.verify(speedListener, Mockito.times(1)).speedsHasChanged(anyMap());
}

@SuppressWarnings({ "ResultOfMethodCallIgnored", "unchecked" })
@Test
public void shouldNotifyThatSpeedHasChangedAfterRegisteringTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(10_000L).when(speedProvider).getCurrentSpeed();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.start();
    final SpeedChangedListener speedListener = spy(new VoidSpeedChangedListener());
    bandwidthDispatcher.setSpeedListener(speedListener);
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    Thread.sleep(30);
    bandwidthDispatcher.stop();
    Mockito.verify(speedListener, Mockito.times(1)).speedsHasChanged(anyMap());
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsWithZeroLeechers() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getInBytesPerSeconds();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 100, 0);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsWithZeroLeechers() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000_000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 100, 0);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsWithZeroSeeders() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getInBytesPerSeconds();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 0, 100);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsWithZeroSeeders() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000_000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 0, 100);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsBeforePeersHaveBeenAdded() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getInBytesPerSeconds();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsBeforePeersHaveBeenAdded() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000_000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldReturnZeroIfInfoHashIsNotRegistered() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getInBytesPerSeconds();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(new InfoHash(new byte[] { 12 }));
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldReturnZeroIfInfoHashIsNotRegistered() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000_000L).when(speedProvider).getCurrentSpeed();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(new InfoHash(new byte[] { 12 }));
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@Override
public void run() {
    try {
        while (!this.stop) {
            Thread.sleep(this.threadPauseInterval);
            ++this.threadLoopCounter;
            if (this.threadLoopCounter == 1200000 / this.threadPauseInterval) {
                this.refreshCurrentBandwidth();
                this.threadLoopCounter = 0;
            }
            this.lock.readLock().lock();
            final Set<Map.Entry<InfoHash, TorrentSeedStats>> entrySet = Sets.newHashSet(this.torrentsSeedStats.entrySet());
            this.lock.readLock().unlock();
            for (final Map.Entry<InfoHash, TorrentSeedStats> entry : entrySet) {
                final Speed speed = this.speedMap.get(entry.getKey());
                final long speedInBytesPerSecond = speed == null ? 0 : speed.getBytesPerSeconds();
                entry.getValue().addUploaded((speedInBytesPerSecond * this.threadPauseInterval) / 1000);
            }
        }
    } catch (final InterruptedException ignore) {
    }
}

@Override
public void run() {
    try {
        while (!this.stop) {
            Thread.sleep(this.threadPauseIntervalMs);
            ++this.threadLoopCounter;
            if (this.threadLoopCounter == TWENTY_MINS_MS / this.threadPauseIntervalMs) {
                this.refreshCurrentBandwidth();
                this.threadLoopCounter = 0;
            }
            this.lock.readLock().lock();
            final Set<Map.Entry<InfoHash, TorrentSeedStats>> entrySet = new HashSet<>(this.torrentsSeedStats.entrySet());
            this.lock.readLock().unlock();
            for (final Map.Entry<InfoHash, TorrentSeedStats> entry : entrySet) {
                final long speedInBytesPerSecond = ofNullable(this.speedMap.get(entry.getKey())).map(Speed::getBytesPerSecond).orElse(0L);
                entry.getValue().addUploaded((speedInBytesPerSecond * this.threadPauseIntervalMs) / 1000);
            }
        }
    } catch (final InterruptedException ignore) {
    }
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldIncrementRegisteredTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getInBytesPerSeconds();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final InfoHash infoHash2 = new InfoHash(new byte[] { 100 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    bandwidthDispatcher.registerTorrent(infoHash2);
    bandwidthDispatcher.updateTorrentPeers(infoHash2, 20, 30);
    bandwidthDispatcher.start();
    Thread.sleep(30);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    final TorrentSeedStats seedStats2 = bandwidthDispatcher.getSeedStatForTorrent(infoHash2);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getUploaded()).isGreaterThan(1);
    assertThat(seedStats2.getUploaded()).isGreaterThan(1);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldIncrementRegisteredTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000_000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final InfoHash infoHash2 = new InfoHash(new byte[] { 100 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    bandwidthDispatcher.registerTorrent(infoHash2);
    bandwidthDispatcher.updateTorrentPeers(infoHash2, 20, 30);
    bandwidthDispatcher.start();
    Thread.sleep(30);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    final TorrentSeedStats seedStats2 = bandwidthDispatcher.getSeedStatForTorrent(infoHash2);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getUploaded()).isGreaterThan(1);
    assertThat(seedStats2.getUploaded()).isGreaterThan(1);
}


@Test
public void shouldProvideValueWithinRange() {
    final RandomSpeedProvider speedProvider = new RandomSpeedProvider(this.mockedConf());
    for (int i = 0; i < 50; i++) {
        speedProvider.refresh();
        assertThat(speedProvider.getInBytesPerSeconds() / 1000).isBetween(MIN_UPLOAD_RATE, MAX_UPLOAD_RATE);
    }
}

@Test
public void shouldProvideValueWithinRange() {
    final RandomSpeedProvider speedProvider = new RandomSpeedProvider(this.mockedConf());
    for (int i = 0; i < 50; i++) {
        speedProvider.refresh();
        assertThat(speedProvider.getCurrentSpeed() / 1000).isBetween(MIN_UPLOAD_RATE, MAX_UPLOAD_RATE);
    }
}

public void refresh() {
    final Long minUploadRateInBytes = appConfiguration.getMinUploadRate() * 1000L;
    final Long maxUploadRateInBytes = appConfiguration.getMaxUploadRate() * 1000L;
    this.currentSpeed = (minUploadRateInBytes.equals(maxUploadRateInBytes)) ? maxUploadRateInBytes : ThreadLocalRandom.current().nextLong(minUploadRateInBytes, maxUploadRateInBytes);
}

public void refresh() {
    final long minUploadRateInBytes = appConfiguration.getMinUploadRate() * 1000L;
    final long maxUploadRateInBytes = appConfiguration.getMaxUploadRate() * 1000L;
    this.currentSpeed = (minUploadRateInBytes == maxUploadRateInBytes) ? maxUploadRateInBytes : ThreadLocalRandom.current().nextLong(minUploadRateInBytes, maxUploadRateInBytes);
}


@Test
public void shouldNotFailToDeserializeIfKeyGeneratorIsNotDefined() throws IOException {
    final String jsonWithoutKeyGenerator = validJSON.replaceAll("\"keyGenerator\":[ ]*\\{.*?algorithm\":[ ]*\\{.*?}.*?},", "").replaceAll("&key=\\{key}", "");
    final BitTorrentClientConfig bitTorrentClientConfig = mapper.readValue(jsonWithoutKeyGenerator, BitTorrentClientConfig.class);
    assertThat(bitTorrentClientConfig).isNotNull();
    assertThat(bitTorrentClientConfig.createClient().getKey(null, RequestEvent.STARTED)).isEmpty();
}

@Test
public void shouldNotFailToDeserializeIfKeyGeneratorIsNotDefined() throws IOException {
    final String jsonWithoutKeyGenerator = validJSON.replaceAll("\"keyGenerator\":[ ]*\\{.*?algorithm\":[ ]*\\{.*?}.*?},", "").replaceAll("&key=\\{key}", "");
    final BitTorrentClientConfig bitTorrentClientConfig = mapper.readValue(jsonWithoutKeyGenerator, BitTorrentClientConfig.class);
    assertThat(bitTorrentClientConfig).isNotNull();
    assertThat(provider.createClient(bitTorrentClientConfig).getKey(null, RequestEvent.STARTED)).isEmpty();
}

@VisibleForTesting
Optional<String> getKey(final InfoHash infoHash, final RequestEvent event) {
    if (keyGenerator == null) {
        return Optional.empty();
    }
    return Optional.of(keyGenerator.getKey(infoHash, event));
}

@VisibleForTesting
Optional<String> getKey(final InfoHash infoHash, final RequestEvent event) {
    return ofNullable(keyGenerator).map(keyGen -> keyGen.getKey(infoHash, event));
}

@Test
public void shouldNotFailToDeserializeIfRequestHeadersIsEmpty() throws IOException {
    final String jsonWithEmptyHeaders = validJSON.replaceAll("\"requestHeaders\":[ ]*\\[.*?\\]", "\"requestHeaders\":[]");
    final BitTorrentClientConfig bitTorrentClientConfig = mapper.readValue(jsonWithEmptyHeaders, BitTorrentClientConfig.class);
    assertThat(bitTorrentClientConfig).isNotNull();
    assertThat(bitTorrentClientConfig.createClient().getHeaders()).hasSize(0);
}

@Test
public void shouldNotFailToDeserializeIfRequestHeadersIsEmpty() throws IOException {
    final String jsonWithEmptyHeaders = validJSON.replaceAll("\"requestHeaders\":[ ]*\\[.*?\\]", "\"requestHeaders\":[]");
    final BitTorrentClientConfig bitTorrentClientConfig = mapper.readValue(jsonWithEmptyHeaders, BitTorrentClientConfig.class);
    assertThat(bitTorrentClientConfig).isNotNull();
    assertThat(provider.createClient(bitTorrentClientConfig).getHeaders()).hasSize(0);
}

@VisibleForTesting
public BitTorrentClient createClient() {
    return new BitTorrentClient(this.peerIdGenerator, this.keyGenerator, this.urlEncoder, query, ImmutableList.copyOf(requestHeaders), new NumwantProvider(this.numwant, this.numwantOnStop));
}


@Test
public void shouldBuildEvenIfKeyGeneratorIsNotDefined() {
    final String query = "info_hash={infohash}&peer_id={peerid}&supportcrypto=1&port={port}&azudp={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&event={event}&numwant={numwant}&no_peer_id=1&compact=1&azver=3";
    final List<BitTorrentClientConfig.HttpHeader> requestHeaders = Arrays.asList(new HttpHeader("User-Agent", "Azureus 5.7.5.0;{os};1.8.0_66"), new HttpHeader("Connection", "close"), new HttpHeader("Accept-Encoding", "gzip"), new HttpHeader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
    final BitTorrentClientConfig config = new BitTorrentClientConfig(defaultPeerIdGenerator, query, null, defaultUrlEncoder, requestHeaders, 200, 0);
    assertThat(config.createClient().getKey(Mockito.mock(InfoHash.class), RequestEvent.STARTED)).isEmpty();
}

@Test
public void shouldBuildEvenIfKeyGeneratorIsNotDefined() {
    final String query = "info_hash={infohash}&peer_id={peerid}&supportcrypto=1&port={port}&azudp={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&event={event}&numwant={numwant}&no_peer_id=1&compact=1&azver=3";
    final List<BitTorrentClientConfig.HttpHeader> requestHeaders = Arrays.asList(new HttpHeader("User-Agent", "Azureus 5.7.5.0;{os};1.8.0_66"), new HttpHeader("Connection", "close"), new HttpHeader("Accept-Encoding", "gzip"), new HttpHeader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
    final BitTorrentClientConfig config = new BitTorrentClientConfig(defaultPeerIdGenerator, query, null, defaultUrlEncoder, requestHeaders, 200, 0);
    assertThat(provider.createClient(config).getKey(mock(InfoHash.class), RequestEvent.STARTED)).isEmpty();
}

@Test
public void shouldBuildClient() {
    final String query = "info_hash={infohash}&peer_id={peerid}&supportcrypto=1&port={port}&azudp={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&event={event}&numwant={numwant}&no_peer_id=1&compact=1&key={key}&azver=3";
    final List<BitTorrentClientConfig.HttpHeader> requestHeaders = Arrays.asList(new HttpHeader("User-Agent", "Azureus 5.7.5.0;{os};1.8.0_66"), new HttpHeader("Connection", "close"), new HttpHeader("Accept-Encoding", "gzip"), new HttpHeader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
    final PeerIdGenerator peerIdGenerator = PeerIdGeneratorTest.createDefault();
    final KeyGenerator keyGenerator = KeyGeneratorTest.createDefault();
    final BitTorrentClientConfig config = new BitTorrentClientConfig(peerIdGenerator, query, keyGenerator, defaultUrlEncoder, requestHeaders, 200, 0);
    final BitTorrentClient client = config.createClient();
    assertThat(client.getHeaders()).isEqualTo(requestHeaders.stream().map(header -> new AbstractMap.SimpleEntry<>(header.getName(), header.getValue())).collect(Collectors.toList()));
    assertThat(client.getQuery()).isEqualTo(query);
    assertThat(client.getKey(null, RequestEvent.STARTED).get()).isEqualTo(keyGenerator.getKey(null, RequestEvent.STARTED));
    assertThat(client.getPeerId(null, RequestEvent.STARTED)).isEqualTo(peerIdGenerator.getPeerId(null, RequestEvent.STARTED));
    assertThat(client.getNumwant(RequestEvent.STARTED)).isEqualTo(200);
}

@Test
public void shouldBuildClient() {
    final String query = "info_hash={infohash}&peer_id={peerid}&supportcrypto=1&port={port}&azudp={port}&uploaded={uploaded}&downloaded={downloaded}&left={left}&corrupt=0&event={event}&numwant={numwant}&no_peer_id=1&compact=1&key={key}&azver=3";
    final List<BitTorrentClientConfig.HttpHeader> requestHeaders = Arrays.asList(new HttpHeader("User-Agent", "Azureus 5.7.5.0;{os};1.8.0_66"), new HttpHeader("Connection", "close"), new HttpHeader("Accept-Encoding", "gzip"), new HttpHeader("Accept", "text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2"));
    final PeerIdGenerator peerIdGenerator = PeerIdGeneratorTest.createDefault();
    final KeyGenerator keyGenerator = KeyGeneratorTest.createDefault();
    final BitTorrentClientConfig config = new BitTorrentClientConfig(peerIdGenerator, query, keyGenerator, defaultUrlEncoder, requestHeaders, 200, 0);
    final BitTorrentClient client = provider.createClient(config);
    assertThat(client.getHeaders()).isEqualTo(requestHeaders.stream().map(header -> new AbstractMap.SimpleEntry<>(header.getName(), header.getValue())).collect(Collectors.toList()));
    assertThat(client.getQuery()).isEqualTo(query);
    assertThat(client.getKey(null, RequestEvent.STARTED).get()).isEqualTo(keyGenerator.getKey(null, RequestEvent.STARTED));
    assertThat(client.getPeerId(null, RequestEvent.STARTED)).isEqualTo(peerIdGenerator.getPeerId(null, RequestEvent.STARTED));
    assertThat(client.getNumwant(RequestEvent.STARTED)).isEqualTo(200);
}


@SuppressWarnings("unchecked")
@Test
public void shouldDeleteTorrentIfNoMorePeersOnlyIfConfigValueShouldKeepInactiveTorrentIsFalse() {
    final AppConfiguration appConfiguration = this.createMockedConf();
    doReturn(false).when(appConfiguration).shouldKeepTorrentWithZeroLeechers();
    final TorrentFileProvider torrentFileProvider = mock(TorrentFileProvider.class);
    final Client client = (Client) ClientBuilder.builder().withAnnouncerFactory(mock(AnnouncerFactory.class)).withBandwidthDispatcher(mock(BandwidthDispatcher.class)).withAppConfiguration(appConfiguration).withTorrentFileProvider(torrentFileProvider).withEventPublisher(mock(ApplicationEventPublisher.class)).withDelayQueue(mock(DelayQueue.class)).build();
    final InfoHash infoHash = InfoHashTest.createOne("abcd");
    client.onNoMorePeers(infoHash);
    verify(torrentFileProvider, times(1)).moveToArchiveFolder(eq(infoHash));
    final InfoHash infoHash2 = InfoHashTest.createOne("abcd");
    doReturn(true).when(appConfiguration).shouldKeepTorrentWithZeroLeechers();
    client.onNoMorePeers(infoHash);
    verify(torrentFileProvider, times(1)).moveToArchiveFolder(eq(infoHash2));
}

@SuppressWarnings("unchecked")
@Test
public void shouldDeleteTorrentIfNoMorePeersOnlyIfConfigValueShouldKeepInactiveTorrentIsFalse() {
    final AppConfiguration appConfiguration = this.createMockedConf();
    doReturn(false).when(appConfiguration).isKeepTorrentWithZeroLeechers();
    final TorrentFileProvider torrentFileProvider = mock(TorrentFileProvider.class);
    final Client client = (Client) ClientBuilder.builder().withAnnouncerFactory(mock(AnnouncerFactory.class)).withBandwidthDispatcher(mock(BandwidthDispatcher.class)).withAppConfiguration(appConfiguration).withTorrentFileProvider(torrentFileProvider).withEventPublisher(mock(ApplicationEventPublisher.class)).withDelayQueue(mock(DelayQueue.class)).build();
    final InfoHash infoHash = InfoHashTest.createOne("abcd");
    client.onNoMorePeers(infoHash);
    verify(torrentFileProvider, times(1)).moveToArchiveFolder(eq(infoHash));
    final InfoHash infoHash2 = InfoHashTest.createOne("abcd");
    doReturn(true).when(appConfiguration).isKeepTorrentWithZeroLeechers();
    client.onNoMorePeers(infoHash);
    verify(torrentFileProvider, times(1)).moveToArchiveFolder(eq(infoHash2));
}

public void onNoMorePeers(final InfoHash infoHash) {
    if (!this.appConfiguration.shouldKeepTorrentWithZeroLeechers()) {
        this.torrentFileProvider.moveToArchiveFolder(infoHash);
    }
}

public void onNoMorePeers(final InfoHash infoHash) {
    if (!this.appConfiguration.isKeepTorrentWithZeroLeechers()) {
        this.torrentFileProvider.moveToArchiveFolder(infoHash);
    }
}


@Test
public void shouldSaveConfiguration() {
    final SeedManager seedManager = mock(SeedManager.class);
    final JoalMessageSendingTemplate sendingTemplate = mock(JoalMessageSendingTemplate.class);
    final WebSocketController controller = new WebSocketController(seedManager, sendingTemplate);
    final AppConfiguration config = AppConfigurationTest.createOne();
    final ConfigIncomingMessage configIncomingMessage = mock(ConfigIncomingMessage.class);
    doReturn(config.getMinUploadRate()).when(configIncomingMessage).getMinUploadRate();
    doReturn(config.getMaxUploadRate()).when(configIncomingMessage).getMaxUploadRate();
    doReturn(config.getSimultaneousSeed()).when(configIncomingMessage).getSimultaneousSeed();
    doReturn(config.getClientFileName()).when(configIncomingMessage).getClient();
    doReturn(config.shouldKeepTorrentWithZeroLeechers()).when(configIncomingMessage).shouldKeepTorrentWithZeroLeechers();
    doReturn(config).when(configIncomingMessage).toAppConfiguration();
    controller.saveNewConf(configIncomingMessage);
    Mockito.verify(seedManager, times(1)).saveNewConfiguration(eq(config));
}

@Test
public void shouldSaveConfiguration() {
    final SeedManager seedManager = mock(SeedManager.class);
    final JoalMessageSendingTemplate sendingTemplate = mock(JoalMessageSendingTemplate.class);
    final WebSocketController controller = new WebSocketController(seedManager, sendingTemplate);
    final AppConfiguration config = AppConfigurationTest.createOne();
    final ConfigIncomingMessage configIncomingMessage = mock(ConfigIncomingMessage.class);
    doReturn(config.getMinUploadRate()).when(configIncomingMessage).getMinUploadRate();
    doReturn(config.getMaxUploadRate()).when(configIncomingMessage).getMaxUploadRate();
    doReturn(config.getSimultaneousSeed()).when(configIncomingMessage).getSimultaneousSeed();
    doReturn(config.getClient()).when(configIncomingMessage).getClient();
    doReturn(config.isKeepTorrentWithZeroLeechers()).when(configIncomingMessage).isKeepTorrentWithZeroLeechers();
    doReturn(config).when(configIncomingMessage).toAppConfiguration();
    controller.saveNewConf(configIncomingMessage);
    Mockito.verify(seedManager, times(1)).saveNewConfiguration(eq(config));
}

@MessageMapping("/config/save")
public void saveNewConf(final ConfigIncomingMessage message) {
    if (logger.isDebugEnabled()) {
        logger.debug("Client ask to save new conf {}", message.toString());
    }
    try {
        seedManager.saveNewConfiguration(message.toAppConfiguration());
    } catch (final Exception e) {
        logger.warn("Failed to save conf {}", message.toString(), e);
        messageSendingTemplate.convertAndSend("/config", new InvalidConfigPayload(e));
    }
}

@MessageMapping("/config/save")
public void saveNewConf(final ConfigIncomingMessage message) {
    if (log.isDebugEnabled()) {
        log.debug("Client ask to save new conf {}", message.toString());
    }
    try {
        seedManager.saveNewConfiguration(message.toAppConfiguration());
    } catch (final Exception e) {
        log.warn("Failed to save conf {}", message.toString(), e);
        messageSendingTemplate.convertAndSend("/config", new InvalidConfigPayload(e));
    }
}


@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldRemoveUnregisteredTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    bandwidthDispatcher.start();
    Thread.sleep(20);
    final long uploadedBeforeUnregistering = bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded();
    assertThat(uploadedBeforeUnregistering).isGreaterThan(1);
    bandwidthDispatcher.unregisterTorrent(infoHash);
    assertThat(bandwidthDispatcher.getSeedStatForTorrent(infoHash).getUploaded()).isEqualTo(0);
    bandwidthDispatcher.stop();
}

public void unregisterTorrent(final InfoHash infoHash) {
    logger.debug("{} has been removed from bandwidth dispatcher.", infoHash.humanReadableValue());
    this.lock.writeLock().lock();
    try {
        this.weightHolder.remove(infoHash);
        this.torrentsSeedStats.remove(infoHash);
        this.speedMap.remove(infoHash);
        this.recomputeSpeeds();
    } finally {
        this.lock.writeLock().unlock();
    }
}

public void unregisterTorrent(final InfoHash infoHash) {
    logger.debug("{} has been removed from bandwidth dispatcher.", infoHash.getHumanReadable());
    this.lock.writeLock().lock();
    try {
        this.weightHolder.remove(infoHash);
        this.torrentsSeedStats.remove(infoHash);
        this.speedMap.remove(infoHash);
        this.recomputeSpeeds();
    } finally {
        this.lock.writeLock().unlock();
    }
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldBeSafeToUpdateTorrentSeedsStatsWhileRegisteringTorrents() throws ExecutionException, InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getCurrentSpeed();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(1, speedProvider);
    bandwidthDispatcher.start();
    final ExecutorService executorService = Executors.newFixedThreadPool(5);
    final Collection<Future<?>> futures = new ArrayList<>();
    for (int i = 0; i < 25; ++i) {
        final InfoHash infoHash = new InfoHash((i + "").getBytes());
        futures.add(executorService.submit(() -> {
            bandwidthDispatcher.registerTorrent(infoHash);
            bandwidthDispatcher.updateTorrentPeers(infoHash, 20, 50);
        }));
    }
    for (final Future<?> future : futures) {
        future.get();
    }
    bandwidthDispatcher.stop();
}

void setBytesPerSeconds(final long bytesPerSeconds) {
    this.bytesPerSeconds = bytesPerSeconds;
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldRefreshRandomSpeedProviderAndRecomputeSpeed() {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getCurrentSpeed();
    final BandwidthDispatcher bandwidthDispatcher = spy(new BandwidthDispatcher(2, speedProvider));
    bandwidthDispatcher.refreshCurrentBandwidth();
    Mockito.verify(speedProvider, Mockito.times(1)).refresh();
    Mockito.verify(bandwidthDispatcher, times(1)).recomputeSpeeds();
}

@VisibleForTesting
void refreshCurrentBandwidth() {
    logger.debug("Refreshing global bandwidth");
    this.lock.writeLock().lock();
    try {
        this.randomSpeedProvider.refresh();
        this.recomputeSpeeds();
        if (logger.isDebugEnabled()) {
            logger.debug("Global bandwidth refreshed, new value is {}", FileUtils.byteCountToDisplaySize(this.randomSpeedProvider.getInBytesPerSeconds()));
        }
    } finally {
        this.lock.writeLock().unlock();
    }
}

@VisibleForTesting
void refreshCurrentBandwidth() {
    logger.debug("Refreshing global bandwidth");
    this.lock.writeLock().lock();
    try {
        this.randomSpeedProvider.refresh();
        this.recomputeSpeeds();
        if (logger.isDebugEnabled()) {
            logger.debug("Global bandwidth refreshed, new value is {}", FileUtils.byteCountToDisplaySize(this.randomSpeedProvider.getCurrentSpeed()));
        }
    } finally {
        this.lock.writeLock().unlock();
    }
}

@SuppressWarnings({ "ResultOfMethodCallIgnored", "unchecked" })
@Test
public void shouldNotifyThatSpeedHasChangedAfterRegisteringTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(10000L).when(speedProvider).getCurrentSpeed();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.start();
    final SpeedChangedListener speedListener = spy(new VoidSpeedChangedListener());
    bandwidthDispatcher.setSpeedListener(speedListener);
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    Thread.sleep(30);
    bandwidthDispatcher.stop();
    Mockito.verify(speedListener, Mockito.times(1)).speedsHasChanged(anyMap());
}

public void registerTorrent(final InfoHash infoHash) {
    logger.debug("{} has been added to bandwidth dispatcher.", infoHash.humanReadableValue());
    this.lock.writeLock().lock();
    try {
        this.torrentsSeedStats.put(infoHash, new TorrentSeedStats());
        this.speedMap.put(infoHash, new Speed(0));
    } finally {
        this.lock.writeLock().unlock();
    }
}

public void registerTorrent(final InfoHash infoHash) {
    logger.debug("{} has been added to bandwidth dispatcher.", infoHash.getHumanReadable());
    this.lock.writeLock().lock();
    try {
        this.torrentsSeedStats.put(infoHash, new TorrentSeedStats());
        this.speedMap.put(infoHash, new Speed(0));
    } finally {
        this.lock.writeLock().unlock();
    }
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsWithZeroLeechers() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 100, 0);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsWithZeroSeeders() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 0, 100);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldNotIncrementRegisteredTorrentsBeforePeersHaveBeenAdded() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldReturnZeroIfInfoHashIsNotRegistered() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getCurrentSpeed();
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.start();
    Thread.sleep(10);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(new InfoHash(new byte[] { 12 }));
    bandwidthDispatcher.stop();
    assertThat(seedStats.getDownloaded()).isEqualTo(0);
    assertThat(seedStats.getUploaded()).isEqualTo(0);
    assertThat(seedStats.getLeft()).isEqualTo(0);
}

public long getLeft() {
    return 0L;
}

@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void shouldIncrementRegisteredTorrent() throws InterruptedException {
    final RandomSpeedProvider speedProvider = Mockito.mock(RandomSpeedProvider.class);
    Mockito.doReturn(1000000L).when(speedProvider).getCurrentSpeed();
    final InfoHash infoHash = new InfoHash(new byte[] { 12 });
    final InfoHash infoHash2 = new InfoHash(new byte[] { 100 });
    final BandwidthDispatcher bandwidthDispatcher = new BandwidthDispatcher(2, speedProvider);
    bandwidthDispatcher.registerTorrent(infoHash);
    bandwidthDispatcher.updateTorrentPeers(infoHash, 10, 10);
    bandwidthDispatcher.registerTorrent(infoHash2);
    bandwidthDispatcher.updateTorrentPeers(infoHash2, 20, 30);
    bandwidthDispatcher.start();
    Thread.sleep(30);
    final TorrentSeedStats seedStats = bandwidthDispatcher.getSeedStatForTorrent(infoHash);
    final TorrentSeedStats seedStats2 = bandwidthDispatcher.getSeedStatForTorrent(infoHash2);
    bandwidthDispatcher.stop();
    assertThat(seedStats.getUploaded()).isGreaterThan(1);
    assertThat(seedStats2.getUploaded()).isGreaterThan(1);
}


public long getInBytesPerSeconds() {
    return this.currentSpeed;
}


@JsonProperty("keepTorrentWithZeroLeechers")
public boolean shouldKeepTorrentWithZeroLeechers() {
    return keepTorrentWithZeroLeechers;
}


@Test
void testFiltering() {
    final int streamSize = 85;
    final List<Tile> tiles = new ArrayList<>();
    for (int z = 0; z <= 3; z++) {
        for (int x = 0; x < IntMath.pow(2, z); x++) {
            for (int y = 0; y < IntMath.pow(2, z); y++) {
                tiles.add(new Tile(x, y, z));
            }
        }
    }
    assertEquals(streamSize, tiles.size());
    final int batchArraySize = 5;
    for (int batchArrayIndex = 0; batchArrayIndex < batchArraySize; batchArrayIndex++) {
        List<Tile> batch = tiles.stream().filter(new TileBatchPredicate(batchArraySize, batchArrayIndex)).sorted(Comparator.comparingLong(Tile::index)).toList();
        assertEquals(streamSize / batchArraySize, batch.size());
        int tileIndex = batchArrayIndex;
        for (Tile tile : batch) {
            assertEquals(tileIndex, tile.index());
            tileIndex += batchArraySize;
        }
    }
}

@Test
void testFiltering() {
    final int streamSize = 85;
    final List<TileCoord> tileCoords = new ArrayList<>();
    for (int z = 0; z <= 3; z++) {
        for (int x = 0; x < IntMath.pow(2, z); x++) {
            for (int y = 0; y < IntMath.pow(2, z); y++) {
                tileCoords.add(new TileCoord(x, y, z));
            }
        }
    }
    assertEquals(streamSize, tileCoords.size());
    final int batchArraySize = 5;
    for (int batchArrayIndex = 0; batchArrayIndex < batchArraySize; batchArrayIndex++) {
        List<TileCoord> batch = tileCoords.stream().filter(new TileBatchPredicate(batchArraySize, batchArrayIndex)).sorted(Comparator.comparingLong(TileCoord::index)).toList();
        assertEquals(streamSize / batchArraySize, batch.size());
        int tileIndex = batchArrayIndex;
        for (TileCoord tileCoord : batch) {
            assertEquals(tileIndex, tileCoord.index());
            tileIndex += batchArraySize;
        }
    }
}

public long index() {
    long x = this.x;
    long y = this.y;
    long offset = offsets[z];
    long position = x + y * sides[z];
    return offset + position;
}


@Test
public void getByCurrency() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCurrency("EUR");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        Assert.assertTrue(country.getCurrency().contains("EUR"));
    }
}

@Test
public void getByCurrency() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCurrency("EUR");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        Assert.assertTrue(country.getCurrencies().contains("EUR"));
    }
}

public List<Country> getByCurrency(String currency) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        for (String curr : country.getCurrency()) {
            if (curr.toLowerCase().equals(currency.toLowerCase())) {
                result.add(country);
            }
        }
    }
    return result;
}

public List<Country> getByCurrency(String currency) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        for (String curr : country.getCurrencies()) {
            if (curr.toLowerCase().equals(currency.toLowerCase())) {
                result.add(country);
            }
        }
    }
    return result;
}

@Test
public void getByLanguageCode() throws Exception {
    List<Country> countries = CountryService.getInstance().getByLanguage("en");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        Assert.assertTrue(country.getLanguageCodes().contains("en"));
    }
}

@Test
public void getByLanguageCode() throws Exception {
    List<Country> countries = CountryService.getInstance().getByLanguage("en");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    for (Country country : countries) {
        Assert.assertTrue(country.getLanguages().contains("en"));
    }
}

public List<Country> getByLanguage(String language) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        for (String lang : country.getLanguageCodes()) {
            if (lang.toLowerCase().equals(language.toLowerCase())) {
                result.add(country);
            }
        }
    }
    return result;
}

public List<Country> getByLanguage(String language) {
    List<Country> result = new ArrayList<Country>();
    for (Country country : countries) {
        for (String lang : country.getLanguages()) {
            if (lang.toLowerCase().equals(language.toLowerCase())) {
                result.add(country);
            }
        }
    }
    return result;
}

@Test
public void getByCapital() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCapital("Tallinn");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(1, countries.size());
    Assert.assertEquals("EE", countries.get(0).getCca2());
    Assert.assertEquals("Eesti", countries.get(0).getNativeName());
}

@Test
public void getByCapital() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCapital("Tallinn");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(1, countries.size());
    Assert.assertEquals("EE", countries.get(0).getAlpha2Code());
    Assert.assertEquals("Eesti", countries.get(0).getNativeName());
}

public String getCca2() {
    return cca2;
}

@Test
public void getByCallingCode() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCallingcode("57");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(1, countries.size());
    Assert.assertEquals("CO", countries.get(0).getCca2());
}

@Test
public void getByCallingCode() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCallingcode("57");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(1, countries.size());
    Assert.assertEquals("CO", countries.get(0).getAlpha2Code());
}

public String getAlpha2Code() {
    return alpha2Code;
}

@Test
public void getByCodeList() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCodeList("CO;NOR;EE");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(3, countries.size());
    for (Country country : countries) {
        Assert.assertTrue(country.getCca2().equals("CO") || country.getCca2().equals("NO") || country.getCca2().equals("EE"));
    }
}

@Test
public void getByCodeList() throws Exception {
    List<Country> countries = CountryService.getInstance().getByCodeList("CO;NOR;EE");
    Assert.assertNotNull(countries);
    Assert.assertFalse(countries.isEmpty());
    Assert.assertEquals(3, countries.size());
    for (Country country : countries) {
        Assert.assertTrue(country.getAlpha2Code().equals("CO") || country.getAlpha2Code().equals("NO") || country.getAlpha2Code().equals("EE"));
    }
}

@Test
public void getByAlpha2() throws Exception {
    Country country = CountryService.getInstance().getByAlpha("CO");
    Assert.assertNotNull(country);
    Assert.assertEquals("CO", country.getCca2());
}

@Test
public void getByAlpha2() throws Exception {
    Country country = CountryService.getInstance().getByAlpha("CO");
    Assert.assertNotNull(country);
    Assert.assertEquals("CO", country.getAlpha2Code());
}

public Country getByAlpha(String alpha) {
    int alphaLength = alpha.length();
    for (Country country : countries) {
        if (alphaLength == 2) {
            if (country.getCca2().toLowerCase().equals(alpha.toLowerCase())) {
                return country;
            }
        } else if (alphaLength == 3) {
            if (country.getCca3().toLowerCase().equals(alpha.toLowerCase())) {
                return country;
            }
        }
    }
    return null;
}

public Country getByAlpha(String alpha) {
    int alphaLength = alpha.length();
    for (Country country : countries) {
        if (alphaLength == 2) {
            if (country.getAlpha2Code().toLowerCase().equals(alpha.toLowerCase())) {
                return country;
            }
        } else if (alphaLength == 3) {
            if (country.getAlpha3Code().toLowerCase().equals(alpha.toLowerCase())) {
                return country;
            }
        }
    }
    return null;
}

@Test
public void getByAlpha3() throws Exception {
    Country country = CountryService.getInstance().getByAlpha("COL");
    Assert.assertNotNull(country);
    Assert.assertEquals("COL", country.getCca3());
}

@Test
public void getByAlpha3() throws Exception {
    Country country = CountryService.getInstance().getByAlpha("COL");
    Assert.assertNotNull(country);
    Assert.assertEquals("COL", country.getAlpha3Code());
}


@Test
public void test() throws Exception {
    Logger.getLogger("io.netty").setLevel(Level.OFF);
    connectionSeed.connectionManager.startListening();
    for (int i = 6; i < 10; ++i) {
        System.out.println("------------------------------------------------------------------------------");
        System.out.println(i + " started up...");
        clients.get(i).connectionManager.startUp();
        Thread.sleep(500);
    }
    Thread.sleep(1000);
}

@Test
public void test() throws Exception {
    Logger.getLogger("io.netty").setLevel(Level.OFF);
    connectionSeed.connectionManager.startListening(new NullResultCommand());
    for (int i = 6; i < 10; ++i) {
        System.out.println("------------------------------------------------------------------------------");
        System.out.println(i + " started up...");
        clients.get(i).connectionManager.startUp(new NullResultCommand());
        Thread.sleep(500);
    }
    Thread.sleep(1000);
}

@Override
public void startUp() {
    new Thread(new Runnable() {

        @Override
        public void run() {
            try {
                startUpBlocking();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}

@Override
public void startUp(ResultCommand callback) {
    new Thread(new Runnable() {

        @Override
        public void run() {
            try {
                startUpBlocking(callback);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }).start();
}


@Test
public void shouldEncrypt() throws InvalidKeyException, NoSuchAlgorithmException {
    doHandShake();
    ECDHKeySet keySetBeforeEncryption1 = node1.ecdhKeySet.clone();
    RandomDataMessage testMessage = new RandomDataMessage();
    channel1.writeOutbound(testMessage);
    EncryptedMessage encryptedMessage = (EncryptedMessage) channel1.readOutbound();
    MessageEncrypter encrypter = new MessageEncrypterImpl(new MessageSerializerImpl());
    EncryptedMessage messageSelfEncrypted = encrypter.encrypt(testMessage, keySetBeforeEncryption1);
    assertTrue(Arrays.equals(messageSelfEncrypted.getEncryptedBytes(), encryptedMessage.getEncryptedBytes()));
    assertTrue(Arrays.equals(encryptedMessage.getHMAC(), encryptedMessage.getHMAC()));
}

@Test
public void shouldEncrypt() throws InvalidKeyException, NoSuchAlgorithmException {
    doHandShake();
    ECDHKeySet keySetBeforeEncryption1 = node1.ecdhKeySet.clone();
    RandomDataMessage testMessage = new RandomDataMessage();
    channel1.writeOutbound(testMessage);
    EncryptedMessage encryptedMessage = (EncryptedMessage) channel1.readOutbound();
    MessageEncrypter encrypter = new MessageEncrypterImpl(new MessageSerializerImpl());
    EncryptedMessage messageSelfEncrypted = encrypter.encrypt(testMessage, keySetBeforeEncryption1);
    assertTrue(Arrays.equals(messageSelfEncrypted.payload, encryptedMessage.payload));
    assertTrue(Arrays.equals(encryptedMessage.hmac, encryptedMessage.hmac));
}

@Override
public EncryptedMessage encrypt(Message data, ECDHKeySet keySet) {
    byte[] bytes = serializater.serializeMessage(data);
    byte[] enc = CryptoTools.encryptAES_CTR(bytes, keySet.encryptionKey, keySet.ivServer, keySet.counterOut);
    byte[] hmac = CryptoTools.getHMAC(enc, keySet.hmacKey);
    return new EncryptedMessageImpl(hmac, enc);
}

@Override
public EncryptedMessage encrypt(Message data, ECDHKeySet keySet) {
    byte[] bytes = serializater.serializeMessage(data);
    byte[] enc = CryptoTools.encryptAES_CTR(bytes, keySet.encryptionKey, keySet.ivServer, keySet.counterOut);
    byte[] hmac = CryptoTools.getHMAC(enc, keySet.hmacKey);
    return new EncryptedMessage(hmac, enc);
}


@Test
public void authenticationFail() throws NoSuchProviderException, NoSuchAlgorithmException {
    AuthenticationMessage authenticationMessage = (AuthenticationMessage) channel1.readOutbound();
    byte[] sig = authenticationMessage.getSignature();
    byte[] b = new byte[4];
    Random r = new Random();
    r.nextBytes(b);
    System.arraycopy(b, 0, sig, 10, 4);
    AuthenticationMessage falseMessage = new AuthenticationMessageMock(authenticationMessage.getPubkeyServer(), sig);
    channel2.writeInbound(falseMessage);
    Message failureMessage = (Message) channel2.readOutbound();
    assertTrue(failureMessage instanceof FailureMessage);
    assertEquals(((FailureMessage) failureMessage).getFailure(), "Signature does not match..");
}

@Test
public void authenticationFail() throws NoSuchProviderException, NoSuchAlgorithmException {
    AuthenticationMessage authenticationMessage = (AuthenticationMessage) channel1.readOutbound();
    byte[] sig = authenticationMessage.signature;
    byte[] b = new byte[4];
    Random r = new Random();
    r.nextBytes(b);
    System.arraycopy(b, 0, sig, 10, 4);
    AuthenticationMessage falseMessage = new AuthenticationMessage(authenticationMessage.pubKeyServer, sig);
    channel2.writeInbound(falseMessage);
    Message failureMessage = (Message) channel2.readOutbound();
    assertTrue(failureMessage instanceof FailureMessage);
    assertEquals(((FailureMessage) failureMessage).getFailure(), "Signature does not match..");
}

@Override
public byte[] getSignature() {
    return signature;
}


@Test
public void shouldSendDataToNextPeer() throws Exception {
    List<P2PDataObject> dataList = new ArrayList<>();
    for (int i = 0; i < GossipProcessor.OBJECT_AMOUNT_TO_SEND + 1; i++) {
        dataList.add(PubkeyChannelObject.getRandomObject());
    }
    GossipSendMessage sendDataObject = messageFactory.getGossipSendMessage(dataList);
    channel21.writeInbound(sendDataObject);
    channel32.writeInbound(channel23.readOutbound());
    channel23.writeInbound(channel32.readOutbound());
    channel32.writeInbound(channel23.readOutbound());
    channel43.writeInbound(channel34.readOutbound());
    channel34.writeInbound(channel43.readOutbound());
    channel43.writeInbound(channel34.readOutbound());
    assertTrue(checkListsEqual(dbHandler2.totalList, dataList));
    assertTrue(checkListsEqual(dbHandler3.totalList, dataList));
    assertTrue(checkListsEqual(dbHandler4.totalList, dataList));
}

@Test
public void shouldSendDataToNextPeer() throws Exception {
    List<P2PDataObject> dataList = new ArrayList<>();
    for (int i = 0; i < GossipProcessor.OBJECT_AMOUNT_TO_SEND + 1; i++) {
        dataList.add(PubkeyChannelObject.getRandomObject());
    }
    GossipSendMessage sendDataObject = messageFactory.getGossipSendMessage(dataList);
    channel21.writeInbound(sendDataObject);
    channel32.writeInbound(channel23.readOutbound());
    channel23.writeInbound(channel32.readOutbound());
    channel32.writeInbound(channel23.readOutbound());
    channel43.writeInbound(channel34.readOutbound());
    channel34.writeInbound(channel43.readOutbound());
    channel43.writeInbound(channel34.readOutbound());
    assertTrue(dbHandler2.totalList.containsAll(dataList));
    assertTrue(dbHandler3.totalList.containsAll(dataList));
    assertTrue(dbHandler4.totalList.containsAll(dataList));
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    try {
        checkIfMessage(msg);
        if (msg instanceof FailureMessage) {
            System.out.println("In Failure: " + ((FailureMessage) msg).getFailure());
        } else {
            System.out.println(layerName + " I: " + msg);
            Message message = (Message) msg;
            processor.onInboundMessage(message);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
    try {
        checkIfMessage(msg);
        if (msg instanceof FailureMessage) {
            System.out.println("In Failure: " + ((FailureMessage) msg).getFailure());
        } else {
            System.out.println(layerName + " I: " + msg);
            Message message = (Message) msg;
            message.verify();
            processor.onInboundMessage(message);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}