import datetime
import errno
import functools
import http.server
import logging
import os
import pathlib
import ssl
import sys
import tempfile
import threading
from typing import Any
from typing import Callable
from typing import cast
from typing import Dict
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
import unittest

import libtorrent as lt

from . import lib
from . import tdummy

if TYPE_CHECKING:
    from libtorrent import _Entry


class EnumTest(unittest.TestCase):
    def test_category_t(self) -> None:
        self.assertIsInstance(lt.alert.category_t.error_notification, int)
        self.assertIsInstance(lt.alert.category_t.peer_notification, int)
        self.assertIsInstance(lt.alert.category_t.port_mapping_notification, int)
        self.assertIsInstance(lt.alert.category_t.storage_notification, int)
        self.assertIsInstance(lt.alert.category_t.tracker_notification, int)
        self.assertIsInstance(lt.alert.category_t.connect_notification, int)
        self.assertIsInstance(lt.alert.category_t.status_notification, int)
        if lt.api_version < 2:
            self.assertIsInstance(lt.alert.category_t.debug_notification, int)
            self.assertIsInstance(lt.alert.category_t.progress_notification, int)
        self.assertIsInstance(lt.alert.category_t.ip_block_notification, int)
        self.assertIsInstance(lt.alert.category_t.performance_warning, int)
        self.assertIsInstance(lt.alert.category_t.dht_notification, int)
        if lt.api_version < 3:
            self.assertIsInstance(lt.alert.category_t.stats_notification, int)
        self.assertIsInstance(lt.alert.category_t.session_log_notification, int)
        self.assertIsInstance(lt.alert.category_t.torrent_log_notification, int)
        self.assertIsInstance(lt.alert.category_t.peer_log_notification, int)
        self.assertIsInstance(lt.alert.category_t.incoming_request_notification, int)
        self.assertIsInstance(lt.alert.category_t.dht_log_notification, int)
        self.assertIsInstance(lt.alert.category_t.dht_operation_notification, int)
        self.assertIsInstance(lt.alert.category_t.port_mapping_log_notification, int)
        self.assertIsInstance(lt.alert.category_t.picker_log_notification, int)
        self.assertIsInstance(lt.alert.category_t.file_progress_notification, int)
        self.assertIsInstance(lt.alert.category_t.piece_progress_notification, int)
        self.assertIsInstance(lt.alert.category_t.upload_notification, int)
        self.assertIsInstance(lt.alert.category_t.block_progress_notification, int)
        self.assertIsInstance(lt.alert.category_t.all_categories, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_category_t_deprecated(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.alert.category_t.debug_notification, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.alert.category_t.progress_notification, int)
        if lt.api_version < 3:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.alert.category_t.stats_notification, int)

    def test_alert_category(self) -> None:
        self.assertIsInstance(lt.alert_category.error, int)
        self.assertIsInstance(lt.alert_category.peer, int)
        self.assertIsInstance(lt.alert_category.port_mapping, int)
        self.assertIsInstance(lt.alert_category.storage, int)
        self.assertIsInstance(lt.alert_category.tracker, int)
        self.assertIsInstance(lt.alert_category.connect, int)
        self.assertIsInstance(lt.alert_category.status, int)
        self.assertIsInstance(lt.alert_category.ip_block, int)
        self.assertIsInstance(lt.alert_category.performance_warning, int)
        self.assertIsInstance(lt.alert_category.dht, int)
        self.assertIsInstance(lt.alert_category.stats, int)
        self.assertIsInstance(lt.alert_category.session_log, int)
        self.assertIsInstance(lt.alert_category.torrent_log, int)
        self.assertIsInstance(lt.alert_category.peer_log, int)
        self.assertIsInstance(lt.alert_category.incoming_request, int)
        self.assertIsInstance(lt.alert_category.dht_log, int)
        self.assertIsInstance(lt.alert_category.dht_operation, int)
        self.assertIsInstance(lt.alert_category.port_mapping_log, int)
        self.assertIsInstance(lt.alert_category.picker_log, int)
        self.assertIsInstance(lt.alert_category.file_progress, int)
        self.assertIsInstance(lt.alert_category.piece_progress, int)
        self.assertIsInstance(lt.alert_category.upload, int)
        self.assertIsInstance(lt.alert_category.block_progress, int)
        self.assertIsInstance(lt.alert_category.all, int)

    def test_operation_t(self) -> None:
        self.assertIsInstance(lt.operation_t.unknown, int)
        self.assertIsInstance(lt.operation_t.bittorrent, int)
        self.assertIsInstance(lt.operation_t.iocontrol, int)
        self.assertIsInstance(lt.operation_t.getpeername, int)
        self.assertIsInstance(lt.operation_t.getname, int)
        self.assertIsInstance(lt.operation_t.alloc_recvbuf, int)
        self.assertIsInstance(lt.operation_t.alloc_sndbuf, int)
        self.assertIsInstance(lt.operation_t.file_write, int)
        self.assertIsInstance(lt.operation_t.file_read, int)
        self.assertIsInstance(lt.operation_t.file, int)
        self.assertIsInstance(lt.operation_t.sock_write, int)
        self.assertIsInstance(lt.operation_t.sock_read, int)
        self.assertIsInstance(lt.operation_t.sock_open, int)
        self.assertIsInstance(lt.operation_t.sock_bind, int)
        self.assertIsInstance(lt.operation_t.available, int)
        self.assertIsInstance(lt.operation_t.encryption, int)
        self.assertIsInstance(lt.operation_t.connect, int)
        self.assertIsInstance(lt.operation_t.ssl_handshake, int)
        self.assertIsInstance(lt.operation_t.get_interface, int)
        self.assertIsInstance(lt.operation_t.sock_listen, int)
        self.assertIsInstance(lt.operation_t.sock_bind_to_device, int)
        self.assertIsInstance(lt.operation_t.sock_accept, int)
        self.assertIsInstance(lt.operation_t.parse_address, int)
        self.assertIsInstance(lt.operation_t.enum_if, int)
        self.assertIsInstance(lt.operation_t.file_stat, int)
        self.assertIsInstance(lt.operation_t.file_copy, int)
        self.assertIsInstance(lt.operation_t.file_fallocate, int)
        self.assertIsInstance(lt.operation_t.file_hard_link, int)
        self.assertIsInstance(lt.operation_t.file_remove, int)
        self.assertIsInstance(lt.operation_t.file_rename, int)
        self.assertIsInstance(lt.operation_t.file_open, int)
        self.assertIsInstance(lt.operation_t.mkdir, int)
        self.assertIsInstance(lt.operation_t.check_resume, int)
        self.assertIsInstance(lt.operation_t.exception, int)
        self.assertIsInstance(lt.operation_t.alloc_cache_piece, int)
        self.assertIsInstance(lt.operation_t.partfile_move, int)
        self.assertIsInstance(lt.operation_t.partfile_read, int)
        self.assertIsInstance(lt.operation_t.partfile_write, int)
        self.assertIsInstance(lt.operation_t.hostname_lookup, int)
        self.assertIsInstance(lt.operation_t.symlink, int)
        self.assertIsInstance(lt.operation_t.handshake, int)
        self.assertIsInstance(lt.operation_t.sock_option, int)

    def test_operation_name(self) -> None:
        # Programmatically check that operation_name works on all operation_t.* values
        for name in dir(lt.operation_t):
            value = getattr(lt.operation_t, name)
            if not isinstance(value, lt.operation_t):
                continue
            self.assertIsInstance(lt.operation_name(value), str)

    def test_listen_succeeded_alert_socket_type_t(self) -> None:
        if lt.api_version < 2:
            self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.tcp, int)
            self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.tcp_ssl, int)
            self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.udp, int)
            self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.i2p, int)
            self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.socks5, int)
            self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.utp_ssl, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_listen_succeeded_alert_socket_type_t_deprecated(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.tcp, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    lt.listen_succeeded_alert_socket_type_t.tcp_ssl, int
                )
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.udp, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.listen_succeeded_alert_socket_type_t.i2p, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    lt.listen_succeeded_alert_socket_type_t.socks5, int
                )
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    lt.listen_succeeded_alert_socket_type_t.utp_ssl, int
                )

    def test_listen_failed_alert_socket_type_t(self) -> None:
        if lt.api_version < 2:
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.tcp, int)
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.tcp_ssl, int)
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.udp, int)
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.i2p, int)
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.socks5, int)
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.utp_ssl, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_listen_failed_alert_socket_type_t_deprecated(self) -> None:
        with self.assertWarns(DeprecationWarning):
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.tcp, int)
        with self.assertWarns(DeprecationWarning):
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.tcp_ssl, int)
        with self.assertWarns(DeprecationWarning):
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.udp, int)
        with self.assertWarns(DeprecationWarning):
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.i2p, int)
        with self.assertWarns(DeprecationWarning):
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.socks5, int)
        with self.assertWarns(DeprecationWarning):
            self.assertIsInstance(lt.listen_failed_alert_socket_type_t.utp_ssl, int)

    def test_socket_type_t(self) -> None:
        self.assertIsInstance(lt.socket_type_t.tcp, int)
        self.assertIsInstance(lt.socket_type_t.socks5, int)
        self.assertIsInstance(lt.socket_type_t.http, int)
        self.assertIsInstance(lt.socket_type_t.utp, int)
        if lt.api_version < 2:
            self.assertIsInstance(lt.socket_type_t.udp, int)
        self.assertIsInstance(lt.socket_type_t.i2p, int)
        self.assertIsInstance(lt.socket_type_t.tcp_ssl, int)
        self.assertIsInstance(lt.socket_type_t.socks5_ssl, int)
        self.assertIsInstance(lt.socket_type_t.http_ssl, int)
        self.assertIsInstance(lt.socket_type_t.utp_ssl, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_socket_type_t_deprecated(self) -> None:
        with self.assertWarns(DeprecationWarning):
            self.assertIsInstance(lt.socket_type_t.udp, int)

    def test_reason_t(self) -> None:
        self.assertIsInstance(lt.reason_t.ip_filter, int)
        self.assertIsInstance(lt.reason_t.port_filter, int)
        self.assertIsInstance(lt.reason_t.i2p_mixed, int)
        self.assertIsInstance(lt.reason_t.privileged_ports, int)
        self.assertIsInstance(lt.reason_t.utp_disabled, int)
        self.assertIsInstance(lt.reason_t.tcp_disabled, int)
        self.assertIsInstance(lt.reason_t.invalid_local_interface, int)

    def test_performance_warning_t(self) -> None:
        self.assertIsInstance(
            lt.performance_warning_t.outstanding_disk_buffer_limit_reached, int
        )
        self.assertIsInstance(
            lt.performance_warning_t.outstanding_request_limit_reached, int
        )
        self.assertIsInstance(lt.performance_warning_t.upload_limit_too_low, int)
        self.assertIsInstance(lt.performance_warning_t.download_limit_too_low, int)
        self.assertIsInstance(
            lt.performance_warning_t.send_buffer_watermark_too_low, int
        )
        self.assertIsInstance(
            lt.performance_warning_t.too_many_optimistic_unchoke_slots, int
        )
        if lt.api_version < 2:
            self.assertIsInstance(
                lt.performance_warning_t.bittyrant_with_no_uplimit, int
            )
        self.assertIsInstance(lt.performance_warning_t.too_high_disk_queue_limit, int)
        self.assertIsInstance(lt.performance_warning_t.too_few_outgoing_ports, int)
        self.assertIsInstance(lt.performance_warning_t.too_few_file_descriptors, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_performance_warning_t_deprecated(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    lt.performance_warning_t.bittyrant_with_no_uplimit, int
                )

    def test_stats_channel(self) -> None:
        if lt.api_version < 3:
            self.assertIsInstance(lt.stats_channel.upload_payload, int)
            self.assertIsInstance(lt.stats_channel.upload_protocol, int)
            self.assertIsInstance(lt.stats_channel.upload_ip_protocol, int)
            if lt.api_version < 2:
                self.assertIsInstance(lt.stats_channel.upload_dht_protocol, int)
                self.assertIsInstance(lt.stats_channel.upload_tracker_protocol, int)
            self.assertIsInstance(lt.stats_channel.download_payload, int)
            self.assertIsInstance(lt.stats_channel.download_protocol, int)
            self.assertIsInstance(lt.stats_channel.download_ip_protocol, int)
            if lt.api_version < 2:
                self.assertIsInstance(lt.stats_channel.download_dht_protocol, int)
                self.assertIsInstance(lt.stats_channel.download_tracker_protocol, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_stats_channel_deprecated(self) -> None:
        if lt.api_version < 3:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.stats_channel.upload_payload, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.stats_channel.upload_protocol, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.stats_channel.upload_ip_protocol, int)
            if lt.api_version < 2:
                with self.assertWarns(DeprecationWarning):
                    self.assertIsInstance(lt.stats_channel.upload_dht_protocol, int)
                with self.assertWarns(DeprecationWarning):
                    self.assertIsInstance(lt.stats_channel.upload_tracker_protocol, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.stats_channel.download_payload, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.stats_channel.download_protocol, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.stats_channel.download_ip_protocol, int)
            if lt.api_version < 2:
                with self.assertWarns(DeprecationWarning):
                    self.assertIsInstance(lt.stats_channel.download_dht_protocol, int)
                with self.assertWarns(DeprecationWarning):
                    self.assertIsInstance(
                        lt.stats_channel.download_tracker_protocol, int
                    )

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_kind_deprecated(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.kind.tracker_no_anonymous, int)

    def test_close_reason_t(self) -> None:
        self.assertIsInstance(lt.close_reason_t.blocked, int)
        self.assertIsInstance(lt.close_reason_t.corrupt_pieces, int)
        self.assertIsInstance(lt.close_reason_t.duplicate_peer_id, int)
        self.assertIsInstance(lt.close_reason_t.encryption_error, int)
        self.assertIsInstance(lt.close_reason_t.invalid_allow_fast_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_bitfield_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_cancel_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_choke_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_dht_port_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_dont_have_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_extended_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_have_all_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_have_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_have_none_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_info_hash, int)
        self.assertIsInstance(lt.close_reason_t.invalid_interested_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_message_id, int)
        self.assertIsInstance(lt.close_reason_t.invalid_metadata, int)
        self.assertIsInstance(lt.close_reason_t.invalid_metadata_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_metadata_offset, int)
        self.assertIsInstance(lt.close_reason_t.invalid_metadata_request_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_not_interested_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_pex_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_piece_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_reject_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_request_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_suggest_message, int)
        self.assertIsInstance(lt.close_reason_t.invalid_unchoke_message, int)
        self.assertIsInstance(lt.close_reason_t.message_too_big, int)
        self.assertIsInstance(lt.close_reason_t.metadata_too_big, int)
        self.assertIsInstance(lt.close_reason_t.no_memory, int)
        self.assertIsInstance(lt.close_reason_t.none, int)
        self.assertIsInstance(lt.close_reason_t.not_interested_upload_only, int)
        self.assertIsInstance(lt.close_reason_t.peer_churn, int)
        self.assertIsInstance(lt.close_reason_t.pex_message_too_big, int)
        self.assertIsInstance(lt.close_reason_t.pex_too_frequent, int)
        self.assertIsInstance(lt.close_reason_t.port_blocked, int)
        self.assertIsInstance(lt.close_reason_t.protocol_blocked, int)
        self.assertIsInstance(lt.close_reason_t.request_when_choked, int)
        self.assertIsInstance(lt.close_reason_t.self_connection, int)
        self.assertIsInstance(lt.close_reason_t.timed_out_activity, int)
        self.assertIsInstance(lt.close_reason_t.timed_out_handshake, int)
        self.assertIsInstance(lt.close_reason_t.timed_out_interest, int)
        self.assertIsInstance(lt.close_reason_t.timed_out_request, int)
        self.assertIsInstance(lt.close_reason_t.timeout, int)
        self.assertIsInstance(lt.close_reason_t.too_many_connections, int)
        self.assertIsInstance(lt.close_reason_t.too_many_files, int)
        self.assertIsInstance(lt.close_reason_t.torrent_removed, int)
        self.assertIsInstance(lt.close_reason_t.upload_to_upload, int)

    def test_dht_module_t(self) -> None:
        self.assertIsInstance(lt.dht_module_t.node, int)
        self.assertIsInstance(lt.dht_module_t.routing_table, int)
        self.assertIsInstance(lt.dht_module_t.rpc_manager, int)
        self.assertIsInstance(lt.dht_module_t.tracker, int)
        self.assertIsInstance(lt.dht_module_t.traversal, int)

    def test_picker_flags_t(self) -> None:
        self.assertIsInstance(lt.picker_flags_t.backup1, int)
        self.assertIsInstance(lt.picker_flags_t.backup2, int)
        self.assertIsInstance(lt.picker_flags_t.end_game, int)
        self.assertIsInstance(lt.picker_flags_t.extent_affinity, int)
        self.assertIsInstance(lt.picker_flags_t.partial_ratio, int)
        self.assertIsInstance(lt.picker_flags_t.prefer_contiguous, int)
        self.assertIsInstance(lt.picker_flags_t.prio_sequential_pieces, int)
        self.assertIsInstance(lt.picker_flags_t.prioritize_partials, int)
        self.assertIsInstance(lt.picker_flags_t.random_pieces, int)
        self.assertIsInstance(lt.picker_flags_t.rarest_first, int)
        self.assertIsInstance(lt.picker_flags_t.rarest_first_partials, int)
        self.assertIsInstance(lt.picker_flags_t.reverse_pieces, int)
        self.assertIsInstance(lt.picker_flags_t.reverse_rarest_first, int)
        self.assertIsInstance(lt.picker_flags_t.reverse_sequential, int)
        self.assertIsInstance(lt.picker_flags_t.sequential_pieces, int)
        self.assertIsInstance(lt.picker_flags_t.suggested_pieces, int)
        self.assertIsInstance(lt.picker_flags_t.time_critical, int)


_A = TypeVar("_A", bound=lt.alert)


def wait_until_done_checking(handle: lt.torrent_handle, *, timeout: float) -> None:
    for _ in lib.loop_until_timeout(5, msg="checking"):
        if handle.status().state not in (
            lt.torrent_status.checking_files,
            lt.torrent_status.checking_resume_data,
        ):
            break


def remove_handles_and_wait(session: lt.session) -> None:
    handles = session.get_torrents()
    for handle in handles:
        session.remove_torrent(handle)
    for _ in lib.loop_until_timeout(5, msg="clear all handles"):
        if not any(handle.is_valid() for handle in handles):
            break


class AlertTest(unittest.TestCase):
    ALERT_MASK = 0

    def setUp(self) -> None:
        super().setUp()
        self.settings = lib.get_isolated_settings()
        self.settings["alert_mask"] = self.ALERT_MASK
        self.session = lt.session(self.settings)
        self.endpoint = ("127.0.0.1", self.session.listen_port())
        self.endpoint_str = f"{self.endpoint[0]}:{self.endpoint[1]}"
        # Buffer of alerts fetched but not yet consumed by wait_for().
        # Elements are non-owning pointers into the alert arena; they remain
        # valid only until the next pop_alerts() call, so we must clear this
        # buffer before calling pop_alerts() again.
        self._pending_alerts: list[lt.alert] = []

    def tearDown(self) -> None:
        super().tearDown()
        # we do this because sessions writing data can collide with
        # cleaning up temporary directories. session.abort() isn't bound
        remove_handles_and_wait(self.session)

    def wait_for(self, alert_type: Type[_A], *, timeout: float) -> _A:
        # Return the first alert of type _A from self.session, buffering the
        # rest so sequential calls don't discard alerts that arrive in the
        # same pop_alerts() batch.
        for _ in lib.loop_until_timeout(timeout, msg=alert_type.__name__):
            for i, a in enumerate(self._pending_alerts):
                if isinstance(a, alert_type):
                    del self._pending_alerts[i]
                    return a
            # Nothing in the buffer matched; discard it (contents become stale
            # after the next pop_alerts()) and fetch a new batch.
            self._pending_alerts.clear()
            self.session.wait_for_alert(100)
            new_alerts = list(self.session.pop_alerts())
            for a in new_alerts:
                logging.debug("%s: %s", a.what(), a.message())
            self._pending_alerts = new_alerts
        raise AssertionError("unreachable")

    def assert_alert(self, alert: lt.alert, category: int, what: str) -> None:
        self.assertEqual(alert.category(), category)
        self.assertEqual(alert.what(), what)
        self.assertIsInstance(alert.message(), str)
        self.assertNotEqual(alert.message(), "")
        self.assertEqual(str(alert), alert.message())


maybe_progress_notification: int = (
    lt.alert.category_t.progress_notification if lt.api_version < 2 else 0
)


class TorrentAlertTest(AlertTest):
    def setUp(self) -> None:
        super().setUp()
        self.dir = tempfile.TemporaryDirectory()
        self.torrent = tdummy.get_default()
        self.atp = self.torrent.atp()
        self.atp.flags &= ~lt.torrent_flags.auto_managed
        self.atp.flags &= ~lt.torrent_flags.paused
        self.atp.save_path = self.dir.name
        ti = self.atp.ti
        assert ti is not None
        self.file_path = os.path.join(self.dir.name, ti.layout().file_path(0))
        self.torrent_name = ti.name()

    def tearDown(self) -> None:
        super().tearDown()
        lib.cleanup_with_windows_fix(self.dir, timeout=5)

    def assert_torrent_alert(
        self, alert: lt.torrent_alert, handle: lt.torrent_handle
    ) -> None:
        self.assertEqual(alert.handle, handle)
        self.assertEqual(alert.torrent_name, self.torrent_name)


class UseAfterFreeTest(TorrentAlertTest):
    # In theory, we shouldn't hold a reference to an alert after the next
    # pop_alerts(), because the underlying data gets freed. In reality, strict
    # reference management is infeasible in python.

    # For example, note that every exception references the stack frame where it
    # was raised, which references all the locals. So if your alert-handling
    # code uses a local variable for an alert, and any exception is raised by
    # lower-level code and handled by upper-level code, you'll get a reference
    # to an alert you probably didn't intend, and can't feasibly remove.

    # For another example, consider:
    #    logging.info("alert: %s", alert)
    # Seems like this shouldn't cause a lasting reference to the alert, but *it
    # depends on logger configuration. pytest's default logger holds references
    # to all log entries *including arguments* for the lifetime of the test
    # runner.

    # We don't have a defense against code that aggressively introspects all
    # live objects. Hopefully this is too rare to matter

    # Instead, we test some basic accesses that are likely to happen to
    # arbitrary python objects.

    def setUp(self) -> None:
        super().setUp()
        self.session.add_torrent(self.atp)
        self.alert = self.wait_for(lt.add_torrent_alert, timeout=5)
        self.session.pop_alerts()
        # underlying alert data has now been freed

    def test_repr(self) -> None:
        # repr() is used to display results on an interactive command line;
        # let's ensure that doesn't constantly crash. It's also used as a "poor
        # man's introspection" in some other contexts.
        self.assertIsInstance(repr(self.alert), str)

    def test_release(self) -> None:
        # This just tests releasing the python object after the underlying
        # object was freed. Technically this is done by other tests, but I want
        # to explicitly spell out that this is something we need to support
        pass


class TorrentAddedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_torrent_added_alert(self) -> None:
        if lt.api_version < 2:
            handle = self.session.add_torrent(self.atp)
            alert = self.wait_for(lt.torrent_added_alert, timeout=5)

            self.assert_alert(alert, lt.alert_category.status, "torrent_added")
            self.assert_torrent_alert(alert, handle)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertTrue(issubclass(lt.torrent_added_alert, lt.torrent_alert))


class TorrentRemovedAlertTest(TorrentAlertTest):
    def test_torrent_removed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        self.session.remove_torrent(handle)
        alert = self.wait_for(lt.torrent_removed_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "torrent_removed")
        self.assert_torrent_alert(alert, handle)
        if lt.api_version < 3:
            self.assertEqual(alert.info_hash, self.torrent.sha1_hash)
        self.assertEqual(alert.info_hashes.v1, self.torrent.sha1_hash)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        self.session.remove_torrent(handle)
        alert = self.wait_for(lt.torrent_removed_alert, timeout=5)
        if lt.api_version < 3:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.info_hash, self.torrent.sha1_hash)


class ReadPieceAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.piece_progress

    def test_read_piece(self) -> None:
        handle = self.session.add_torrent(self.atp)
        # add_piece() does not work in the checking_* states
        wait_until_done_checking(handle, timeout=5)

        handle.add_piece(0, self.torrent.pieces[0], 0)
        # read_piece() does not work until we have the piece
        self.alert = self.wait_for(lt.piece_finished_alert, timeout=5)
        handle.read_piece(0)
        alert = self.wait_for(lt.read_piece_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.storage, "read_piece")
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.error.value(), 0)
        if lt.api_version < 2:
            self.assertEqual(alert.ec.value(), 0)
        self.assertEqual(alert.buffer, self.torrent.pieces[0])
        self.assertEqual(alert.piece, 0)
        self.assertEqual(alert.size, len(self.torrent.pieces[0]))

    def test_error(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.set_piece_deadline(0, 0, flags=lt.deadline_flags_t.alert_when_available)
        # setting piece priority to 0 cancels a read_piece
        handle.piece_priority(0, 0)
        alert = self.wait_for(lt.read_piece_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.storage, "read_piece")
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.error.value(), errno.ECANCELED)
        self.assertEqual(alert.error.category(), lt.generic_category())
        if lt.api_version < 2:
            self.assertEqual(alert.ec.value(), errno.ECANCELED)
            self.assertEqual(alert.ec.category(), lt.generic_category())

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.set_piece_deadline(0, 0, flags=lt.deadline_flags_t.alert_when_available)
        # setting piece priority to 0 cancels a read_piece
        handle.piece_priority(0, 0)
        alert = self.wait_for(lt.read_piece_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.ec.value(), errno.ECANCELED)


