import os
import pathlib
import socket
import sys
import tempfile
from typing import Any
from typing import cast
from typing import Dict
from typing import TYPE_CHECKING
import unittest
import warnings

import libtorrent as lt

from . import lib
from . import tdummy

if TYPE_CHECKING:
    from libtorrent import _Entry
    from libtorrent import AddTorrentParamsdict
    from libtorrent import load_torrent_limits
    from libtorrent import settings_pack
    from libtorrent import TorrentFileDict
else:
    AddTorrentParamsdict, TorrentFileDict, load_torrent_limits, settings_pack = (
        dict,
        dict,
        dict,
        dict,
    )


class ReadWriteSessionParamsTest(unittest.TestCase):
    def test_read_bdecoded(self) -> None:
        params = lt.session_params({"alert_mask": 123})
        bdecoded = lt.write_session_params(params)

        read_params = lt.read_session_params(bdecoded)
        self.assertIsInstance(read_params, lt.session_params)
        # self.assertEqual(read_params, params)
        self.assertEqual(read_params.settings, params.settings)

        read_params = lt.read_session_params(bdecoded, flags=0)
        self.assertIsInstance(read_params, lt.session_params)
        # self.assertEqual(read_params, lt.session_params())
        self.assertEqual(read_params.settings, lt.session_params().settings)

    def test_read_bencoded(self) -> None:
        params = lt.session_params({"alert_mask": 123})
        bencoded = lt.bencode(lt.write_session_params(params))

        read_params = lt.read_session_params(bencoded)
        self.assertIsInstance(read_params, lt.session_params)
        # self.assertEqual(read_params, params)
        self.assertEqual(read_params.settings, params.settings)

        read_params = lt.read_session_params(bencoded, flags=0)
        self.assertIsInstance(read_params, lt.session_params)
        # self.assertEqual(read_params, lt.session_params())
        self.assertEqual(read_params.settings, lt.session_params().settings)

    def test_write(self) -> None:
        params = lt.session_params({"alert_mask": 123})

        bdecoded = lt.write_session_params(params)
        self.assertIsInstance(bdecoded, dict)
        self.assertEqual(bdecoded, lt.write_session_params(params))

        bdecoded = lt.write_session_params(
            params, flags=lt.save_state_flags_t.save_settings
        )
        self.assertEqual(set(bdecoded.keys()), {b"settings"})

    def test_write_empty(self) -> None:
        bdecoded = lt.write_session_params(lt.session_params(), flags=0)
        self.assertIsInstance(bdecoded, dict)

    def test_round_trip(self) -> None:
        params = lt.session_params()
        params.settings = {"alert_mask": 123}

        read_params = lt.read_session_params(lt.write_session_params(params))
        self.assertIsInstance(read_params, lt.session_params)
        # self.assertEqual(params, read_params)
        self.assertEqual(params.settings, read_params.settings)


class SessionParamsTest(unittest.TestCase):
    def test_constructor(self) -> None:
        params = lt.session_params()
        # self.assertEqual(params, lt.session_params())
        self.assertEqual(params.settings, lt.session_params().settings)

        params = lt.session_params({"alert_mask": 123})
        expected = lt.session_params().settings
        expected["alert_mask"] = 123
        self.assertEqual(params.settings, expected)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6140")
    def test_equality(self) -> None:
        self.assertEqual(lt.session_params(), lt.session_params())
        self.assertEqual(
            lt.session_params({"alert_mask": 123}),
            lt.session_params({"alert_mask": 123}),
        )

    def test_settings(self) -> None:
        params = lt.session_params()
        params.settings = {"alert_mask": 123}
        expected = lt.session_params().settings
        expected["alert_mask"] = 123
        self.assertEqual(params.settings, expected)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6140")
    def test_settings_reference(self) -> None:
        params = lt.session_params()
        params.settings["alert_mask"] = 123
        self.assertEqual(params.settings["alert_mask"], 123)

    def test_ip_filter(self) -> None:
        params = lt.session_params()
        self.assertEqual(
            params.ip_filter.export_filter(),
            (
                [("0.0.0.0", "255.255.255.255")],
                [("::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")],
            ),
        )
        ipf = lt.ip_filter()
        ipf.add_rule("128.0.0.0", "128.0.0.1", 123)
        params.ip_filter = ipf
        self.assertEqual(
            params.ip_filter.export_filter(),
            (
                [
                    ("0.0.0.0", "127.255.255.255"),
                    ("128.0.0.0", "128.0.0.1"),
                    ("128.0.0.2", "255.255.255.255"),
                ],
                [("::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")],
            ),
        )

    def test_ip_filter_reference(self) -> None:
        params = lt.session_params()
        self.assertEqual(
            params.ip_filter.export_filter(),
            (
                [("0.0.0.0", "255.255.255.255")],
                [("::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")],
            ),
        )
        params.ip_filter.add_rule("128.0.0.0", "128.0.0.1", 123)
        self.assertEqual(
            params.ip_filter.export_filter(),
            (
                [
                    ("0.0.0.0", "127.255.255.255"),
                    ("128.0.0.0", "128.0.0.1"),
                    ("128.0.0.2", "255.255.255.255"),
                ],
                [("::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")],
            ),
        )

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6140")
    def test_dht_state(self) -> None:
        params = lt.session_params()
        self.assertEqual(params.dht_state, lt.dht_state())
        state = lt.dht_state()
        state.nodes = [("127.0.0.1", 1234)]  # type: ignore[misc]
        params.dht_state = state
        self.assertEqual(params.dht_state, state)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6140")
    def test_dht_state_reference(self) -> None:
        params = lt.session_params()
        self.assertEqual(params.dht_state, lt.dht_state())
        params.dht_state.nodes = [("127.0.0.1", 1234)]  # type: ignore[misc]
        self.assertEqual(params.dht_state.nodes, [("127.0.0.1", 1234)])


class DhtStateTest(unittest.TestCase):
    def test_constructor(self) -> None:
        state = lt.dht_state()
        self.assertIsInstance(state, lt.dht_state)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6140")
    def test_equality(self) -> None:
        a = lt.dht_state()
        b = lt.dht_state()
        self.assertEqual(a, b)
        a.nodes = [("127.0.0.1", 1234)]  # type: ignore[misc]
        self.assertNotEqual(a, b)
        b.nodes = [("127.0.0.1", 1234)]  # type: ignore[misc]
        self.assertNotEqual(a, b)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6140")
    def test_nids(self) -> None:
        state = lt.dht_state()
        self.assertEqual(state.nids, [])
        state.nids = [("127.0.0.1", lt.sha1_hash(b"a" * 20))]  # type: ignore[misc]
        self.assertEqual(state.nids, [("127.0.0.1", lt.sha1_hash(b"a" * 20))])

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6140")
    def test_nodes(self) -> None:
        state = lt.dht_state()
        self.assertEqual(state.nodes, [])
        state.nodes = [("127.0.0.1", 1234)]  # type: ignore[misc]
        self.assertEqual(state.nodes, [("127.0.0.1", 1234)])

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6140")
    def test_nodes6(self) -> None:
        state = lt.dht_state()
        self.assertEqual(state.nodes6, [])
        state.nodes6 = [("::1", 1234)]  # type: ignore[misc]
        self.assertEqual(state.nodes6, [("::1", 1234)])


class SessionStatusTest(unittest.TestCase):
    def test_fields(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                stat = lt.session_status()

            self.assertIsInstance(stat.has_incoming_connections, bool)

            self.assertIsInstance(stat.upload_rate, int)
            self.assertIsInstance(stat.download_rate, int)
            self.assertIsInstance(stat.total_download, int)
            self.assertIsInstance(stat.total_upload, int)

            self.assertIsInstance(stat.payload_upload_rate, int)
            self.assertIsInstance(stat.payload_download_rate, int)
            self.assertIsInstance(stat.total_payload_download, int)
            self.assertIsInstance(stat.total_payload_upload, int)

            self.assertIsInstance(stat.ip_overhead_upload_rate, int)
            self.assertIsInstance(stat.ip_overhead_download_rate, int)
            self.assertIsInstance(stat.total_ip_overhead_download, int)
            self.assertIsInstance(stat.total_ip_overhead_upload, int)

            self.assertIsInstance(stat.dht_upload_rate, int)
            self.assertIsInstance(stat.dht_download_rate, int)
            self.assertIsInstance(stat.total_dht_download, int)
            self.assertIsInstance(stat.total_dht_upload, int)

            self.assertIsInstance(stat.tracker_upload_rate, int)
            self.assertIsInstance(stat.tracker_download_rate, int)
            self.assertIsInstance(stat.total_tracker_download, int)
            self.assertIsInstance(stat.total_tracker_upload, int)

            self.assertIsInstance(stat.total_redundant_bytes, int)
            self.assertIsInstance(stat.total_failed_bytes, int)

            self.assertIsInstance(stat.num_peers, int)
            self.assertIsInstance(stat.num_unchoked, int)
            self.assertIsInstance(stat.allowed_upload_slots, int)

            self.assertIsInstance(stat.up_bandwidth_queue, int)
            self.assertIsInstance(stat.down_bandwidth_queue, int)

            self.assertIsInstance(stat.up_bandwidth_bytes_queue, int)
            self.assertIsInstance(stat.down_bandwidth_bytes_queue, int)

            self.assertIsInstance(stat.optimistic_unchoke_counter, int)
            self.assertIsInstance(stat.unchoke_counter, int)

            self.assertIsInstance(stat.dht_nodes, int)
            self.assertIsInstance(stat.dht_node_cache, int)
            self.assertIsInstance(stat.dht_torrents, int)
            self.assertIsInstance(stat.dht_global_nodes, int)
            self.assertEqual(stat.active_requests, [])
            self.assertIsInstance(stat.dht_total_allocations, int)

            with self.assertWarns(DeprecationWarning):
                utp = stat.utp_stats
            self.assertIsInstance(utp["num_idle"], int)
            self.assertIsInstance(utp["num_syn_sent"], int)
            self.assertIsInstance(utp["num_connected"], int)
            self.assertIsInstance(utp["num_fin_sent"], int)
            self.assertIsInstance(utp["num_close_wait"], int)

    def test_from_session(self) -> None:
        session = lt.session(lib.get_isolated_settings())
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                stat = session.status()
            self.assertIsInstance(stat, lt.session_status)


class AddTorrentParamsTest(unittest.TestCase):
    def test_fields(self) -> None:
        atp = lt.add_torrent_params()

        atp.version = 123
        self.assertEqual(atp.version, 123)
        atp.ti = lt.torrent_info(lt.sha1_hash())
        self.assertEqual(atp.ti.info_hashes().v1, lt.sha1_hash())
        atp.trackers = ["http://example.com/tr"]
        self.assertEqual(atp.trackers, ["http://example.com/tr"])
        atp.tracker_tiers = [1]
        self.assertEqual(atp.tracker_tiers, [1])
        atp.dht_nodes = [("0.1.2.3", 1234)]
        self.assertEqual(atp.dht_nodes, [("0.1.2.3", 1234)])
        atp.name = "test.txt"
        self.assertEqual(atp.name, "test.txt")
        atp.save_path = "."
        self.assertEqual(atp.save_path, ".")
        atp.storage_mode = lt.storage_mode_t.storage_mode_allocate
        self.assertEqual(atp.storage_mode, lt.storage_mode_t.storage_mode_allocate)
        atp.file_priorities = [1]
        self.assertEqual(atp.file_priorities, [1])
        atp.trackerid = "trackerid"
        self.assertEqual(atp.trackerid, "trackerid")
        atp.flags = lt.torrent_flags.default_flags
        self.assertEqual(atp.flags, lt.torrent_flags.default_flags)
        atp.max_uploads = 1
        self.assertEqual(atp.max_uploads, 1)
        atp.max_connections = 1
        self.assertEqual(atp.max_connections, 1)
        atp.upload_limit = 1024
        self.assertEqual(atp.upload_limit, 1024)
        atp.download_limit = 1024
        self.assertEqual(atp.download_limit, 1024)
        atp.total_uploaded = 1024
        self.assertEqual(atp.total_uploaded, 1024)
        atp.total_downloaded = 1024
        self.assertEqual(atp.total_downloaded, 1024)
        atp.active_time = 1234
        self.assertEqual(atp.active_time, 1234)
        atp.finished_time = 1234
        self.assertEqual(atp.finished_time, 1234)
        atp.seeding_time = 1234
        self.assertEqual(atp.seeding_time, 1234)
        atp.added_time = 1234
        self.assertEqual(atp.added_time, 1234)
        atp.completed_time = 1234
        self.assertEqual(atp.completed_time, 1234)
        atp.last_seen_complete = 1234
        self.assertEqual(atp.last_seen_complete, 1234)
        atp.last_download = 1234
        self.assertEqual(atp.last_download, 1234)
        atp.last_upload = 1234
        self.assertEqual(atp.last_upload, 1234)
        atp.num_complete = 10
        self.assertEqual(atp.num_complete, 10)
        atp.num_incomplete = 10
        self.assertEqual(atp.num_incomplete, 10)
        atp.num_downloaded = 10
        self.assertEqual(atp.num_downloaded, 10)
        atp.info_hashes = lt.info_hash_t(lt.sha1_hash())
        self.assertEqual(atp.info_hashes.v1, lt.sha1_hash())
        atp.url_seeds = ["http://example.com/seed"]
        self.assertEqual(atp.url_seeds, ["http://example.com/seed"])
        atp.peers = [("1.2.3.4", 4321)]
        self.assertEqual(atp.peers, [("1.2.3.4", 4321)])
        atp.banned_peers = [("2.3.4.5", 4321)]
        self.assertEqual(atp.banned_peers, [("2.3.4.5", 4321)])
        atp.unfinished_pieces = {}
        self.assertEqual(atp.unfinished_pieces, {})
        atp.have_pieces = [True, False]
        self.assertEqual(atp.have_pieces, [True, False])
        atp.verified_pieces = [True, False]
        self.assertEqual(atp.verified_pieces, [True, False])
        atp.piece_priorities = [1]
        self.assertEqual(atp.piece_priorities, [1])
        atp.renamed_files = {}
        self.assertEqual(atp.renamed_files, {})

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_name_assign_bytes_deprecated(self) -> None:
        atp = lt.add_torrent_params()

        with self.assertWarns(DeprecationWarning):
            atp.name = b"test.txt"  # type: ignore

    def test_name_assign_bytes(self) -> None:
        atp = lt.add_torrent_params()

        atp.name = b"test.txt"  # type: ignore
        self.assertEqual(atp.name, "test.txt")

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_trackerid_assign_bytes_deprecated(self) -> None:
        atp = lt.add_torrent_params()
        with self.assertWarns(DeprecationWarning):
            atp.trackerid = b"trackerid"  # type: ignore

    def test_trackerid_assign_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.trackerid = b"trackerid"  # type: ignore
        self.assertEqual(atp.trackerid, "trackerid")

    def test_save_path_ascii_str(self) -> None:
        atp = lt.add_torrent_params()
        atp.save_path = "test"
        self.assertEqual(atp.save_path, "test")
        self.assertEqual(lt.write_resume_data(atp)[b"save_path"], b"test")

    def test_save_path_ascii_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.save_path = b"test"  # type: ignore
        self.assertEqual(atp.save_path, "test")
        self.assertEqual(lt.write_resume_data(atp)[b"save_path"], b"test")

    def test_save_path_non_ascii_str(self) -> None:
        atp = lt.add_torrent_params()
        atp.save_path = "\u1234"
        self.assertEqual(atp.save_path, "\u1234")
        self.assertEqual(lt.write_resume_data(atp)[b"save_path"], os.fsencode("\u1234"))

    def test_save_path_non_ascii_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.save_path = os.fsencode("\u1234")  # type: ignore
        self.assertEqual(atp.save_path, "\u1234")
        self.assertEqual(lt.write_resume_data(atp)[b"save_path"], os.fsencode("\u1234"))

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5984")
    @lib.uses_surrogate_paths()
    def test_save_path_surrogate_str(self) -> None:
        atp = lt.add_torrent_params()
        atp.save_path = "\udcff"
        self.assertEqual(atp.save_path, "\udcff")
        self.assertEqual(lt.write_resume_data(atp)[b"save_path"], os.fsencode("\udcff"))

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5984")
    @lib.uses_surrogate_paths()
    def test_save_path_surrogate_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.save_path = os.fsencode("\udcff")  # type: ignore
        self.assertEqual(atp.save_path, "\udcff")
        self.assertEqual(lt.write_resume_data(atp)[b"save_path"], os.fsencode("\udcff"))

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5984")
    @lib.uses_non_unicode_paths()
    def test_save_path_non_unicode_str(self) -> None:
        atp = lt.add_torrent_params()
        atp.save_path = os.fsdecode(b"\xff")
        self.assertEqual(atp.save_path, os.fsdecode(b"\xff"))
        self.assertEqual(lt.write_resume_data(atp)[b"save_path"], b"\xff")

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5984")
    @lib.uses_non_unicode_paths()
    def test_save_path_non_unicode_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.save_path = b"\xff"  # type: ignore
        self.assertEqual(atp.save_path, os.fsdecode(b"\xff"))
        self.assertEqual(lt.write_resume_data(atp)[b"save_path"], b"\xff")

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_info_hash_deprecated(self) -> None:
        atp = lt.add_torrent_params()
        with self.assertWarns(DeprecationWarning):
            self.assertTrue(atp.info_hash.is_all_zeros())
        with self.assertWarns(DeprecationWarning):
            atp.info_hash = lt.sha1_hash(lib.get_random_bytes(20))

    def test_info_hash(self) -> None:
        atp = lt.add_torrent_params()
        if lt.api_version < 4:
            self.assertTrue(atp.info_hash.is_all_zeros())
        self.assertTrue(atp.info_hashes.v1.is_all_zeros())
        self.assertTrue(atp.info_hashes.v2.is_all_zeros())

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_http_seeds_deprecated(self) -> None:
        atp = lt.add_torrent_params()
        with self.assertWarns(DeprecationWarning):
            self.assertEqual(atp.http_seeds, [])
        with self.assertWarns(DeprecationWarning):
            atp.http_seeds = ["http://example.com/seed"]

    def test_http_seeds(self) -> None:
        atp = lt.add_torrent_params()
        atp.http_seeds = ["http://example.com/seed"]
        self.assertEqual(atp.http_seeds, ["http://example.com/seed"])

    def test_unfinished_pieces(self) -> None:
        atp = lt.add_torrent_params()
        atp.unfinished_pieces = {}
        atp.unfinished_pieces = {1: [True, False]}

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_merkle_tree_deprecated(self) -> None:
        atp = lt.add_torrent_params()
        with self.assertWarns(DeprecationWarning):
            self.assertEqual(atp.merkle_tree, [])
        with self.assertWarns(DeprecationWarning):
            atp.merkle_tree = [lt.sha1_hash()]

    def test_merkle_tree(self) -> None:
        atp = lt.add_torrent_params()
        atp.merkle_tree = [lt.sha1_hash()]
        self.assertEqual(atp.merkle_tree, [lt.sha1_hash()])

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_url_deprecated(self) -> None:
        atp = lt.add_torrent_params()
        with self.assertWarns(DeprecationWarning):
            self.assertEqual(atp.url, "")
        with self.assertWarns(DeprecationWarning):
            atp.url = "http://example.com/torrent"

    def test_url(self) -> None:
        atp = lt.add_torrent_params()
        atp.url = "http://example.com/torrent"
        self.assertEqual(atp.url, "http://example.com/torrent")

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_resume_data_deprecated(self) -> None:
        atp = lt.add_torrent_params()
        with self.assertWarns(DeprecationWarning):
            self.assertEqual(atp.resume_data, [])
        with self.assertWarns(DeprecationWarning):
            atp.resume_data = ["a"]

    def test_resume_data(self) -> None:
        atp = lt.add_torrent_params()
        atp.resume_data = ["a"]
        self.assertEqual(atp.resume_data, ["a"])

    def test_renamed_files_ascii_str(self) -> None:
        atp = lt.add_torrent_params()
        atp.renamed_files = {0: "test.txt"}
        self.assertEqual(atp.renamed_files, {0: "test.txt"})

    def test_renamed_files_ascii_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.renamed_files = {0: b"test.txt"}  # type: ignore
        self.assertEqual(atp.renamed_files, {0: "test.txt"})

    def test_renamed_files_non_ascii_str(self) -> None:
        atp = lt.add_torrent_params()
        atp.renamed_files = {0: "\u1234.txt"}
        self.assertEqual(atp.renamed_files, {0: "\u1234.txt"})

    def test_renamed_files_non_ascii_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.renamed_files = {0: os.fsencode("\u1234.txt")}  # type: ignore
        self.assertEqual(atp.renamed_files, {0: "\u1234.txt"})

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5984")
    @lib.uses_surrogate_paths()
    def test_renamed_files_surrogate_str(self) -> None:
        atp = lt.add_torrent_params()
        atp.renamed_files = {0: "\udcff.txt"}
        self.assertEqual(atp.renamed_files, {0: "\udcff.txt"})

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5984")
    @lib.uses_surrogate_paths()
    def test_renamed_files_surrogate_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.renamed_files = {0: os.fsencode("\udcff.txt")}  # type: ignore
        self.assertEqual(atp.renamed_files, {0: "\udcff.txt"})

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5984")
    @lib.uses_non_unicode_paths()
    def test_renamed_files_non_unicode_str(self) -> None:
        atp = lt.add_torrent_params()
        atp.renamed_files = {0: os.fsdecode(b"\xff.txt")}
        self.assertEqual(atp.renamed_files, {0: os.fsdecode("\xff.txt")})

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5984")
    @lib.uses_non_unicode_paths()
    def test_renamed_files_non_unicode_bytes(self) -> None:
        atp = lt.add_torrent_params()
        atp.renamed_files = {0: b"\xff.txt"}  # type: ignore
        self.assertEqual(atp.renamed_files, {0: os.fsdecode("\xff.txt")})

    def test_renamed_files_sparse_bencode(self) -> None:
        # only a non-zero file index is renamed, leaving a gap in the
        # "mapped_files" list written by write_resume_data(). That gap must
        # not turn into a python None (which can't be re-bencoded without a
        # spurious DeprecationWarning), see
        # https://github.com/arvidn/libtorrent/issues/8593
        atp = lt.add_torrent_params()
        atp.renamed_files = {1: "test.txt"}
        resume_data = lt.write_resume_data(atp)
        self.assertEqual(resume_data[b"mapped_files"], [b"", b"test.txt"])
        with warnings.catch_warnings():
            warnings.simplefilter("error", DeprecationWarning)
            lt.bencode(resume_data)


class EnumsTest(unittest.TestCase):
    def test_storage_mode_t(self) -> None:
        self.assertIsInstance(lt.storage_mode_t.storage_mode_allocate, int)
        self.assertIsInstance(lt.storage_mode_t.storage_mode_sparse, int)

    def test_options_t(self) -> None:
        self.assertIsInstance(lt.options_t.delete_files, int)

    def test_session_flags_t(self) -> None:
        self.assertIsInstance(lt.session_flags_t.paused, int)
        if lt.api_version < 3:
            self.assertIsInstance(lt.session_flags_t.add_default_plugins, int)
        if lt.api_version < 2:
            self.assertIsInstance(lt.session_flags_t.start_default_features, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_session_flags_t_deprecated(self) -> None:
        if lt.api_version < 3:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.session_flags_t.add_default_plugins, int)
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.session_flags_t.start_default_features, int)

    def test_torrent_flags(self) -> None:
        self.assertIsInstance(lt.torrent_flags.seed_mode, int)
        self.assertIsInstance(lt.torrent_flags.upload_mode, int)
        self.assertIsInstance(lt.torrent_flags.share_mode, int)
        self.assertIsInstance(lt.torrent_flags.apply_ip_filter, int)
        self.assertIsInstance(lt.torrent_flags.paused, int)
        self.assertIsInstance(lt.torrent_flags.auto_managed, int)
        self.assertIsInstance(lt.torrent_flags.duplicate_is_error, int)
        self.assertIsInstance(lt.torrent_flags.update_subscribe, int)
        self.assertIsInstance(lt.torrent_flags.super_seeding, int)
        self.assertIsInstance(lt.torrent_flags.sequential_download, int)
        self.assertIsInstance(lt.torrent_flags.stop_when_ready, int)
        if lt.api_version < 4:
            self.assertIsInstance(lt.torrent_flags.override_trackers, int)
            self.assertIsInstance(lt.torrent_flags.override_web_seeds, int)
        self.assertIsInstance(lt.torrent_flags.disable_dht, int)
        self.assertIsInstance(lt.torrent_flags.disable_lsd, int)
        self.assertIsInstance(lt.torrent_flags.disable_pex, int)
        self.assertIsInstance(lt.torrent_flags.no_verify_files, int)
        self.assertIsInstance(lt.torrent_flags.default_flags, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_atp_flags_t_deprecated(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.add_torrent_params_flags_t.default_flags, int)

    def test_atp_flags_t(self) -> None:
        if lt.api_version < 2:
            self.assertIsInstance(lt.add_torrent_params_flags_t.flag_seed_mode, int)
            self.assertIsInstance(lt.add_torrent_params_flags_t.flag_upload_mode, int)
            self.assertIsInstance(lt.add_torrent_params_flags_t.flag_share_mode, int)
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_apply_ip_filter, int
            )
            self.assertIsInstance(lt.add_torrent_params_flags_t.flag_paused, int)
            self.assertIsInstance(lt.add_torrent_params_flags_t.flag_auto_managed, int)
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_duplicate_is_error, int
            )
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_update_subscribe, int
            )
            self.assertIsInstance(lt.add_torrent_params_flags_t.flag_super_seeding, int)
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_sequential_download, int
            )
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_stop_when_ready, int
            )
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_override_trackers, int
            )
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_override_web_seeds, int
            )
            self.assertIsInstance(lt.add_torrent_params_flags_t.flag_pinned, int)
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_override_resume_data, int
            )
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_merge_resume_trackers, int
            )
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_use_resume_save_path, int
            )
            self.assertIsInstance(
                lt.add_torrent_params_flags_t.flag_merge_resume_http_seeds, int
            )
            self.assertIsInstance(lt.add_torrent_params_flags_t.default_flags, int)

    def test_portmap_protocol(self) -> None:
        self.assertIsInstance(lt.portmap_protocol.none, int)
        self.assertIsInstance(lt.portmap_protocol.udp, int)
        self.assertIsInstance(lt.portmap_protocol.tcp, int)

    def test_portmap_transport(self) -> None:
        self.assertIsInstance(lt.portmap_transport.natpmp, int)
        self.assertIsInstance(lt.portmap_transport.upnp, int)

    def test_peer_class_type_filter_socket_type_t(self) -> None:
        pctfst = lt.peer_class_type_filter_socket_type_t
        self.assertIsInstance(pctfst.tcp_socket, int)
        self.assertIsInstance(pctfst.utp_socket, int)
        self.assertIsInstance(pctfst.ssl_tcp_socket, int)
        self.assertIsInstance(pctfst.ssl_utp_socket, int)
        self.assertIsInstance(pctfst.i2p_socket, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_protocol_type_deprecated(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.protocol_type.udp, int)

    def test_protocol_type(self) -> None:
        if lt.api_version < 2:
            self.assertIsInstance(lt.protocol_type.udp, int)
            self.assertIsInstance(lt.protocol_type.tcp, int)

    def test_save_state_flags_t(self) -> None:
        self.assertIsInstance(lt.save_state_flags_t.all, int)
        self.assertIsInstance(lt.save_state_flags_t.save_settings, int)
        self.assertIsInstance(lt.save_state_flags_t.save_dht_state, int)
        if lt.api_version < 3:
            self.assertIsInstance(lt.save_state_flags_t.save_dht_settings, int)
        if lt.api_version < 2:
            self.assertIsInstance(lt.save_state_flags_t.save_encryption_settings, int)
            self.assertIsInstance(lt.save_state_flags_t.save_as_map, int)
            self.assertIsInstance(lt.save_state_flags_t.save_i2p_proxy, int)
            self.assertIsInstance(lt.save_state_flags_t.save_proxy, int)
            self.assertIsInstance(lt.save_state_flags_t.save_dht_proxy, int)
            self.assertIsInstance(lt.save_state_flags_t.save_peer_proxy, int)
            self.assertIsInstance(lt.save_state_flags_t.save_web_proxy, int)
            self.assertIsInstance(lt.save_state_flags_t.save_tracker_proxy, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_save_state_flags_t_deprecated(self) -> None:
        if lt.api_version < 3:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.save_state_flags_t.save_dht_settings, int)
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    lt.save_state_flags_t.save_encryption_settings, int
                )
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.save_state_flags_t.save_as_map, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.save_state_flags_t.save_i2p_proxy, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.save_state_flags_t.save_proxy, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.save_state_flags_t.save_dht_proxy, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.save_state_flags_t.save_peer_proxy, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.save_state_flags_t.save_web_proxy, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.save_state_flags_t.save_tracker_proxy, int)

    def test_listen_on_flags_t(self) -> None:
        if lt.api_version < 2:
            self.assertIsInstance(lt.listen_on_flags_t.listen_reuse_address, int)
            self.assertIsInstance(lt.listen_on_flags_t.listen_no_system_port, int)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_listen_on_flags_t_deprecated(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.listen_on_flags_t.listen_reuse_address, int)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(lt.listen_on_flags_t.listen_no_system_port, int)

    def test_metric_type_t(self) -> None:
        self.assertIsInstance(lt.metric_type_t.counter, int)
        self.assertIsInstance(lt.metric_type_t.gauge, int)

    def test_session_static_vars(self) -> None:
        self.assertIsInstance(lt.session.tcp, int)
        self.assertIsInstance(lt.session.udp, int)

        self.assertIsInstance(lt.session.global_peer_class_id, int)
        self.assertIsInstance(lt.session.tcp_peer_class_id, int)
        self.assertIsInstance(lt.session.local_peer_class_id, int)

        self.assertIsInstance(lt.session.reopen_map_ports, int)

        self.assertIsInstance(lt.session.delete_files, int)
        self.assertIsInstance(lt.session.delete_partfile, int)

    def test_announce_flags_t(self) -> None:
        self.assertIsInstance(lt.announce_flags_t.seed, int)
        self.assertIsInstance(lt.announce_flags_t.implied_port, int)
        self.assertIsInstance(lt.announce_flags_t.ssl_torrent, int)