class PeerAlertTest(TorrentAlertTest):
    def setUp(self) -> None:
        super().setUp()
        self.session.apply_settings({"close_redundant_connections": False})

        self.peer = lt.session(lib.get_isolated_settings())
        self.peer.apply_settings({"close_redundant_connections": False})
        self.peer_dir = tempfile.TemporaryDirectory()
        self.peer_atp = self.torrent.atp()
        self.peer_atp.save_path = self.peer_dir.name
        self.peer_endpoint = ("127.0.0.1", self.peer.listen_port())
        self.peer_endpoint_str = f"{self.peer_endpoint[0]}:{self.peer_endpoint[1]}"
        self.peer_fingerprint = self.peer.get_settings()["peer_fingerprint"].encode()

    def tearDown(self) -> None:
        super().tearDown()
        remove_handles_and_wait(self.peer)
        lib.cleanup_with_windows_fix(self.peer_dir, timeout=5)

    def assert_peer_alert(
        self,
        alert: lt.peer_alert,
        endpoint: Tuple[str, int],
        *,
        pid: Optional[lt.sha1_hash] = None,
        fingerprint: Optional[bytes] = None,
    ) -> None:
        if lt.api_version < 2:
            self.assertEqual(alert.ip, endpoint)
        self.assertEqual(alert.endpoint, endpoint)
        if pid is not None:
            self.assertEqual(alert.pid, pid)
        if fingerprint is not None:
            self.assertEqual(alert.pid.to_bytes()[: len(fingerprint)], fingerprint)


class LambdaRequestHandler(http.server.BaseHTTPRequestHandler):
    default_request_version = "HTTP/1.1"

    def __init__(
        self, get_data: Callable[[], bytes], *args: Any, **kwargs: Any
    ) -> None:
        self.get_data = get_data
        super().__init__(*args, **kwargs)

    def do_GET(self) -> None:
        data = self.get_data()
        self.send_response(200)
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Content-Length", str(len(data)))
        self.end_headers()
        self.wfile.write(data)


class TrackerAlertTest(TorrentAlertTest):
    RESPONSE = None

    def setUp(self) -> None:
        super().setUp()
        # TODO: this is currently necessary. Is it a bug?
        self.session.apply_settings({"ssrf_mitigation": False})
        self.tracker_response: Dict[bytes, Any] = {}
        self.tracker = http.server.HTTPServer(
            ("127.0.0.1", 0),
            functools.partial(
                LambdaRequestHandler, lambda: lt.bencode(self.tracker_response)
            ),
        )
        ctx = self.get_tracker_ssl_ctx()
        if ctx is not None:
            self.tracker.socket = ctx.wrap_socket(self.tracker.socket, server_side=True)
        self.tracker_thread = threading.Thread(target=self.tracker.serve_forever)
        self.tracker_thread.start()
        # HTTPServer.server_name seems to resolve to things like
        # "localhost.localdomain"
        port = self.tracker.server_port
        proto = "http" if ctx is None else "https"
        self.tracker_url = f"{proto}://127.0.0.1:{port}/announce"

    def tearDown(self) -> None:
        super().tearDown()
        self.tracker.shutdown()
        # Explicitly clean up server sockets, to avoid ResourceWarning
        self.tracker.server_close()

    def get_tracker_ssl_ctx(self) -> Optional[ssl.SSLContext]:
        return None

    def assert_tracker_alert(self, alert: lt.tracker_alert) -> None:
        if lt.api_version < 2:
            self.assertEqual(alert.url, self.tracker_url)
        self.assertEqual(alert.tracker_url(), self.tracker_url)
        endpoint = alert.local_endpoint
        self.assertIsInstance(endpoint, tuple)
        self.assertEqual(len(endpoint), 2)
        self.assertEqual(endpoint[0], "127.0.0.1")
        self.assertIsInstance(endpoint[1], int)


class TrackerErrorAlertTest(TrackerAlertTest):
    ALERT_MASK = lt.alert_category.tracker

    def test_tracker_error_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        self.tracker_response = {
            b"failure reason": b"test",
        }
        handle.add_tracker({"url": self.tracker_url})
        alert = self.wait_for(lt.tracker_error_alert, timeout=5)

        self.assert_alert(
            alert, lt.alert_category.tracker | lt.alert_category.error, "tracker_error"
        )
        self.assert_torrent_alert(alert, handle)
        self.assert_tracker_alert(alert)
        if lt.api_version < 2:
            self.assertEqual(alert.msg, "test")
            self.assertEqual(alert.status_code, -1)
        self.assertEqual(alert.error_message(), "test")
        self.assertEqual(alert.failure_reason(), "test")
        self.assertEqual(alert.times_in_row, 1)
        self.assertEqual(alert.error.category(), lt.libtorrent_category())
        self.assertEqual(alert.error.value(), 173)  # "tracker sent a failure message"

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        self.tracker_response = {
            b"failure reason": b"test",
        }
        handle.add_tracker({"url": self.tracker_url})
        alert = self.wait_for(lt.tracker_error_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.url, self.tracker_url)
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.msg, "test")
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.status_code, -1)


class TrackerWarningAlertTest(TrackerAlertTest):
    ALERT_MASK = lt.alert_category.tracker

    def test_tracker_warning_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        self.tracker_response = {b"warning message": b"test"}
        handle.add_tracker({"url": self.tracker_url})
        alert = self.wait_for(lt.tracker_warning_alert, timeout=5)

        self.assert_alert(
            alert,
            lt.alert_category.tracker | lt.alert_category.error,
            "tracker_warning",
        )
        self.assert_torrent_alert(alert, handle)
        self.assert_tracker_alert(alert)
        # the actual warning message is not bound
        self.assertIn("warning: test", alert.message())