class PeerClassTypeFilterTest(unittest.TestCase):
    def test_filter(self) -> None:
        pctf = lt.peer_class_type_filter()
        tcp_socket = lt.peer_class_type_filter_socket_type_t.tcp_socket
        pctf.add(tcp_socket, 1)
        pctf.remove(tcp_socket, 1)
        pctf.allow(tcp_socket, 1)
        pctf.disallow(tcp_socket, 1)
        self.assertEqual(pctf.apply(tcp_socket, 1), 1)

    def test_enum_values(self) -> None:
        self.assertIsInstance(lt.peer_class_type_filter.tcp_socket, int)
        self.assertIsInstance(lt.peer_class_type_filter.utp_socket, int)
        self.assertIsInstance(lt.peer_class_type_filter.ssl_tcp_socket, int)
        self.assertIsInstance(lt.peer_class_type_filter.ssl_utp_socket, int)
        self.assertIsInstance(lt.peer_class_type_filter.i2p_socket, int)


class ResumeDataTest(unittest.TestCase):
    def do_test_round_trip(self, atp: lt.add_torrent_params) -> None:
        first = lt.write_resume_data(atp)
        second = lt.write_resume_data(
            lt.read_resume_data(lt.write_resume_data_buf(atp))
        )

        self.assertEqual(first, second)

    def test_round_trip(self) -> None:
        atp = lt.add_torrent_params()
        atp.name = "test"
        self.do_test_round_trip(atp)

    def test_limit_decode_depth(self) -> None:
        atp = lt.add_torrent_params()
        buf = lt.write_resume_data_buf(atp)
        with self.assertRaises(RuntimeError):
            lt.read_resume_data(buf, load_torrent_limits({"max_decode_depth": 1}))

    def test_limit_decode_tokens(self) -> None:
        atp = lt.add_torrent_params()
        buf = lt.write_resume_data_buf(atp)
        with self.assertRaises(RuntimeError):
            lt.read_resume_data(buf, load_torrent_limits({"max_decode_tokens": 1}))

    def test_limit_pieces(self) -> None:
        atp = lt.add_torrent_params()
        atp.ti = lt.torrent_info(
            TorrentFileDict(
                {
                    b"info": {
                        b"name": b"test.txt",
                        b"length": 1234000,
                        b"piece length": 16384,
                        b"pieces": b"aaaaaaaaaaaaaaaaaaaa" * (1234000 // 16384 + 1),
                    }
                }
            )
        )
        buf = lt.write_resume_data_buf(atp)
        with self.assertRaises(RuntimeError):
            lt.read_resume_data(buf, load_torrent_limits({"max_pieces": 1}))


class ConstructorTest(unittest.TestCase):
    # A bunch of these technically break isolation, but they need to be tested.
    # We compensate by immediately setting isolated settings

    def test_args(self) -> None:
        # no-args

        session = lt.session()
        session.apply_settings(lib.get_isolated_settings())

        # positional args

        lt.session(lib.get_isolated_settings())
        lt.session(lib.get_isolated_settings(), 0)

        # kwargs

        lt.session(settings=lib.get_isolated_settings())

        if lt.api_version < 2:
            session = lt.session(flags=0)
        else:
            session = lt.session()
        session.apply_settings(lib.get_isolated_settings())

        lt.session(settings=lib.get_isolated_settings(), flags=0)

    def test_invalid_settings(self) -> None:
        with self.assertRaises(TypeError):
            lt.session({"alert_mask": "not-an-int"})  # type: ignore[call-overload]
        with self.assertRaises(KeyError):
            lt.session({"not-a-setting": 123})  # type: ignore[call-overload]

    def test_predefined_settings_packs(self) -> None:
        session = lt.session(lt.default_settings())
        session.apply_settings(lib.get_isolated_settings())

        session = lt.session(lt.high_performance_seed())
        session.apply_settings(lib.get_isolated_settings())

        session = lt.session(lt.min_memory_usage())
        session.apply_settings(lib.get_isolated_settings())

    def test_fingerprint(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                fingerprint = lt.fingerprint("AB", 1, 2, 3, 4)

            session = lt.session(fingerprint)
            session.apply_settings(lib.get_isolated_settings())

            session = lt.session(fingerprint, 0)
            session.apply_settings(lib.get_isolated_settings())

            session = lt.session(fingerprint, 0, 0)
            session.apply_settings(lib.get_isolated_settings())

            session = lt.session(fingerprint=fingerprint)
            session.apply_settings(lib.get_isolated_settings())

            session = lt.session(fingerprint=fingerprint, flags=0)
            session.apply_settings(lib.get_isolated_settings())

            session = lt.session(fingerprint=fingerprint, flags=0, alert_mask=0)
            session.apply_settings(lib.get_isolated_settings())

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_fingerprint_deprecated(self) -> None:
        if lt.api_version < 2:
            fingerprint = lt.fingerprint("AB", 1, 2, 3, 4)
            with self.assertWarns(DeprecationWarning):
                session = lt.session(fingerprint)
                session.apply_settings(lib.get_isolated_settings())
            with self.assertWarns(DeprecationWarning):
                session = lt.session(fingerprint, 0)
                session.apply_settings(lib.get_isolated_settings())
            with self.assertWarns(DeprecationWarning):
                session = lt.session(fingerprint, 0, 0)
                session.apply_settings(lib.get_isolated_settings())
            with self.assertWarns(DeprecationWarning):
                session = lt.session(fingerprint=fingerprint)
                session.apply_settings(lib.get_isolated_settings())
            with self.assertWarns(DeprecationWarning):
                session = lt.session(fingerprint=fingerprint, flags=0)
                session.apply_settings(lib.get_isolated_settings())
            with self.assertWarns(DeprecationWarning):
                session = lt.session(fingerprint=fingerprint, flags=0, alert_mask=0)
                session.apply_settings(lib.get_isolated_settings())


class DhtTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_functions(self) -> None:
        self.assertFalse(self.session.is_dht_running())

        # Should be "pretty safe" for isolation purposes
        endpoint = ("127.1.2.3", 65535)

        self.session.add_dht_node(endpoint)

        sha1 = lt.sha1_hash(b"a" * 20)

        self.session.dht_get_immutable_item(sha1)
        self.session.dht_get_mutable_item(b"a" * 32, b"salt")
        self.assertIsInstance(
            self.session.dht_put_immutable_item(b"test"), lt.sha1_hash
        )
        self.assertIsInstance(self.session.dht_put_immutable_item(12345), lt.sha1_hash)
        self.assertIsInstance(
            self.session.dht_put_immutable_item(cast("_Entry", {b"a": 1})), lt.sha1_hash
        )
        self.assertIsInstance(
            self.session.dht_put_immutable_item([1, 2, 3]), lt.sha1_hash
        )
        self.session.dht_put_mutable_item(b"a" * 64, b"b" * 32, b"data", b"salt")
        self.session.dht_get_peers(sha1)
        self.session.dht_announce(sha1)
        self.session.dht_announce(sha1, 0, 0)
        self.session.dht_announce(sha1, port=0, flags=lt.announce_flags_t.seed)
        self.session.dht_live_nodes(sha1)
        self.session.dht_sample_infohashes(endpoint, sha1)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.session.add_dht_router(*endpoint)

        if lt.api_version < 3:
            dht_settings = self.session.get_dht_settings()
            self.session.set_dht_settings(dht_settings)

    def test_bad_args(self) -> None:
        with self.assertRaises(ValueError):
            self.session.dht_get_mutable_item(b"short", b"salt")
        with self.assertRaises(ValueError):
            self.session.dht_put_mutable_item(b"short", b"b" * 32, b"data", b"salt")
        with self.assertRaises(ValueError):
            self.session.dht_put_mutable_item(b"a" * 64, b"short", b"data", b"salt")

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_deprecated(self) -> None:
        with self.assertWarns(DeprecationWarning):
            self.session.get_dht_settings()
        with self.assertWarns(DeprecationWarning):
            lt.dht_settings()
        dht_settings = lt.dht_settings()
        with self.assertWarns(DeprecationWarning):
            self.session.set_dht_settings(dht_settings)
        with self.assertWarns(DeprecationWarning):
            self.session.dht_get_mutable_item("a" * 32, "salt")
        with self.assertWarns(DeprecationWarning):
            self.session.dht_put_mutable_item(
                "a" * 64,
                "b" * 32,
                "data",
                "salt",
            )

    def test_dht_lookup(self) -> None:
        if lt.api_version < 2:
            lookup = lt.dht_lookup()
            self.assertIsInstance(lookup.branch_factor, int)
            self.assertIsInstance(lookup.outstanding_requests, int)
            self.assertIsInstance(lookup.response, int)
            self.assertIsInstance(lookup.timeouts, int)
            self.assertIsNone(lookup.type)  # Should be a str, for real lookups


class AlertHandlingTest(unittest.TestCase):
    def setUp(self) -> None:
        settings = lib.get_isolated_settings()
        settings["alert_mask"] = 0
        self.session = lt.session(settings)

    def test_wait_and_pop(self) -> None:
        # wait_for_alert() should report no pending alerts
        self.assertFalse(self.session.wait_for_alert(0))

        # Force an alert to fire
        self.session.post_torrent_updates()
        self.assertTrue(self.session.wait_for_alert(10000))
        alerts = self.session.pop_alerts()
        self.assertEqual(len(alerts), 1)
        self.assertIsInstance(alerts[0], lt.state_update_alert)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_set_alert_notify_deprecated(self) -> None:
        with self.assertWarns(DeprecationWarning):
            self.session.set_alert_notify(lambda: None)

    @unittest.skipIf(sys.platform == "win32", "windows doesn't support pipes")
    def test_set_alert_fd_pipe(self) -> None:
        # Redundant sys.platform checks are to help mypy
        r, w = os.pipe()
        # Should always be non-blocking, or we'll block the event loop
        if sys.platform != "win32":
            os.set_blocking(w, False)

        self.session.set_alert_fd(w)

        # Pipe should initially be empty
        if sys.platform != "win32":
            os.set_blocking(r, False)
        with self.assertRaises(BlockingIOError):
            os.read(r, 1024)
        if sys.platform != "win32":
            os.set_blocking(r, True)

        # Force an alert to fire
        self.session.post_torrent_updates()

        # We should have data shortly
        data = os.read(r, 1024)
        self.assertGreater(len(data), 0)

    def test_set_alert_fd_socket(self) -> None:
        r, w = socket.socketpair()
        # Should always be non-blocking, or we'll block the event loop
        w.setblocking(False)

        self.session.set_alert_fd(w.fileno())

        # Pipe should initially be empty
        r.setblocking(False)
        with self.assertRaises(BlockingIOError):
            r.recv(1024)
        r.setblocking(True)

        # Force an alert to fire
        self.session.post_torrent_updates()

        # Should now have data
        data = r.recv(1024)
        self.assertGreater(len(data), 0)

        # Explicit cleanup to avoid ResourceWarning
        r.close()
        w.close()


class Test5155(unittest.TestCase):
    # We attempt to force a torrent_info to be allocated in python, but
    # released from C++.
    # See https://github.com/arvidn/libtorrent/issues/5155
    def setUp(self) -> None:
        self.dir = tempfile.TemporaryDirectory()
        settings = lib.get_isolated_settings()
        settings["alert_mask"] = 0
        self.session = lt.session(settings)

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

    def test_5155(self) -> None:
        # Ensure the torrent_info only is referenced by function locals
        def do_add() -> lt.torrent_handle:
            torrent = tdummy.Torrent.single_file(
                piece_length=16384, name=b"test.txt", length=16384 * 9 + 1000
            )
            atp = lt.add_torrent_params()
            atp.ti = torrent.torrent_info()
            atp.save_path = "."
            return self.session.add_torrent(atp)

        handle = do_add()
        # The bug depends on this exact sequence of alert management. I don't
        # have a good explanation for why
        self.session.wait_for_alert(10000)  # add_torrent_alert
        self.session.pop_alerts()
        self.session.remove_torrent(handle)
        self.session.wait_for_alert(10000)  # torrent_removed_alert
        self.session.pop_alerts()


class PostAlertsTest(unittest.TestCase):
    def setUp(self) -> None:
        settings = lib.get_isolated_settings()
        settings["alert_mask"] = 0
        self.session = lt.session(settings)

    def _assert_pending_alert(self, alert_type: type) -> None:
        self.assertTrue(self.session.wait_for_alert(10000))
        alerts = self.session.pop_alerts()
        self.assertTrue(any(isinstance(a, alert_type) for a in alerts))

    def test_post_torrent_updates(self) -> None:
        # no args
        self.session.post_torrent_updates()
        self._assert_pending_alert(lt.state_update_alert)

        # positional args
        self.session.post_torrent_updates(0)
        self._assert_pending_alert(lt.state_update_alert)

        # kwargs
        self.session.post_torrent_updates(flags=0)
        self._assert_pending_alert(lt.state_update_alert)

    def test_post_dht_stats(self) -> None:
        self.session.post_dht_stats()
        self._assert_pending_alert(lt.dht_stats_alert)


class AddTorrentTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())
        self.torrent = tdummy.get_default()
        self.dir = tempfile.TemporaryDirectory()

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

    def test_old_style_with_wrong_args(self) -> None:
        with self.assertRaises(TypeError):
            self.session.add_torrent(  # type: ignore
                self.torrent.torrent_info(),
                resume_data=None,
            )

    def test_old_style(self) -> None:
        if lt.api_version < 2:
            ti = self.torrent.torrent_info()
            # positional args
            with self.assertWarns(DeprecationWarning):
                handle = self.session.add_torrent(ti, self.dir.name)
            self.assertIsInstance(handle, lt.torrent_handle)
            self.assertTrue(handle.is_valid())
            self.assertEqual(handle.status().save_path, self.dir.name)

            with self.assertWarns(DeprecationWarning):
                handle = self.session.add_torrent(
                    ti,
                    self.dir.name,
                    None,
                    lt.storage_mode_t.storage_mode_sparse,
                    False,
                )
            self.assertIsInstance(handle, lt.torrent_handle)
            self.assertTrue(handle.is_valid())

            # kwargs
            with self.assertWarns(DeprecationWarning):
                handle = self.session.add_torrent(
                    ti,
                    self.dir.name,
                    resume_data=None,
                    storage_mode=lt.storage_mode_t.storage_mode_sparse,
                    paused=False,
                )
            self.assertIsInstance(handle, lt.torrent_handle)
            self.assertTrue(handle.is_valid())

    def test_atp(self) -> None:
        atp = self.torrent.atp()
        atp.save_path = self.dir.name

        self.session.async_add_torrent(atp)
        handle = self.session.add_torrent(atp)
        self.assertIsInstance(handle, lt.torrent_handle)
        self.assertTrue(handle.is_valid())

    def test_dict_deprecated(self) -> None:
        with self.assertWarns(DeprecationWarning):
            atp = {"save_path": ".", "ti": self.torrent.torrent_info()}
            self.session.add_torrent(atp)  # type: ignore
        with self.assertWarns(DeprecationWarning):
            atp = {"save_path": ".", "ti": self.torrent.torrent_info()}
            self.session.async_add_torrent(atp)  # type: ignore

    def do_test_dict(self, params: AddTorrentParamsdict) -> lt.torrent_handle:
        with self.assertWarns(DeprecationWarning):
            self.session.async_add_torrent(params)
        with self.assertWarns(DeprecationWarning):
            handle = self.session.add_torrent(params)
        self.assertIsInstance(handle, lt.torrent_handle)
        self.assertTrue(handle.is_valid())
        return handle

    def test_dict(self) -> None:
        ti = self.torrent.torrent_info()
        atp = AddTorrentParamsdict(
            {
                "ti": ti,
                "info_hashes": ti.info_hashes().v1.to_bytes(),
                "save_path": self.dir.name,
                "storage_mode": lt.storage_mode_t.storage_mode_allocate,
                "trackers": ["http://127.1.2.1/tr"],
                "url_seeds": ["http://127.1.2.2/us"],
                "http_seeds": ["http://127.1.2.3/hs"],
                "dht_nodes": [("127.1.2.4", 1234)],
                "banned_peers": [("127.1.2.5", 1234)],
                "peers": [("127.1.2.6", 1234)],
                "flags": lt.torrent_flags.sequential_download,
                "trackerid": "trackerid",
                "renamed_files": {0: "renamed.txt"},
                "file_priorities": [2],
            }
        )

        if lt.api_version < 4:
            atp["info_hash"] = ti.info_hashes().v1.to_bytes()

        if lt.api_version < 2:
            atp["url"] = "http://127.1.2.7/u"

        handle = self.do_test_dict(atp)

        status = handle.status()
        self.assertEqual(handle.status().save_path, self.dir.name)
        self.assertEqual(status.storage_mode, lt.storage_mode_t.storage_mode_allocate)
        self.assertEqual(
            [tr["url"] for tr in handle.trackers()], ["http://127.1.2.1/tr"]
        )
        self.assertEqual(handle.url_seeds(), ["http://127.1.2.2/us"])
        # TODO: can we test dht nodes?
        # TODO: can we test banned_nodes?
        # TODO: why is this sometimes 2, sometimes 1?
        self.assertIn(status.list_peers, (1, 2))
        self.assertEqual(handle.flags(), lt.torrent_flags.sequential_download)
        # TODO: can we test trackerid?
        torrent_file = handle.torrent_file()
        assert torrent_file is not None
        # the torrent_info object is immutable now, and renamed files are
        # recorded in a separate object
        # self.assertEqual(torrent_file.files().file_path(0), "renamed.txt")
        self.assertEqual(handle.get_file_priorities(), [2])

    def test_dict_no_torrent_info_old(self) -> None:
        self.do_test_dict(
            AddTorrentParamsdict({"info_hashes": b"a" * 20, "save_path": self.dir.name})
        )

    def test_no_torrent_info_old(self) -> None:
        atp = lt.add_torrent_params()
        atp.info_hashes = lt.info_hash_t(lt.sha1_hash(b"a" * 20))
        atp.save_path = self.dir.name
        self.session.add_torrent(atp)

    def test_dict_name(self) -> None:
        # This can only be tested *without* torrent info
        handle = self.do_test_dict(
            AddTorrentParamsdict(
                {
                    "info_hashes": b"a" * 20,
                    "save_path": self.dir.name,
                    "name": "test-name",
                }
            )
        )

        self.assertEqual(handle.status().name, "test-name")

    def test_dict_no_torrent_info_sha1(self) -> None:
        handle = self.do_test_dict(
            AddTorrentParamsdict({"info_hashes": b"a" * 20, "save_path": self.dir.name})
        )
        self.assertEqual(handle.info_hashes().v1.to_bytes(), b"a" * 20)

    def test_dict_no_torrent_info_sha256(self) -> None:
        handle = self.do_test_dict(
            AddTorrentParamsdict({"info_hashes": b"a" * 32, "save_path": self.dir.name})
        )

        self.assertEqual(handle.info_hashes().v2.to_bytes(), b"a" * 32)

    def test_dict_errors(self) -> None:
        with self.assertWarns(DeprecationWarning):
            with self.assertRaises(KeyError):
                self.session.add_torrent({"invalid-key": None})  # type: ignore
        with self.assertWarns(DeprecationWarning):
            with self.assertRaises(KeyError):
                self.session.async_add_torrent({"invalid-key": None})  # type: ignore

    def test_errors(self) -> None:
        atp = self.torrent.atp()
        atp.save_path = self.dir.name
        atp.flags |= lt.torrent_flags.duplicate_is_error

        self.session.add_torrent(atp)
        with self.assertRaises(RuntimeError):
            self.session.add_torrent(atp)


class StateTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def check_state(self, state: Dict[bytes, Any]) -> None:
        self.assertIsInstance(state[b"settings"], dict)
        self.assertIsInstance(state[b"dht"], dict)
        # we disable dht, so we don't expect b"dht state"

    def test_save(self) -> None:
        if lt.api_version < 3:
            self.check_state(self.session.save_state())
            self.check_state(self.session.save_state(flags=2**32 - 1))

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/5988")
    def test_deprecated(self) -> None:
        with self.assertWarns(DeprecationWarning):
            self.session.save_state()
        state = self.session.save_state()
        with self.assertWarns(DeprecationWarning):
            self.session.load_state(state)

    def test_load(self) -> None:
        state = self.session.save_state()
        self.session.load_state(state)
        self.session.load_state(state, flags=2**32 - 1)


class GetTorrentsTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())
        self.dir = tempfile.TemporaryDirectory()
        self.torrent = tdummy.get_default()
        atp = self.torrent.atp()
        atp.save_path = self.dir.name
        self.handle = self.session.add_torrent(atp)

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

    def test_get_torrents(self) -> None:
        torrents = self.session.get_torrents()
        self.assertEqual(torrents, [self.handle])

    def test_find_torrent_sha1(self) -> None:
        handle = self.session.find_torrent(self.torrent.sha1_hash)
        self.assertEqual(handle, self.handle)


class PauseTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_pause(self) -> None:
        self.assertFalse(self.session.is_paused())
        self.session.pause()
        self.assertTrue(self.session.is_paused())
        self.session.resume()
        self.assertFalse(self.session.is_paused())


class ComponentsTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_start_stop(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.session.start_upnp()
            with self.assertWarns(DeprecationWarning):
                self.session.stop_upnp()
            with self.assertWarns(DeprecationWarning):
                self.session.start_lsd()
            with self.assertWarns(DeprecationWarning):
                self.session.stop_lsd()
            with self.assertWarns(DeprecationWarning):
                self.session.start_natpmp()
            with self.assertWarns(DeprecationWarning):
                self.session.stop_natpmp()
            with self.assertWarns(DeprecationWarning):
                self.session.start_dht()
            with self.assertWarns(DeprecationWarning):
                self.session.stop_dht()


class PortsTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_ports(self) -> None:
        # NB: this typically is a no-op with our isolated settings, so we
        # may not test the case where it actually works
        tags = self.session.add_port_mapping(lt.portmap_protocol.tcp, 65535, 65535)
        for tag in tags:
            self.session.delete_port_mapping(tag)
        self.session.delete_port_mapping(12345)
        self.session.reopen_network_sockets(0)
        self.assertTrue(self.session.is_listening())
        self.assertIsInstance(self.session.listen_port(), int)

        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.session.outgoing_ports(1024, 65535)

            # not sure how to correctly test str arg, because it raises
            # RuntimeError on an invalid interface

            # positional args
            with self.assertWarns(DeprecationWarning):
                self.session.listen_on(1024, 65535)
            with self.assertWarns(DeprecationWarning):
                self.session.listen_on(1024, 65535, None)
            with self.assertWarns(DeprecationWarning):
                self.session.listen_on(
                    1024, 65535, None, lt.listen_on_flags_t.listen_no_system_port
                )

            # kwargs
            with self.assertWarns(DeprecationWarning):
                self.session.listen_on(1024, 65535)
            with self.assertWarns(DeprecationWarning):
                self.session.listen_on(1024, 65535, interface=None)
            with self.assertWarns(DeprecationWarning):
                self.session.listen_on(
                    1024, 65535, flags=lt.listen_on_flags_t.listen_no_system_port
                )

            with self.assertWarns(DeprecationWarning):
                with self.assertRaises(RuntimeError):
                    self.session.listen_on(1024, 65535, "interface-does-not-exist")


class SettingsTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def do_test_settings_pack(self, settings: settings_pack) -> None:
        self.session.apply_settings({**settings, **lib.get_isolated_settings()})

    def test_settings_packs(self) -> None:
        self.do_test_settings_pack(lt.default_settings())
        self.do_test_settings_pack(lt.high_performance_seed())
        self.do_test_settings_pack(lt.min_memory_usage())

        self.assertIsInstance(self.session.get_settings(), dict)

    def test_old_settings(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(self.session.local_download_rate_limit(), int)
            with self.assertWarns(DeprecationWarning):
                self.session.set_local_download_rate_limit(0)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(self.session.local_upload_rate_limit(), int)
            with self.assertWarns(DeprecationWarning):
                self.session.set_local_upload_rate_limit(0)

            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(self.session.download_rate_limit(), int)
            with self.assertWarns(DeprecationWarning):
                self.session.set_download_rate_limit(0)
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(self.session.upload_rate_limit(), int)
            with self.assertWarns(DeprecationWarning):
                self.session.set_upload_rate_limit(0)

            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(self.session.max_connections(), int)
            with self.assertWarns(DeprecationWarning):
                self.session.set_max_connections(0)

            with self.assertWarns(DeprecationWarning):
                self.session.set_max_uploads(0)
            with self.assertWarns(DeprecationWarning):
                self.session.set_max_half_open_connections(0)
            with self.assertWarns(DeprecationWarning):
                self.session.set_alert_queue_size_limit(0)
            with self.assertWarns(DeprecationWarning):
                self.session.set_alert_mask(0)

            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    self.session.proxy(), lt.proxy_type_t.proxy_settings
                )
            with self.assertWarns(DeprecationWarning):
                self.session.set_proxy(lt.proxy_type_t.proxy_settings())
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    self.session.dht_proxy(), lt.proxy_type_t.proxy_settings
                )
            with self.assertWarns(DeprecationWarning):
                self.session.set_dht_proxy(lt.proxy_type_t.proxy_settings())
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    self.session.peer_proxy(), lt.proxy_type_t.proxy_settings
                )
            with self.assertWarns(DeprecationWarning):
                self.session.set_peer_proxy(lt.proxy_type_t.proxy_settings())
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    self.session.tracker_proxy(), lt.proxy_type_t.proxy_settings
                )
            with self.assertWarns(DeprecationWarning):
                self.session.set_tracker_proxy(lt.proxy_type_t.proxy_settings())
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    self.session.web_seed_proxy(), lt.proxy_type_t.proxy_settings
                )
            with self.assertWarns(DeprecationWarning):
                self.session.set_web_seed_proxy(lt.proxy_type_t.proxy_settings())
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(
                    self.session.i2p_proxy(), lt.proxy_type_t.proxy_settings
                )
            with self.assertWarns(DeprecationWarning):
                self.session.set_i2p_proxy(lt.proxy_type_t.proxy_settings())


class PeSettingsTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_pe_settings(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(self.session.get_pe_settings(), lt.pe_settings)
            with self.assertWarns(DeprecationWarning):
                self.session.set_pe_settings(lt.pe_settings())


class IpFilterTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_ip_filter(self) -> None:
        self.assertIsInstance(self.session.get_ip_filter(), lt.ip_filter)
        self.session.set_ip_filter(lt.ip_filter())


def unlink_all_files(path: str) -> None:
    for dirpath, _, filenames in os.walk(path):
        for filename in filenames:
            os.unlink(os.path.join(dirpath, filename))


class RemoveTorrentTest(unittest.TestCase):
    def setUp(self) -> None:
        settings = lib.get_isolated_settings()
        settings["alert_mask"] = 0
        self.session = lt.session(settings)
        self.dir = tempfile.TemporaryDirectory()
        self.torrent = tdummy.get_default()
        self.file_path = pathlib.Path(self.dir.name) / os.fsdecode(
            self.torrent.files[0].path
        )
        atp = self.torrent.atp()
        atp.save_path = self.dir.name
        self.handle = self.session.add_torrent(atp)

        # add_piece() does not work in the checking_* states
        for _ in lib.loop_until_timeout(5, msg="checking"):
            if self.handle.status().state not in (
                lt.torrent_status.checking_files,
                lt.torrent_status.checking_resume_data,
            ):
                break

        # add synthetic data
        for i, data in enumerate(self.torrent.pieces):
            self.handle.add_piece(i, data, 0)

        # wait until data is written to disk
        for _ in lib.loop_until_timeout(5, msg="file write"):
            if not self.file_path.is_file():
                continue
            if self.file_path.read_bytes() == self.torrent.data:
                break

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

    def test_remove(self) -> None:
        self.session.remove_torrent(self.handle)
        # There's no good way to synchronize so that we test the data doesn't
        # *eventually* get deleted, but this at least makes the test more
        # readable
        self.assertEqual(self.file_path.read_bytes(), self.torrent.data)

    def test_remove_data(self) -> None:
        self.session.remove_torrent(self.handle, option=self.session.delete_files)
        for _ in lib.loop_until_timeout(5, msg="file delete"):
            if not self.file_path.is_file():
                break


class TorrentStatusTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())
        self.dir = tempfile.TemporaryDirectory()
        self.torrent = tdummy.get_default()
        atp = self.torrent.atp()
        atp.save_path = self.dir.name
        self.handle = self.session.add_torrent(atp)

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6008")
    def test_get_torrents_status(self) -> None:
        # NB: the predicate function may suffer from being being "owned" by
        # libtorrent and inappropriately freed. Test against this by _not_
        # keeping a reference to the functions here

        status_list = self.session.get_torrent_status(lambda _: True)
        self.assertEqual(len(status_list), 1)
        status = status_list[0]
        # flags should default to 0, so no pieces
        self.assertEqual(status.pieces, [])

        status_list = self.session.get_torrent_status(
            lambda _: True, flags=lt.status_flags_t.query_pieces
        )
        status = status_list[0]
        self.assertGreater(len(status.pieces), 0)

        status_list = self.session.get_torrent_status(lambda _: False)
        self.assertEqual(len(status_list), 0)

        with self.assertRaises(TypeError):
            self.session.get_torrent_status(None)  # type: ignore
        with self.assertRaises(TypeError):
            self.session.get_torrent_status(lambda: True)  # type: ignore

    @unittest.skip("https://github.com/arvidn/libtorrent/issues/6008")
    def test_refresh_torrent_status(self) -> None:
        status_list = self.session.get_torrent_status(lambda _: True)
        updated_list = self.session.refresh_torrent_status(status_list)
        self.assertEqual(len(updated_list), 1)


class FieldsTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_peer_id(self) -> None:
        sha1 = lt.sha1_hash(b"a" * 20)
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.session.set_peer_id(sha1)
            with self.assertWarns(DeprecationWarning):
                self.assertEqual(self.session.id(), sha1)

    def test_num_connections(self) -> None:
        if lt.api_version < 2:
            with self.assertWarns(DeprecationWarning):
                self.assertIsInstance(self.session.num_connections(), int)


class PeerClassTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_peer_class_filter(self) -> None:
        self.session.set_peer_class_filter(lt.ip_filter())

    def test_peer_class_type_filter(self) -> None:
        self.session.set_peer_class_type_filter(lt.peer_class_type_filter())

    def test_create_delete_peer_class(self) -> None:
        pc = self.session.create_peer_class("test")
        self.session.delete_peer_class(pc)

    def test_get_set_peer_class(self) -> None:
        pci = self.session.get_peer_class(0)
        self.assertIsInstance(pci["ignore_unchoke_slots"], bool)
        self.assertIsInstance(pci["connection_limit_factor"], int)
        self.assertIsInstance(pci["label"], str)
        self.assertIsInstance(pci["upload_limit"], int)
        self.assertIsInstance(pci["download_limit"], int)
        self.assertIsInstance(pci["upload_priority"], int)
        self.assertIsInstance(pci["download_priority"], int)

        self.session.set_peer_class(0, pci)