class TrackerReplyAlertTest(TrackerAlertTest):
    ALERT_MASK = lt.alert_category.tracker

    def test_tracker_reply_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        self.tracker_response = {b"peers": [{b"ip": b"127.0.0.1", b"port": 65535}]}
        handle.add_tracker({"url": self.tracker_url})
        alert = self.wait_for(lt.tracker_reply_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.tracker, "tracker_reply")
        self.assert_torrent_alert(alert, handle)
        self.assert_tracker_alert(alert)
        self.assertEqual(alert.num_peers, 1)


class TrackerAnnounceAlertTest(TrackerAlertTest):
    ALERT_MASK = lt.alert_category.tracker

    def test_tracker_reply_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.add_tracker({"url": self.tracker_url})
        alert = self.wait_for(lt.tracker_announce_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.tracker, "tracker_announce")
        self.assert_torrent_alert(alert, handle)
        self.assert_tracker_alert(alert)


class HashFailedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_hash_failed_alert_test(self) -> None:
        handle = self.session.add_torrent(self.atp)
        # add_piece() does not work in the checking_* states
        wait_until_done_checking(handle, timeout=5)
        # add_piece() with bad data
        handle.add_piece(0, b"a" * len(self.torrent.pieces[0]), 0)

        alert = self.wait_for(lt.hash_failed_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "hash_failed")
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.piece_index, 0)


class PeerBanAlertTest(PeerAlertTest):
    @unittest.skip("TODO: Probably need to mock a bad actor peer")
    def test_peer_ban_alert(self) -> None:
        # peer_ban_alert is raised when libtorrent finds a peer sending
        # data that fails hash check. how do we convince the peer session
        # to do this?
        raise NotImplementedError


class PeerErrorAlertTest(PeerAlertTest):
    @unittest.skip("TODO: Probably need to mock a bad actor peer")
    def test_peer_error_alert(self) -> None:
        # peer_error_alert is raised in various cases of peers doing invalid
        # protocol things
        raise NotImplementedError


class InvalidRequestAlertTest(PeerAlertTest):
    @unittest.skip("TODO: Probably need to mock a bad actor peer")
    def test_invalid_request_alert(self) -> None:
        # peer_error_alert is raised in various cases of peers doing invalid
        # protocol things
        raise NotImplementedError


class TorrentErrorAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    @unittest.skipIf(sys.platform == "win32", "TODO: make this work on windows")
    def test_torrent_error_alert(self) -> None:
        # create the target file as a directory
        os.mkdir(self.file_path)
        handle = self.session.add_torrent(self.atp)

        alert = self.wait_for(lt.torrent_error_alert, timeout=5)

        self.assert_alert(
            alert, lt.alert_category.error | lt.alert_category.status, "torrent_error"
        )
        self.assert_torrent_alert(alert, handle)
        self.assertIsInstance(alert.error.value(), int)
        self.assertNotEqual(alert.error.value(), 0)
        # NB: posix storage results in different error codes than mmap storage
        self.assertIsInstance(alert.error.category(), lt.error_category)


class TorrentFinishedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_torrent_finished_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        # add_piece() doesn't work in checking state
        wait_until_done_checking(handle, timeout=5)
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)

        alert = self.wait_for(lt.torrent_finished_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "torrent_finished")
        self.assert_torrent_alert(alert, handle)


class PieceFinishedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.piece_progress

    def test_piece_finished_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        # add_piece() doesn't work in checking state
        wait_until_done_checking(handle, timeout=5)
        handle.add_piece(0, self.torrent.pieces[0], 0)

        alert = self.wait_for(lt.piece_finished_alert, timeout=5)

        self.assert_alert(
            alert,
            lt.alert_category.piece_progress | maybe_progress_notification,
            "piece_finished",
        )
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.piece_index, 0)


class BlockFinishedAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.block_progress

    def test_block_finished_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        peer_handle = self.peer.add_torrent(self.peer_atp)
        # add_piece() doesn't work in checking state
        wait_until_done_checking(peer_handle, timeout=5)
        peer_handle.add_piece(0, self.torrent.pieces[0], 0)
        handle.connect_peer(self.peer_endpoint)

        alert = self.wait_for(lt.block_finished_alert, timeout=10)

        self.assert_alert(
            alert,
            lt.alert_category.block_progress | maybe_progress_notification,
            "block_finished",
        )
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.piece_index, 0)
        self.assertEqual(alert.block_index, 0)


class BlockDownloadingAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.block_progress

    def test_block_downloading_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        peer_handle = self.peer.add_torrent(self.peer_atp)
        # add_piece() doesn't work in checking state
        wait_until_done_checking(peer_handle, timeout=5)
        peer_handle.add_piece(0, self.torrent.pieces[0], 0)
        handle.connect_peer(self.peer_endpoint)

        alert = self.wait_for(lt.block_downloading_alert, timeout=10)

        self.assert_alert(
            alert,
            lt.alert_category.block_progress | maybe_progress_notification,
            "block_downloading",
        )
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.piece_index, 0)
        self.assertEqual(alert.block_index, 0)
        if lt.api_version < 2:
            self.assertIsInstance(alert.peer_speedmsg, str)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        peer_handle = self.peer.add_torrent(self.peer_atp)
        # add_piece() doesn't work in checking state
        wait_until_done_checking(peer_handle, timeout=5)
        peer_handle.add_piece(0, self.torrent.pieces[0], 0)
        handle.connect_peer(self.peer_endpoint)

        alert = self.wait_for(lt.block_downloading_alert, timeout=10)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(alert.peer_speedmsg, str)


class StorageMovedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    def test_storage_moved_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        wait_until_done_checking(handle, timeout=5)
        new = os.path.join(self.dir.name, "new")
        handle.move_storage(new)

        alert = self.wait_for(lt.storage_moved_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.storage, "storage_moved")
        self.assert_torrent_alert(alert, handle)
        if lt.api_version < 2:
            self.assertEqual(alert.path, new)
        self.assertEqual(alert.storage_path(), new)
        self.assertEqual(alert.old_path(), self.dir.name)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        wait_until_done_checking(handle, timeout=5)
        new = os.path.join(self.dir.name, "new")
        handle.move_storage(new)

        alert = self.wait_for(lt.storage_moved_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.path, new)


class StorageMovedFailedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    def test_storage_moved_failed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        # add some data, otherwise storage_moved_failed_alert isn't triggered
        wait_until_done_checking(handle, timeout=5)
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)
        # create a file at the new path
        new = os.path.join(self.dir.name, "new")
        with open(new, mode="wb") as fp:
            fp.write(b"data")
        handle.move_storage(new)

        alert = self.wait_for(lt.storage_moved_failed_alert, timeout=5)

        print(f"{alert.error.message()} : {alert.error.category().name()}")

        self.assert_alert(alert, lt.alert_category.storage, "storage_moved_failed")
        self.assert_torrent_alert(alert, handle)
        self.assertIsInstance(alert.error.value(), int)
        self.assertNotEqual(alert.error.value(), 0)
        self.assertTrue(
            alert.error.category() == lt.system_category()
            or alert.error.category() == lt.generic_category()
        )
        self.assertEqual(alert.file_path(), self.file_path)
        self.assertTrue(
            alert.op in [lt.operation_t.file_open, lt.operation_t.file_rename]
        )
        if lt.api_version < 2:
            self.assertTrue(alert.operation in ["file_open", "file_rename"])

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        # add some data, otherwise storage_moved_failed_alert isn't triggered
        wait_until_done_checking(handle, timeout=5)
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)
        # create a file at the new path
        new = os.path.join(self.dir.name, "new")
        with open(new, mode="wb") as fp:
            fp.write(b"data")
        handle.move_storage(new)

        alert = self.wait_for(lt.storage_moved_failed_alert, timeout=5)

        with self.assertWarns(DeprecationWarning):
            self.assertEqual(alert.operation, "file_rename")


class TorrentDeletedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    def test_torrent_deleted_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        # add some data, otherwise storage_moved_failed_alert isn't triggered
        wait_until_done_checking(handle, timeout=5)
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)
        self.session.remove_torrent(
            handle, option=lt.session.delete_files | lt.session.delete_partfile
        )

        alert = self.wait_for(lt.torrent_deleted_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.storage, "torrent_deleted")
        self.assert_torrent_alert(alert, handle)
        if lt.api_version < 2:
            self.assertEqual(alert.info_hash, self.torrent.sha1_hash)
        self.assertEqual(alert.info_hashes.v1, self.torrent.sha1_hash)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        # add some data, otherwise storage_moved_failed_alert isn't triggered
        wait_until_done_checking(handle, timeout=5)
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)
        self.session.remove_torrent(
            handle, option=lt.session.delete_files | lt.session.delete_partfile
        )

        alert = self.wait_for(lt.torrent_deleted_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.info_hash, self.torrent.sha1_hash)


class TorrentPausedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_torrent_paused_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.pause()

        alert = self.wait_for(lt.torrent_paused_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "torrent_paused")
        self.assert_torrent_alert(alert, handle)


class TorrentCheckedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_torrent_checked_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)

        alert = self.wait_for(lt.torrent_checked_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "torrent_checked")
        self.assert_torrent_alert(alert, handle)


class UrlSeedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.peer

    def test_url_seed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.add_url_seed("test://test")

        alert = self.wait_for(lt.url_seed_alert, timeout=5)

        self.assert_alert(
            alert, lt.alert_category.peer | lt.alert_category.error, "url_seed"
        )
        self.assert_torrent_alert(alert, handle)
        if lt.api_version < 2:
            self.assertEqual(alert.url, "test://test")
            self.assertIsInstance(alert.msg, str)
        self.assertEqual(alert.error.category(), lt.libtorrent_category())
        self.assertEqual(alert.error.value(), 24)  # unsupported URL protocol
        self.assertEqual(alert.server_url(), "test://test")
        # Seems to be always empty
        self.assertEqual(alert.error_message(), "")

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.add_url_seed("test://test")

        alert = self.wait_for(lt.url_seed_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.url, "test://test")
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(alert.msg, str)


class FileErrorAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    @unittest.skipIf(sys.platform == "win32", "TODO: make this work on windows")
    def test_file_error_alert(self) -> None:
        # create the target file as a directory
        os.mkdir(self.file_path)
        handle = self.session.add_torrent(self.atp)

        alert = self.wait_for(lt.file_error_alert, timeout=5)

        self.assert_alert(
            alert,
            lt.alert_category.error
            | lt.alert_category.storage
            | lt.alert_category.status,
            "file_error",
        )
        self.assert_torrent_alert(alert, handle)
        self.assertIsInstance(alert.error.value(), int)
        self.assertNotEqual(alert.error.value(), 0)
        # NB: posix storage results in different error codes than mmap storage
        self.assertIsInstance(alert.error.category(), lt.error_category)
        self.assertEqual(alert.filename(), self.file_path)
        if lt.api_version < 2:
            self.assertEqual(alert.file, self.file_path)
            self.assertEqual(alert.msg, alert.error.message())

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        # create the target file as a directory
        os.mkdir(self.file_path)
        self.session.add_torrent(self.atp)

        alert = self.wait_for(lt.file_error_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.file, self.file_path)
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.msg, alert.error.message())


class MetadataFailedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.error

    def test_metadata_failed_alert(self) -> None:
        self.atp.ti = None
        self.atp.name = self.torrent_name
        self.atp.info_hashes = lt.info_hash_t(self.torrent.sha1_hash)
        handle = self.session.add_torrent(self.atp)
        handle.set_metadata(b"invalid")

        alert = self.wait_for(lt.metadata_failed_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.error, "metadata_failed")
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.error.category(), lt.libtorrent_category())
        self.assertEqual(alert.error.value(), 30)  # invalid metadata received


class MetadataReceivedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_metadata_failed_alert(self) -> None:
        self.atp.ti = None
        self.atp.name = self.torrent_name
        self.atp.info_hashes = lt.info_hash_t(self.torrent.sha1_hash)
        handle = self.session.add_torrent(self.atp)
        handle.set_metadata(lt.bencode(self.torrent.info))

        alert = self.wait_for(lt.metadata_received_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "metadata_received")
        self.assert_torrent_alert(alert, handle)


class ListenFailedAlertTest(AlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_listen_failed_alert(self) -> None:
        self.session.apply_settings({"listen_interfaces": "does-not-exist"})

        alert = self.wait_for(lt.listen_failed_alert, timeout=5)

        self.assert_alert(
            alert, lt.alert_category.status | lt.alert_category.error, "listen_failed"
        )
        self.assertEqual(alert.address, "0.0.0.0")
        self.assertEqual(alert.port, 0)
        self.assertEqual(alert.listen_interface(), "does-not-exist")
        self.assertEqual(alert.error.category(), lt.libtorrent_category())
        self.assertEqual(alert.error.value(), 32)  # parse error
        self.assertEqual(alert.op, lt.operation_t.parse_address)
        if lt.api_version < 2:
            self.assertEqual(alert.endpoint, ("0.0.0.0", 0))
            self.assertEqual(alert.operation, 0)
            self.assertIsInstance(alert.sock_type, lt.listen_failed_alert_socket_type_t)
        self.assertIsInstance(alert.socket_type, lt.socket_type_t)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        self.session.apply_settings({"listen_interfaces": "does-not-exist"})

        alert = self.wait_for(lt.listen_failed_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.endpoint, ("0.0.0.0", 0))
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.operation, 0)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    alert.sock_type, lt.listen_failed_alert_socket_type_t
                )


class ListenSucceededAlertTest(AlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_listen_succeeded_alert(self) -> None:
        alert = self.wait_for(lt.listen_succeeded_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "listen_succeeded")
        if lt.api_version < 2:
            self.assertEqual(alert.endpoint, ("127.0.0.1", self.session.listen_port()))
            self.assertIsInstance(
                alert.sock_type, lt.listen_succeeded_alert_socket_type_t
            )
        self.assertEqual(alert.address, "127.0.0.1")
        self.assertEqual(alert.port, self.session.listen_port())
        self.assertIsInstance(alert.socket_type, lt.socket_type_t)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        alert = self.wait_for(lt.listen_succeeded_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(
                    alert.endpoint, ("127.0.0.1", self.session.listen_port())
                )
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    alert.sock_type, lt.listen_failed_alert_socket_type_t
                )


class PortmapErrorAlertTest(AlertTest):
    @unittest.skip("TODO: how do we test this in isolation?")
    def test_portmap_error_alert(self) -> None:
        raise NotImplementedError


class PortmapAlertTest(AlertTest):
    @unittest.skip("TODO: how do we test this in isolation?")
    def test_portmap_alert(self) -> None:
        raise NotImplementedError


class PortmapLogAlertTest(AlertTest):
    @unittest.skip("TODO: how do we test this in isolation?")
    def test_portmap_log_alert(self) -> None:
        raise NotImplementedError


class FastresumeRejectedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_fastresume_rejected_alert(self) -> None:
        # setting atp.have_pieces makes libtorrent think of the atp as
        # "resume data", triggering the alert
        self.atp.have_pieces = [True] * len(self.torrent.pieces)
        # create the target file as a directory
        os.mkdir(self.file_path)
        handle = self.session.add_torrent(self.atp)

        alert = self.wait_for(lt.fastresume_rejected_alert, timeout=5)

        self.assert_alert(
            alert,
            lt.alert_category.error | lt.alert_category.status,
            "fastresume_rejected",
        )
        self.assert_torrent_alert(alert, handle)
        self.assertIsInstance(alert.error.value(), int)
        self.assertNotEqual(alert.error.value(), 0)
        self.assertEqual(alert.error.category(), lt.libtorrent_category())
        self.assertEqual(alert.file_path(), self.file_path)
        self.assertEqual(alert.op, lt.operation_t.check_resume)
        if lt.api_version < 2:
            self.assertEqual(alert.operation, "check_resume")
            self.assertIsInstance(alert.msg, str)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        # setting atp.have_pieces makes libtorrent think of the atp as
        # "resume data", triggering the alert
        self.atp.have_pieces = [True] * len(self.torrent.pieces)
        # create the target file as a directory
        os.mkdir(self.file_path)
        self.session.add_torrent(self.atp)

        alert = self.wait_for(lt.fastresume_rejected_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.operation, "check_resume")
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(alert.msg, str)


class PeerBlockedAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.ip_block

    def test_peer_blocked_alert(self) -> None:
        # block all ips
        ipf = lt.ip_filter()
        ipf.add_rule("0.0.0.0", "255.255.255.255", 1)
        self.session.set_ip_filter(ipf)
        # add the torrent
        handle = self.session.add_torrent(self.atp)
        # add on the peer
        self.peer.add_torrent(self.peer_atp)
        # connect us to peer
        handle.connect_peer(self.peer_endpoint)

        alert = self.wait_for(lt.peer_blocked_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.ip_block, "peer_blocked")
        self.assert_torrent_alert(alert, handle)
        self.assert_peer_alert(alert, self.peer_endpoint, pid=lt.sha1_hash())
        if lt.api_version < 2:
            self.assertEqual(alert.ip, self.peer_endpoint)
        self.assertEqual(alert.reason, lt.reason_t.ip_filter)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        # block all ips
        ipf = lt.ip_filter()
        ipf.add_rule("0.0.0.0", "255.255.255.255", 1)
        self.session.set_ip_filter(ipf)
        # add the torrent
        handle = self.session.add_torrent(self.atp)
        # add on the peer
        self.peer.add_torrent(self.peer_atp)
        # connect us to peer
        handle.connect_peer(self.peer_endpoint)

        alert = self.wait_for(lt.peer_blocked_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.ip_block, "peer_blocked")
        with self.assertWarns(DeprecationWarning):
            self.assertEqual(alert.ip, self.peer_endpoint)


class ScrapeReplyAlertTest(TrackerAlertTest):
    ALERT_MASK = lt.alert_category.tracker

    def test_scrape_reply_alert(self) -> None:
        self.tracker_response = {
            b"files": {
                self.torrent.sha1_hash.to_bytes(): {
                    b"complete": 123,
                    b"incomplete": 234,
                    b"downloaded": 345,
                    b"downloaders": 456,
                }
            }
        }
        handle = self.session.add_torrent(self.atp)
        handle.add_tracker({"url": self.tracker_url})
        handle.scrape_tracker()

        alert = self.wait_for(lt.scrape_reply_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.tracker, "scrape_reply")
        self.assert_torrent_alert(alert, handle)
        self.assert_tracker_alert(alert)
        self.assertEqual(alert.complete, 123)
        self.assertEqual(alert.incomplete, 234)


class ScrapeFailedAlertTest(TrackerAlertTest):
    ALERT_MASK = lt.alert_category.tracker

    def test_scrape_failed_alert(self) -> None:
        # leave the default empty tracker reply, to trigger an error
        handle = self.session.add_torrent(self.atp)
        handle.add_tracker({"url": self.tracker_url})
        handle.scrape_tracker()

        alert = self.wait_for(lt.scrape_failed_alert, timeout=5)

        self.assert_alert(
            alert, lt.alert_category.tracker | lt.alert_category.error, "scrape_failed"
        )
        self.assert_torrent_alert(alert, handle)
        self.assert_tracker_alert(alert)
        if lt.api_version < 2:
            self.assertEqual(alert.msg, alert.error.message())
        self.assertEqual(alert.error_message(), "")
        self.assertEqual(alert.error.category(), lt.libtorrent_category())
        self.assertEqual(alert.error.value(), 174)  # invalid files entry


class UdpErrorAlertTest(AlertTest):
    ALERT_MASK = lt.alert_category.error

    @unittest.skip("TODO: how do we test this?")
    def test_udp_error_alert(self) -> None:
        raise NotImplementedError


class ExternalIpAlertTest(TrackerAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_external_ip_alert(self) -> None:
        self.tracker_response = {
            b"external ip": b"\x01\x02\x03\x04",
        }
        handle = self.session.add_torrent(self.atp)
        handle.add_tracker({"url": self.tracker_url})

        alert = self.wait_for(lt.external_ip_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "external_ip")
        self.assertEqual(alert.external_address, "1.2.3.4")


class SaveResumeDataAlertTest(TorrentAlertTest):
    def test_save_resume_data_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.save_resume_data()

        alert = self.wait_for(lt.save_resume_data_alert, timeout=5)
        self.assert_alert(alert, lt.alert_category.storage, "save_resume_data")
        self.assert_torrent_alert(alert, handle)
        self.assertIsInstance(alert.params, lt.add_torrent_params)
        self.assertEqual(alert.params.info_hashes.v1, self.torrent.sha1_hash)
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                atp = lt.read_resume_data(lt.bencode(alert.resume_data))
            self.assertEqual(atp.info_hashes.v1, self.torrent.sha1_hash)


class FileCompletedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.file_progress

    def test_file_completed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        wait_until_done_checking(handle, timeout=5)
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)

        alert = self.wait_for(lt.file_completed_alert, timeout=5)

        self.assert_alert(
            alert,
            lt.alert_category.file_progress | maybe_progress_notification,
            "file_completed",
        )
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.index, 0)


class FileRenamedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    def test_file_renamed_alert_test(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.rename_file(0, "other.txt")

        alert = self.wait_for(lt.file_renamed_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.storage, "file_renamed")
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.index, 0)
        if lt.api_version < 2:
            self.assertEqual(alert.name, "other.txt")
        self.assertEqual(alert.new_name(), "other.txt")
        ti = self.atp.ti
        assert ti is not None
        self.assertEqual(alert.old_name(), ti.layout().file_path(0))

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.rename_file(0, "other.txt")

        alert = self.wait_for(lt.file_renamed_alert, timeout=5)

        with self.assertWarns(DeprecationWarning):
            self.assertEqual(alert.name, "other.txt")


class FileRenameFailedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    def test_file_rename_failed_alert_test(self) -> None:
        handle = self.session.add_torrent(self.atp)
        wait_until_done_checking(handle, timeout=5)
        # add the data
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)
        # mkdir at the new path
        new = os.path.join(self.dir.name, "other.txt")
        os.mkdir(new)
        # attempt to rename over the dir
        handle.rename_file(0, "other.txt")

        alert = self.wait_for(lt.file_rename_failed_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.storage, "file_rename_failed")
        self.assert_torrent_alert(alert, handle)
        self.assertEqual(alert.index, 0)
        # NB: posix storage results in different error codes than mmap storage
        self.assertIsInstance(alert.error.category(), lt.error_category)
        self.assertIsInstance(alert.error.value(), int)
        self.assertNotEqual(alert.error.value(), 0)


class TorrentResumedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_torrent_resumed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.pause()
        handle.resume()

        alert = self.wait_for(lt.torrent_resumed_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "torrent_resumed")
        self.assert_torrent_alert(alert, handle)


class StateChangedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.status

    def test_state_changed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)

        alert = self.wait_for(lt.state_changed_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "state_changed")
        self.assert_torrent_alert(alert, handle)
        self.assertIsInstance(alert.state, lt.torrent_status.states)
        self.assertIsInstance(alert.prev_state, lt.torrent_status.states)
        self.assertNotEqual(alert.state, alert.prev_state)


class StateUpdateAlertTest(TorrentAlertTest):
    def test_state_update_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        self.session.post_torrent_updates()

        alert = self.wait_for(lt.state_update_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "state_update")
        self.assertEqual(len(alert.status), 1)
        self.assertEqual(alert.status[0].handle, handle)


class I2pAlertTest(AlertTest):
    ALERT_MASK = lt.alert_category.error

    def test_i2p_alert(self) -> None:
        self.session.apply_settings({"i2p_hostname": "127.1.2.3"})

        alert = self.wait_for(lt.i2p_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.error, "i2p")
        self.assertEqual(alert.error.category(), lt.system_category())
        self.assertIsInstance(alert.error.value(), int)
        self.assertNotEqual(alert.error.value(), 0)


class DhtAlertTest(PeerAlertTest):
    def setUp(self) -> None:
        super().setUp()
        # Configure the peer to start dht, but with no routers. Add the dht
        # category to their alert mask, so we can wait for them to be done
        # bootstrapping
        peer_alert_mask = self.peer.get_settings()["alert_mask"]
        self.peer.apply_settings(
            {"enable_dht": True, "alert_mask": peer_alert_mask | lt.alert_category.dht}
        )
        # We will ping our routers during bootstrap, so make sure we wait for
        # the peer to be done bootstrapping before we bootstrap ourselves
        for _ in lib.loop_until_timeout(5, msg="dht_bootstrap_alert"):
            self.peer.wait_for_alert(100)
            alerts = self.peer.pop_alerts()
            done = any(isinstance(a, lt.dht_bootstrap_alert) for a in alerts)
            for a in alerts:
                logging.debug("peer: %s: %s", a.what(), a.message())
            del alerts
            if done:
                break

        alert_mask = self.session.get_settings()["alert_mask"]
        self.session.apply_settings(
            {
                "enable_dht": True,
                "dht_bootstrap_nodes": self.peer_endpoint_str,
                "alert_mask": alert_mask | lt.alert_category.dht,
            }
        )
        # DHT functions won't work until our bootstrap is done
        self.bootstrap_alert = self.wait_for(lt.dht_bootstrap_alert, timeout=5)


class DhtReplyAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht

    @unittest.skip("TODO: why isn't this triggered?")
    def test_dht_reply_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        self.peer.add_torrent(self.peer_atp)
        handle.force_dht_announce()

        alert = self.wait_for(lt.dht_reply_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht, "dht_reply")
        self.assertEqual(alert.num_peers, 1)


class DhtAnnounceAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht

    def test_dht_announce_alert(self) -> None:
        self.session.add_torrent(self.atp)
        self.peer.add_torrent(self.peer_atp)

        alert = self.wait_for(lt.dht_announce_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht, "dht_announce")
        # The peer returns us as a peer, so we may get announces from ourselves
        self.assertIn(alert.ip, (self.peer_endpoint[0], self.endpoint[0]))
        self.assertIn(alert.port, (self.peer_endpoint[1], self.endpoint[1]))
        self.assertEqual(alert.info_hash, self.torrent.sha1_hash)


class DhtGetPeersAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht

    def test_dht_get_peers_alert(self) -> None:
        self.session.add_torrent(self.atp)
        self.peer.add_torrent(self.peer_atp)

        alert = self.wait_for(lt.dht_get_peers_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht, "dht_get_peers")
        # DHT nodes apparently send lots of get_peers requests, not always for
        # a torrent's info-hash. We could wait for one that actually matches,
        # but I think this is sufficient
        self.assertIsInstance(alert.info_hash, lt.sha1_hash)
        self.assertFalse(alert.info_hash.is_all_zeros())


class PeerUnsnubbedAlertTest(PeerAlertTest):
    @unittest.skip("TODO: how do we test this?")
    def test_peer_unsnubbed_alert(self) -> None:
        raise NotImplementedError


class PeerSnubbedAlertTest(PeerAlertTest):
    @unittest.skip("TODO: how do we test this?")
    def test_peer_snubbed_alert(self) -> None:
        raise NotImplementedError


class PeerConnectAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.connect

    def test_peer_connect_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.connect_peer(self.peer_endpoint)
        alert = self.wait_for(lt.peer_connect_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.connect, "peer_connect")
        self.assert_torrent_alert(alert, handle)
        self.assert_peer_alert(alert, self.peer_endpoint, pid=lt.sha1_hash())

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.connect_peer(self.peer_endpoint)
        alert = self.wait_for(lt.peer_connect_alert, timeout=5)
        with self.assertWarns(DeprecationWarning):
            self.assertEqual(alert.ip, self.peer_endpoint)


class PeerDisconnectedAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.connect

    def test_peer_disconnected_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        peer_handle = self.peer.add_torrent(self.peer_atp)
        handle.connect_peer(self.peer_endpoint)
        self.wait_for(lt.peer_connect_alert, timeout=5)
        self.peer.remove_torrent(peer_handle)
        alert = self.wait_for(lt.peer_disconnected_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.connect, "peer_disconnected")
        self.assert_torrent_alert(alert, handle)
        self.assert_peer_alert(alert, self.peer_endpoint)
        self.assertIsInstance(alert.socket_type, lt.socket_type_t)
        self.assertIsInstance(alert.op, lt.operation_t)
        self.assertIsInstance(alert.error.category(), lt.error_category)
        self.assertIsInstance(alert.error.value(), int)
        self.assertNotEqual(alert.error.value(), 0)
        self.assertIsInstance(alert.reason, lt.close_reason_t)
        if lt.api_version < 2:
            self.assertEqual(alert.msg, alert.error.message())

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        if lt.api_version < 2:
            handle = self.session.add_torrent(self.atp)
            peer_handle = self.peer.add_torrent(self.peer_atp)
            handle.connect_peer(self.peer_endpoint)
            self.peer.remove_torrent(peer_handle)

            alert = self.wait_for(lt.peer_disconnected_alert, timeout=15)

            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.msg, alert.error.message())


class RequestDroppedAlertTest(PeerAlertTest):
    @unittest.skip("TODO: is this dead?")
    def test_request_dropped_alert(self) -> None:
        raise NotImplementedError


class BlockTimeoutAlertTest(PeerAlertTest):
    @unittest.skip("TODO: how do we test this?")
    def test_block_timeout_alert(self) -> None:
        raise NotImplementedError


class UnwantedBlockAlertTest(PeerAlertTest):
    @unittest.skip("TODO: how do we test this?")
    def test_unwanted_block_alert(self) -> None:
        raise NotImplementedError


class TorrentDeleteFailedAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    def test_torrent_delete_failed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        wait_until_done_checking(handle, timeout=5)
        # add the data
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)
        # wait until written
        for _ in lib.loop_until_timeout(5, msg="write"):
            try:
                with open(self.file_path, mode="rb") as fp:
                    if fp.read() == self.torrent.files[0].data:
                        break
            except FileNotFoundError:
                pass

        # Change the file to a non-empty directory, so libtorrent won't be able
        # to delete it.
        for _ in lib.loop_until_timeout(5):
            # On Windows, we expect anti-virus software to prevent us from
            # deleting the file for a short time.
            try:
                os.unlink(self.file_path)
                break
            except PermissionError:
                pass
        os.mkdir(self.file_path)
        with open(os.path.join(self.file_path, "test.txt"), mode="wb") as fp:
            pass

        # TODO: https://github.com/arvidn/libtorrent/issues/6158 : we'd like to
        # supply delete_files|delete_partfile here for safety, but it doesn't
        # currently work.
        self.session.remove_torrent(handle, option=lt.session.delete_files)

        alert = self.wait_for(lt.torrent_delete_failed_alert, timeout=5)

        self.assert_alert(
            alert,
            lt.alert_category.storage | lt.alert_category.error,
            "torrent_delete_failed",
        )
        self.assert_torrent_alert(alert, handle)
        if lt.api_version < 2:
            self.assertEqual(alert.msg, alert.error.message())
        error_value = alert.error.value()
        # On non-Windows, the error value is an errno. On Windows it's a
        # "winerror" value. This uses python's winerror-to-errno translation on
        # Windows only.
        errno_value = OSError(error_value, "", None, error_value).errno
        self.assertEqual(errno_value, errno.ENOTEMPTY)
        self.assertEqual(alert.error.category(), lt.system_category())
        if lt.api_version < 3:
            self.assertEqual(alert.info_hash, self.torrent.sha1_hash)
        self.assertEqual(alert.info_hashes.v1, self.torrent.sha1_hash)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        wait_until_done_checking(handle, timeout=5)
        # add the data
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)
        # wait until written
        for _ in lib.loop_until_timeout(5, msg="write"):
            with open(self.file_path, mode="rb") as fp:
                if fp.read() == self.torrent.files[0].data:
                    break

        # Change the file to a non-empty directory, so libtorrent won't be able
        # to delete it.
        for _ in lib.loop_until_timeout(5):
            # On Windows, we expect anti-virus software to prevent us from
            # deleting the file for a short time.
            try:
                os.unlink(self.file_path)
                break
            except PermissionError:
                pass
        os.mkdir(self.file_path)
        with open(os.path.join(self.file_path, "test.txt"), mode="wb") as fp:
            pass

        # remove the torrent and delete the torrent
        # TODO: https://github.com/arvidn/libtorrent/issues/6158 : we'd like to
        # supply delete_files|delete_partfile here for safety, but it doesn't
        # currently work.
        self.session.remove_torrent(handle, option=lt.session.delete_files)

        alert = self.wait_for(lt.torrent_delete_failed_alert, timeout=5)

        with self.assertWarns(DeprecationWarning):
            self.assertEqual(alert.msg, alert.error.message())
        with self.assertWarns(DeprecationWarning):
            self.assertEqual(alert.info_hash, self.torrent.sha1_hash)