class ExtensionTest(unittest.TestCase):
    def setUp(self) -> None:
        self.session = lt.session(lib.get_isolated_settings())

    def test_extensions(self) -> None:
        self.session.add_extension(lt.create_smart_ban_plugin)
        self.session.add_extension(lt.create_ut_metadata_plugin)
        self.session.add_extension(lt.create_ut_pex_plugin)

        # Should not raise an error
        self.session.add_extension("does-not-exist")  # type: ignore

        # TODO: can we test whether extensions are loaded?


class SessionStatsMetricsTest(unittest.TestCase):
    def test_session_stats_metrics(self) -> None:
        metrics = lt.session_stats_metrics()
        self.assertGreater(len(metrics), 0)
        metric = metrics[0]
        self.assertIsInstance(metric.name, str)
        self.assertIsInstance(metric.type, lt.metric_type_t)
        self.assertIsInstance(metric.value_index, int)

        self.assertEqual(lt.find_metric_idx(metric.name), 0)

        self.assertLess(lt.find_metric_idx("does-not-exist"), 0)


class SessionStateTest(unittest.TestCase):
    def setUp(self) -> None:
        self.settings = lib.get_isolated_settings()
        self.session = lt.session(self.settings)

    def test_session_state(self) -> None:
        # By default, everything should be included
        params = self.session.session_state()
        self.assertLessEqual(set(self.settings.items()), set(params.settings.items()))

        # With flags=0, nothing should be included
        params = self.session.session_state(flags=0)
        self.assertEqual(params.settings, lt.session_params().settings)