class SaveResumeDataFailedAlertTest(TorrentAlertTest):
    def test_save_resume_data_failed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.save_resume_data()
        handle.save_resume_data(flags=lt.save_resume_flags_t.only_if_modified)

        alert = self.wait_for(lt.save_resume_data_failed_alert, timeout=5)

        self.assert_alert(
            alert,
            lt.alert_category.storage | lt.alert_category.error,
            "save_resume_data_failed",
        )
        self.assert_torrent_alert(alert, handle)
        if lt.api_version < 2:
            self.assertEqual(alert.msg, alert.error.message())
        self.assertEqual(alert.error.category(), lt.libtorrent_category())
        self.assertEqual(alert.error.value(), 143)  # not modified since last save

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.save_resume_data()
        handle.save_resume_data(flags=lt.save_resume_flags_t.only_if_modified)

        alert = self.wait_for(lt.save_resume_data_failed_alert, timeout=5)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(alert.msg, alert.error.message())


class PerformanceAlertTest(AlertTest):
    ALERT_MASK = lt.alert_category.performance_warning

    def test_performance_alert(self) -> None:
        # create some inconsistent settings
        self.session.apply_settings(
            {"num_optimistic_unchoke_slots": 100, "unchoke_slots_limit": 100}
        )

        alert = self.wait_for(lt.performance_alert, timeout=5)
        self.assert_alert(alert, lt.alert_category.performance_warning, "performance")
        self.assertEqual(
            alert.warning_code,
            lt.performance_warning_t.too_many_optimistic_unchoke_slots,
        )
        self.assertFalse(alert.handle.is_valid())


class StatsAlert(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.stats

    def test_stats_alert(self) -> None:
        if lt.api_version < 3:
            handle = self.session.add_torrent(self.atp)

            alert = self.wait_for(lt.stats_alert, timeout=5)

            self.assert_alert(alert, lt.alert_category.stats, "stats")
            self.assert_torrent_alert(alert, handle)
            self.assertEqual(alert.transferred, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
            self.assertGreater(alert.interval, 0)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        if lt.api_version < 3:
            with self.assertWarns(DeprecationWarning):
                self.assertTrue(issubclass(lt.stats_alert, lt.torrent_alert))


class CacheFlushedAlert(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    def test_cache_flushed_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.flush_cache()

        alert = self.wait_for(lt.cache_flushed_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.storage, "cache_flushed")
        self.assert_torrent_alert(alert, handle)


class AnonymousModeAlertTest(TorrentAlertTest):
    @unittest.skip("TODO: is this dead?")
    def test_anonymous_mode_alert(self) -> None:
        raise NotImplementedError


class IncomingConnectionAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.peer

    def test_incoming_connection_alert(self) -> None:
        self.session.add_torrent(self.atp)
        peer_handle = self.peer.add_torrent(self.peer_atp)
        peer_handle.connect_peer(self.endpoint)

        alert = self.wait_for(lt.incoming_connection_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.peer, "incoming_connection")
        self.assertIsInstance(alert.socket_type, lt.socket_type_t)
        if lt.api_version < 2:
            self.assertEqual(alert.ip, self.peer_endpoint)
        self.assertEqual(alert.endpoint, self.peer_endpoint)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        self.session.add_torrent(self.atp)
        peer_handle = self.peer.add_torrent(self.peer_atp)
        peer_handle.connect_peer(self.endpoint)

        alert = self.wait_for(lt.incoming_connection_alert, timeout=5)

        with self.assertWarns(DeprecationWarning):
            self.assertEqual(alert.ip, self.peer_endpoint)


class TorrentNeedCertAlertTest(TorrentAlertTest):
    @unittest.skip("TODO: how do we test this?")
    def test_torrent_need_cert_alert(self) -> None:
        raise NotImplementedError


class AddTorrentAlertTest(TorrentAlertTest):
    def test_torrent_alert_properties(self) -> None:
        handle = self.session.add_torrent(self.atp)

        alert = self.wait_for(lt.add_torrent_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "add_torrent")
        self.assert_torrent_alert(alert, handle)


class DhtOutgoingGetPeersAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht

    def test_outgoing_get_peers_alert(self) -> None:
        self.session.add_torrent(self.atp)
        self.peer.add_torrent(self.peer_atp)

        alert = self.wait_for(lt.dht_outgoing_get_peers_alert, timeout=10)

        self.assert_alert(alert, lt.alert_category.dht, "dht_outgoing_get_peers")
        self.assertIsInstance(alert.info_hash, lt.sha1_hash)
        self.assertFalse(alert.info_hash.is_all_zeros())
        self.assertIsInstance(alert.obfuscated_info_hash, lt.sha1_hash)
        self.assertFalse(alert.obfuscated_info_hash.is_all_zeros())
        # Our peer may return us as a peer, so we may get alerts for querying
        # ourselves
        if lt.api_version < 2:
            self.assertIn(alert.ip, (self.peer_endpoint, self.endpoint))
        self.assertIn(alert.endpoint, (self.peer_endpoint, self.endpoint))

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5967")
    def test_deprecated(self) -> None:
        self.session.add_torrent(self.atp)
        self.peer.add_torrent(self.peer_atp)

        alert = self.wait_for(lt.dht_outgoing_get_peers_alert, timeout=10)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIn(alert.ip, (self.peer_endpoint, self.endpoint))


class LogAlertTest(AlertTest):
    ALERT_MASK = lt.alert_category.session_log

    def test_log_alert(self) -> None:
        alert = self.wait_for(lt.log_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.session_log, "log")
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(alert.msg(), str)
            with self.assertWarns(DeprecationWarning):
                self.assertNotEqual(alert.msg(), "")
        self.assertIsInstance(alert.log_message(), str)
        self.assertNotEqual(alert.log_message(), "")


class PeerLogAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.peer_log

    def test_peer_log_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.connect_peer(self.peer_endpoint)

        alert = self.wait_for(lt.peer_log_alert, timeout=5)
        self.assert_alert(alert, lt.alert_category.peer_log, "peer_log")
        self.assert_torrent_alert(alert, handle)
        self.assert_peer_alert(alert, self.peer_endpoint, pid=lt.sha1_hash())
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(alert.msg(), str)
            with self.assertWarns(DeprecationWarning):
                self.assertNotEqual(alert.msg(), "")
        self.assertIsInstance(alert.log_message(), str)
        self.assertNotEqual(alert.log_message(), "")


class PickerLogAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.picker_log

    def test_picker_log_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        peer_handle = self.peer.add_torrent(self.peer_atp)
        wait_until_done_checking(peer_handle, timeout=5)
        for i, piece in enumerate(self.torrent.pieces):
            peer_handle.add_piece(i, piece, 0)
        handle.connect_peer(self.peer_endpoint)

        alert = self.wait_for(lt.picker_log_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.picker_log, "picker_log")
        self.assert_torrent_alert(alert, handle)
        self.assert_peer_alert(
            alert, self.peer_endpoint, fingerprint=self.peer_fingerprint
        )
        self.assertIsInstance(alert.picker_flags, int)
        blocks = alert.blocks()
        self.assertGreater(len(blocks), 0)
        num_pieces = len(self.torrent.pieces)
        for piece_idx, block_idx in blocks:
            self.assertIsInstance(piece_idx, int)
            self.assertIsInstance(block_idx, int)
            self.assertGreaterEqual(piece_idx, 0)
            self.assertLess(piece_idx, num_pieces)
            self.assertGreaterEqual(block_idx, 0)


class LsdErrorAlertTest(AlertTest):
    @unittest.skip("TODO: how do we test this?")
    def test_lsd_error_alert(self) -> None:
        raise NotImplementedError


class DhtStatsAlertTest(DhtAlertTest):
    def test_dht_stats_alert(self) -> None:
        self.session.post_dht_stats()

        alert = self.wait_for(lt.dht_stats_alert, timeout=5)
        self.assert_alert(alert, 0, "dht_stats")
        self.assertEqual(alert.active_requests, [])
        self.assertEqual(alert.routing_table, [{"num_nodes": 0, "num_replacements": 0}])


class DhtLogAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht_log

    def test_dht_alert_log_alert(self) -> None:
        # Generate some logs
        self.session.dht_announce(lt.sha1_hash(b"a" * 20))

        alert = self.wait_for(lt.dht_log_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht_log, "dht_log")
        self.assertIsInstance(alert.module, lt.dht_module_t)
        self.assertIsInstance(alert.log_message(), str)
        self.assertNotEqual(alert.log_message(), "")


class DhtPktAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht_log

    def test_dht_pkt_alert(self) -> None:
        # Generate some packets
        self.session.dht_announce(lt.sha1_hash(b"a" * 20))

        alert = self.wait_for(lt.dht_pkt_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht_log, "dht_pkt")
        self.assertIsInstance(alert.pkt_buf, bytes)
        self.assertNotEqual(alert.pkt_buf, b"")
        # Ensure it's in bencoded form
        lt.bencode(alert.pkt_buf)


class DhtImmutableItemAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht

    def test_dht_immutable_item_alert(self) -> None:
        item = {b"test": b"test"}
        sha1 = self.session.dht_put_immutable_item(cast("_Entry", item))
        # Wait for the put to complete, or the get will short-circuit
        self.wait_for(lt.dht_put_alert, timeout=5)
        self.session.dht_get_immutable_item(sha1)

        alert = self.wait_for(lt.dht_immutable_item_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht, "dht_immutable_item")
        self.assertEqual(alert.target, sha1)
        self.assertEqual(alert.item, item)


class DhtMutableItemAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht

    def test_dht_mutable_item_alert(self) -> None:
        # NB: libtorrent uses private keys in hased-and-exponentiated form, not
        # concatenated <seed><pubkey> form used by some implementations
        pk = bytes.fromhex(
            "77ff84905a91936367c01360803104f92432fcd904a43511876df5cdf3e7e548"
        )
        sk = bytes.fromhex(
            "e06d3183d14159228433ed599221b80bd0a5ce8352e4bdf0262f76786ef1c74d"
            "b7e7a9fea2c0eb269d61e3b38e450a22e754941ac78479d6c54e1faf6037881d"
        )
        data = b"test"
        salt = b"salt"
        # Put the item, and wait for success
        self.session.dht_put_mutable_item(sk, pk, data, salt)
        self.wait_for(lt.dht_put_alert, timeout=5)
        self.session.dht_get_mutable_item(pk, salt)

        alert = self.wait_for(lt.dht_mutable_item_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht, "dht_mutable_item")
        self.assertEqual(alert.key, pk)
        self.assertEqual(alert.item, data)
        self.assertEqual(
            alert.signature,
            bytes.fromhex(
                "aeaa269b37bab7f87126c8d0cc31fb9c88ea81f0d0d5338166d103be8ea16f16"
                "2d017ce812926a24dc697beb6915bec986e9eb0a37f262150cd90f2add54f805"
            ),
        )
        self.assertEqual(alert.salt, salt)
        self.assertEqual(alert.seq, 1)
        self.assertFalse(alert.authoritative)


class DhtPutAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht

    def test_dht_put_alert_with_immutable(self) -> None:
        item = {b"test": b"test"}
        sha1 = self.session.dht_put_immutable_item(cast("_Entry", item))

        alert = self.wait_for(lt.dht_put_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht, "dht_put")
        self.assertEqual(alert.target, sha1)
        self.assertEqual(alert.public_key, b"\0" * 32)
        self.assertEqual(alert.signature, b"\0" * 64)
        self.assertEqual(alert.salt, b"")
        self.assertEqual(alert.seq, 0)
        # In our current setup, our peer returns us as a neighbor of itself,
        # so we end up forwarding the put request to ourselves, so
        # num_success is actually 2. This may change in the future, but it
        # should at least be 1.
        self.assertGreater(alert.num_success, 0)

    def test_dht_put_alert_with_mutable(self) -> None:
        # NB: libtorrent uses private keys in hased-and-exponentiated form, not
        # concatenated <seed><pubkey> form used by some implementations
        pk = bytes.fromhex(
            "77ff84905a91936367c01360803104f92432fcd904a43511876df5cdf3e7e548"
        )
        sk = bytes.fromhex(
            "e06d3183d14159228433ed599221b80bd0a5ce8352e4bdf0262f76786ef1c74d"
            "b7e7a9fea2c0eb269d61e3b38e450a22e754941ac78479d6c54e1faf6037881d"
        )
        data = b"test"
        salt = b"salt"
        self.session.dht_put_mutable_item(sk, pk, data, salt)

        alert = self.wait_for(lt.dht_put_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht, "dht_put")
        self.assertEqual(alert.target, lt.sha1_hash())
        self.assertEqual(alert.public_key, pk)
        self.assertEqual(
            alert.signature,
            bytes.fromhex(
                "aeaa269b37bab7f87126c8d0cc31fb9c88ea81f0d0d5338166d103be8ea16f16"
                "2d017ce812926a24dc697beb6915bec986e9eb0a37f262150cd90f2add54f805"
            ),
        )
        self.assertEqual(alert.salt, salt)
        self.assertEqual(alert.seq, 1)
        # In our current setup, our peer returns us as a neighbor of itself,
        # so we end up forwarding the put request to ourselves, so
        # num_success is actually 2. This may change in the future, but it
        # should at least be 1.
        self.assertGreater(alert.num_success, 0)


class SessionStatsAlertTest(AlertTest):
    def test_session_stats_alert(self) -> None:
        self.session.post_session_stats()

        alert = self.wait_for(lt.session_stats_alert, timeout=5)

        self.assert_alert(alert, 0, "session_stats")
        self.assertIsInstance(alert.values, dict)
        self.assertEqual(
            set(alert.values.keys()),
            set(metric.name for metric in lt.session_stats_metrics()),
        )
        self.assertEqual(
            [v for v in alert.values.values() if not isinstance(v, int)], []
        )


class SessionStatsHeaderAlertTest(AlertTest):
    def test_session_stats_alert(self) -> None:
        self.session.post_session_stats()

        alert = self.wait_for(lt.session_stats_header_alert, timeout=5)

        self.assert_alert(alert, 0, "session_stats_header")


class DhtGetPeersReplyAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht_operation

    def test_get_peers_dht_reply_alert(self) -> None:
        self.session.add_torrent(self.atp)
        # Wait for the peer to reflect the announce back to us
        self.wait_for(lt.dht_announce_alert, timeout=5)
        self.session.dht_get_peers(self.torrent.sha1_hash)

        alert = self.wait_for(lt.dht_get_peers_reply_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht_operation, "dht_get_peers_reply")
        self.assertEqual(alert.info_hash, self.torrent.sha1_hash)
        self.assertEqual(alert.num_peers(), 1)
        self.assertEqual(alert.peers(), [self.endpoint])


class BlockUploadedAlertTest(PeerAlertTest):
    ALERT_MASK = lt.alert_category.upload

    def test_block_uploaded_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        wait_until_done_checking(handle, timeout=5)
        for i, piece in enumerate(self.torrent.pieces):
            handle.add_piece(i, piece, 0)
        peer_handle = self.peer.add_torrent(self.peer_atp)
        peer_handle.connect_peer(self.endpoint)

        alert = self.wait_for(lt.block_uploaded_alert, timeout=5)

        self.assert_alert(
            alert,
            lt.alert_category.upload | maybe_progress_notification,
            "block_uploaded",
        )
        self.assertIsInstance(alert.block_index, int)
        self.assertIsInstance(alert.piece_index, int)


class AlertsDroppedAlertTest(AlertTest):
    def test_alerts_dropped_alert(self) -> None:
        self.session.apply_settings({"alert_queue_size": 1})
        self.session.pop_alerts()
        for _ in range(100):
            self.session.post_session_stats()

        alert = self.wait_for(lt.alerts_dropped_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.error, "alerts_dropped")
        # The alert types aren't mapped anywhere, but this should indicate
        # session_stats_alert
        self.assertEqual(alert.dropped_alerts.index(True), 70)


class Socks5AlertTest(AlertTest):
    ALERT_MASK = lt.alert_category.error

    @unittest.skipIf(sys.platform == "darwin", "TODO: doesn't fire on osx")
    def test_socks5_alert(self) -> None:
        self.session.apply_settings(
            {
                "proxy_type": lt.proxy_type_t.socks5,
                "proxy_hostname": "127.1.2.3",
                "proxy_port": 12345,
            }
        )

        alert = self.wait_for(lt.socks5_alert, timeout=15)

        self.assert_alert(alert, lt.alert_category.error, "socks5")
        self.assertIsInstance(alert.error.value(), int)
        self.assertNotEqual(alert.error.value(), 0)
        self.assertEqual(alert.error.category(), lt.system_category())
        self.assertEqual(alert.op, lt.operation_t.connect)
        self.assertEqual(alert.ip, ("127.1.2.3", 12345))


class FilePrioAlertTest(TorrentAlertTest):
    ALERT_MASK = lt.alert_category.storage

    def test_file_prio_alert(self) -> None:
        handle = self.session.add_torrent(self.atp)
        handle.file_priority(0, 0)

        alert = self.wait_for(lt.file_prio_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.storage, "file_prio")
        self.assert_torrent_alert(alert, handle)


class DhtLiveNodesAlertTest(DhtAlertTest):
    def test_dht_live_nodes_alert(self) -> None:
        self.session.dht_live_nodes(self.torrent.sha1_hash)

        alert = self.wait_for(lt.dht_live_nodes_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.dht, "dht_live_nodes")
        self.assertEqual(alert.node_id, self.torrent.sha1_hash)
        self.assertEqual(alert.num_nodes, 0)
        self.assertEqual(alert.nodes, [])


class DhtSampleInfohashesAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht_operation

    def test_dht_sample_infohashes_alert(self) -> None:
        self.session.add_torrent(self.peer_atp)
        # Wait for the peer to reflect the announce back to us
        self.wait_for(lt.dht_announce_alert, timeout=5)
        self.session.dht_sample_infohashes(self.peer_endpoint, lt.sha1_hash())

        alert = self.wait_for(lt.dht_sample_infohashes_alert, timeout=5)

        self.assert_alert(
            alert, lt.alert_category.dht_operation, "dht_sample_infohashes"
        )
        self.assertEqual(alert.endpoint, self.peer_endpoint)
        self.assertEqual(
            alert.interval,
            datetime.timedelta(
                seconds=self.session.get_settings()["dht_sample_infohashes_interval"]
            ),
        )
        self.assertEqual(alert.num_infohashes, 1)
        self.assertEqual(alert.num_samples, 1)
        self.assertEqual(alert.samples, [self.torrent.sha1_hash])
        self.assertEqual(alert.num_nodes, 1)
        # TODO: how do we get our node id, to make this simpler?
        self.assertEqual(len(alert.nodes), 1)
        self.assertEqual(alert.nodes[0]["endpoint"], self.endpoint)
        self.assertIsInstance(alert.nodes[0]["nid"], lt.sha1_hash)
        self.assertFalse(alert.nodes[0]["nid"].is_all_zeros())


class DhtBootstrapAlertTest(DhtAlertTest):
    ALERT_MASK = lt.alert_category.dht

    def test_dht_bootstrap_alert(self) -> None:
        self.assert_alert(self.bootstrap_alert, lt.alert_category.dht, "dht_bootstrap")


class SSLTrackerAlertTest(TrackerAlertTest):
    ALERT_MASK = lt.alert_category.status

    def get_tracker_ssl_ctx(self) -> ssl.SSLContext:
        ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
        cert_path = (
            pathlib.Path(__file__).parent.parent.parent.parent
            / "test"
            / "ssl"
            / "server.pem"
        )
        ctx.load_cert_chain(cert_path)
        return ctx

    def setUp(self) -> None:
        super().setUp()

        # I couldn't get validation to work on all platforms. Setting
        # SSL_CERT_FILE to our self-signed cert works on linux and mac, but
        # not on Windows.
        self.session.apply_settings({"validate_https_trackers": False})

    def test_external_ip_alert_via_ssl_tracker(self) -> None:
        self.tracker_response = {
            b"external ip": b"\x01\x02\x03\x04",
        }
        handle = self.session.add_torrent(self.atp)
        handle.add_tracker({"url": self.tracker_url})

        alert = self.wait_for(lt.external_ip_alert, timeout=5)

        self.assert_alert(alert, lt.alert_category.status, "external_ip")
        self.assertEqual(alert.external_address, "1.2.3.4")
