import logging
import os
import shutil
from tempfile import mkdtemp

from libtorrent import bdecode
from nose.tools import raises
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks
from twisted.web.server import Site
from twisted.web.static import File

from Tribler.Core.TorrentDef import TorrentDef, TorrentDefNoMetainfo
from Tribler.Core.Utilities.network_utils import get_random_port
from Tribler.Core.Utilities.utilities import create_valid_metainfo, valid_torrent_file
from Tribler.Core.exceptions import TorrentDefNotFinalizedException, HttpError
from Tribler.Core.simpledefs import INFOHASH_LENGTH
from Tribler.Test.common import TESTS_DATA_DIR, TORRENT_UBUNTU_FILE
from Tribler.Test.test_as_server import BaseTestCase
from Tribler.Test.twisted_thread import deferred
from Tribler.pyipv8.ipv8.util import blocking_call_on_reactor_thread

TRACKER = 'http://www.tribler.org/announce'


class TestTorrentDef(BaseTestCase):

    VIDEO_FILE_NAME = "video.avi"

    """
    Testing TorrentDef version 0
    """
    def __init__(self, *argv, **kwargs):
        super(TestTorrentDef, self).__init__(*argv, **kwargs)
        self._logger = logging.getLogger(self.__class__.__name__)
        self.file_server = None

    @blocking_call_on_reactor_thread
    def setUpFileServer(self, port, path):
        # Create a local file server, can be used to serve local files. This is preferred over an external network
        # request in order to get files.
        resource = File(path)
        factory = Site(resource)
        self.file_server = reactor.listenTCP(port, factory)

    @blocking_call_on_reactor_thread
    @inlineCallbacks
    def tearDown(self):
        super(TestTorrentDef, self).tearDown()
        if self.file_server:
            yield self.file_server.stopListening()

    def test_add_content_file_and_copy(self):
        """ Add a single file to a TorrentDef """
        t = TorrentDef()
        fn = os.path.join(TESTS_DATA_DIR, self.VIDEO_FILE_NAME)
        t.add_content(fn)
        t.set_tracker(TRACKER)
        t.finalize()

        s = os.path.getsize(fn)

        metainfo = t.get_metainfo()
        self.general_check(metainfo)

        self.assertEqual(metainfo['info']['name'], self.VIDEO_FILE_NAME)
        self.assertEqual(metainfo['info']['length'], s)
        self.assertTrue(t.get_pieces())
        self.assertEqual(len(t.get_infohash()), INFOHASH_LENGTH)
        self.assertTrue(t.get_name())

        # test copy constructor
        nt = TorrentDef(t.input, t.metainfo, t.infohash)
        self.assertEqual(nt.input, t.input)
        self.assertEqual(nt.metainfo, t.metainfo)
        self.assertEqual(nt.infohash, t.infohash)

        # test removing content
        nt.remove_content("/test123")
        self.assertEqual(len(nt.input['files']), 1)
        nt.remove_content(unicode(fn))
        self.assertEqual(len(nt.input['files']), 0)
        nt.remove_content(unicode(fn))

    def test_add_content_dir(self):
        """ Add a single dir to a TorrentDef """
        t = TorrentDef()
        dn = os.path.join(TESTS_DATA_DIR, "contentdir")
        t.add_content(dn, "dirintorrent")
        t.set_tracker(TRACKER)
        t.finalize()

        exps = 0
        for f in os.listdir(dn):
            if f.startswith('.'):
                continue
            p = os.path.join(dn, f)
            s = os.path.getsize(p)
            exps += s
            self._logger.debug("Expected size %s %d", f, s)

        self._logger.debug("Expected total size of files in torrent %d", exps)

        metainfo = t.get_metainfo()
        self.general_check(metainfo)

        self.assertEqual(metainfo['info']['name'], 'dirintorrent')
        reals = 0
        for file in metainfo['info']['files']:
            s = file['length']
            self._logger.debug("real size %s %d", file['path'], s)
            reals += s

        self._logger.debug("Real size of files in torrent %d", reals)

        self.assertEqual(exps, reals)

    def test_add_content_dir_and_file(self):
        """ Add a single dir and single file to a TorrentDef """
        t = TorrentDef()

        dn = os.path.join(TESTS_DATA_DIR, "contentdir")
        t.add_content(dn, "dirintorrent")

        fn = os.path.join(TESTS_DATA_DIR, self.VIDEO_FILE_NAME)
        t.add_content(fn, os.path.join("dirintorrent", self.VIDEO_FILE_NAME))

        t.set_tracker(TRACKER)
        t.finalize()

        # Check
        exps = os.path.getsize(fn)
        for f in os.listdir(dn):
            if f.startswith('.'):
                continue
            p = os.path.join(dn, f)
            exps += os.path.getsize(p)

        metainfo = t.get_metainfo()
        self.general_check(metainfo)
        self.assertEqual(metainfo['info']['name'], 'dirintorrent')

        reals = 0
        for file in metainfo['info']['files']:
            reals += file['length']
        self.assertEqual(exps, reals)

    def test_get_name_utf8(self):
        """ Add a TorrentDef with non-utf8 encoding"""
        t = TorrentDef()
        t.set_name('\xA1\xC0')
        t.set_encoding('euc_kr')
        t.set_tracker(TRACKER)
        t.finalize()

        self.assertEqual(t.get_name_utf8(), u'\xf7')

    def test_get_name_utf8_unknown(self):
        """ Add a TorrentDef with non-utf8 encoding"""
        t = TorrentDef()
        t.set_name('\xA1\xC0')
        t.set_tracker(TRACKER)
        t.finalize()

        self.assertEqual(t.get_name_utf8(), u'\xa1\xc0')

    def test_add_content_announce_list(self):
        """ Add a single file with announce-list to a TorrentDef """
        t = TorrentDef()
        fn = os.path.join(TESTS_DATA_DIR, self.VIDEO_FILE_NAME)

        t.add_content(fn)
        t.set_tracker(TRACKER)
        exphier = [[TRACKER], ['http://tracker1.tribler.org:6969/announce', 'http://tracker2.tribler.org:7070/ann'],
                   ['http://www.cs.vu.nl', 'http://www.st.ewi.tudelft.nl', 'http://www.vuze.com']]
        t.set_tracker_hierarchy(exphier)
        t.finalize()

        metainfo = t.get_metainfo()
        self.general_check(metainfo)
        realhier = metainfo['announce-list']
        self.assertEqual(realhier, exphier)

    def test_add_content_httpseeds(self):
        """ Add a single file with BitTornado httpseeds to a TorrentDef """
        t = TorrentDef()
        fn = os.path.join(TESTS_DATA_DIR, self.VIDEO_FILE_NAME)
        t.add_content(fn)
        t.set_tracker(TRACKER)
        expseeds = ['http://www.cs.vu.nl/index.html', 'http://www.st.ewi.tudelft.nl/index.html']
        t.set_httpseeds(expseeds)
        t.finalize()

        metainfo = t.get_metainfo()
        self.general_check(metainfo)
        realseeds = metainfo['httpseeds']
        self.assertEqual(realseeds, expseeds)

    def test_add_content_piece_length(self):
        """ Add a single file with piece length to a TorrentDef """
        t = TorrentDef()
        fn = os.path.join(TESTS_DATA_DIR, self.VIDEO_FILE_NAME)
        t.add_content(fn)
        t.set_piece_length(2 ** 16)
        t.set_tracker(TRACKER)
        t.finalize()

        metainfo = t.get_metainfo()
        self.general_check(metainfo)
        self.assertEqual(metainfo['info']['piece length'], 2 ** 16)

    def test_add_content_file_save(self):
        """ Add a single file to a TorrentDef and save the latter"""
        t = TorrentDef()
        fn = os.path.join(TESTS_DATA_DIR, self.VIDEO_FILE_NAME)
        t.add_content(fn)
        t.set_tracker(TRACKER)
        t.finalize()

        tfn = os.path.join(os.getcwd(), "gen.torrent")
        t.save(tfn)

        f = open(tfn, "rb")
        bdata = f.read()
        f.close()
        os.remove(tfn)

        data = bdecode(bdata)
        metainfo = t.get_metainfo()
        self.general_check(metainfo)
        self.assertEqual(metainfo, data)

    def test_is_private(self):
        privatefn = os.path.join(TESTS_DATA_DIR, "private.torrent")
        publicfn = os.path.join(TESTS_DATA_DIR, "bak_single.torrent")

        t1 = TorrentDef.load(privatefn)
        t2 = TorrentDef.load(publicfn)

        self.assertTrue(t1.is_private())
        self.assertFalse(t2.is_private())

    @deferred(timeout=10)
    def test_load_from_url(self):
        # Setup file server to serve torrent file
        self.session_base_dir = mkdtemp(suffix="_tribler_test_load_from_url")
        files_path = os.path.join(self.session_base_dir, 'http_torrent_files')
        os.mkdir(files_path)
        shutil.copyfile(TORRENT_UBUNTU_FILE, os.path.join(files_path, 'ubuntu.torrent'))

        file_server_port = get_random_port()
        self.setUpFileServer(file_server_port, files_path)

        def _on_load(torrent_def):
            torrent_def.metainfo = create_valid_metainfo(torrent_def.get_metainfo())
            self.assertTrue(valid_torrent_file(torrent_def.get_metainfo()))
            self.assertEqual(torrent_def.get_metainfo(), TorrentDef.load(TORRENT_UBUNTU_FILE).get_metainfo())
            self.assertEqual(torrent_def.infohash, TorrentDef.load(TORRENT_UBUNTU_FILE).infohash)

        torrent_url = 'http://localhost:%d/ubuntu.torrent' % file_server_port
        deferred = TorrentDef.load_from_url(torrent_url)
        deferred.addCallback(_on_load)
        return deferred

    @deferred(timeout=10)
    def test_load_from_url_404(self):
        # Setup file server to serve torrent file
        self.session_base_dir = mkdtemp(suffix="_tribler_test_load_from_url")
        files_path = os.path.join(self.session_base_dir, 'http_torrent_files')
        os.mkdir(files_path)
        # Do not copy the torrent file to produce 404

        file_server_port = get_random_port()
        self.setUpFileServer(file_server_port, files_path)

        def _on_error(failure):
            failure.trap(HttpError)
            self.assertEqual(failure.value.response.code, 404)

        torrent_url = 'http://localhost:%d/ubuntu.torrent' % file_server_port
        deferred = TorrentDef.load_from_url(torrent_url)
        deferred.addErrback(_on_error)
        return deferred

    def test_torrent_encoding(self):
        t = TorrentDef()
        t.set_encoding("my_fancy_encoding")
        self.assertEqual(t.get_encoding(), "my_fancy_encoding")

    @raises(ValueError)
    def test_set_tracker_invalid_url(self):
        t = TorrentDef()
        t.set_tracker("http/tracker.org")

    def test_set_tracker_strip_slash(self):
        t = TorrentDef()
        t.set_tracker("http://tracker.org/")
        self.assertEqual(t.input['announce'], "http://tracker.org")

    @raises(ValueError)
    def test_set_trackers_no_list_hierarchy(self):
        t = TorrentDef()
        t.set_tracker_hierarchy("http://tracker.org")

    @raises(ValueError)
    def test_set_trackers_no_list_tier(self):
        t = TorrentDef()
        t.set_tracker_hierarchy(["http://tracker.org"])

    def test_set_trackers(self):
        t = TorrentDef()
        t.set_tracker_hierarchy([["http://tracker.org", "http://tracker2.org/", "http/tracker3.org"]])
        self.assertEqual(len(t.get_tracker_hierarchy()[0]), 2)
        self.assertEqual(t.get_tracker_hierarchy()[0][0], "http://tracker.org")
        self.assertEqual(t.get_tracker_hierarchy()[0][1], "http://tracker2.org")

        self.assertEqual(t.get_trackers_as_single_tuple(), ('http://tracker.org', 'http://tracker2.org'))

    def test_set_tracker(self):
        t = TorrentDef()
        self.assertFalse(t.get_trackers_as_single_tuple())
        t.set_tracker("http://tracker.org")
        self.assertEqual(t.get_trackers_as_single_tuple(), ('http://tracker.org',))

    @raises(ValueError)
    def test_set_dht_nodes_no_list(self):
        t = TorrentDef()
        t.set_dht_nodes(("127.0.0.1", 1234))

    @raises(ValueError)
    def test_set_dht_nodes_node_no_list(self):
        t = TorrentDef()
        t.set_dht_nodes([("127.0.0.1", 1234)])

    @raises(ValueError)
    def test_set_dht_nodes_node_no_string(self):
        t = TorrentDef()
        t.set_dht_nodes([[1234, "127.0.0.1"]])

    @raises(ValueError)
    def test_set_dht_nodes_node_no_int(self):
        t = TorrentDef()
        t.set_dht_nodes([["127.0.0.1", "1234"]])

    def test_set_dht_nodes(self):
        t = TorrentDef()
        t.set_dht_nodes([["127.0.0.1", 1234]])
        self.assertEqual(t.get_dht_nodes(), [["127.0.0.1", 1234]])

    def test_set_comment(self):
        t = TorrentDef()
        t.set_comment("lorem ipsum")
        self.assertEqual(t.get_comment(), "lorem ipsum")
        self.assertEqual(t.get_comment_as_unicode(), u"lorem ipsum")

    def test_set_created_by(self):
        t = TorrentDef()
        t.set_created_by("dolor sit")
        self.assertEqual(t.get_created_by(), "dolor sit")

    @raises(ValueError)
    def test_set_urllist_wrong_url(self):
        t = TorrentDef()
        t.set_urllist(["http/url.com"])

    def test_set_urllist_urls(self):
        t = TorrentDef()
        t.set_urllist(["http://url.com"])
        self.assertEqual(t.get_urllist(), ["http://url.com"])

    @raises(ValueError)
    def test_set_httpseeds_wrong_url(self):
        t = TorrentDef()
        t.set_httpseeds(["http/httpseed.com"])

    def test_set_httpseeds(self):
        t = TorrentDef()
        t.set_httpseeds(["http://httpseed.com"])
        self.assertEqual(t.get_httpseeds(), ["http://httpseed.com"])

    @raises(ValueError)
    def test_set_piece_length_invalid_type(self):
        t = TorrentDef()
        t.set_piece_length("20")

    def test_get_piece_length(self):
        t = TorrentDef()
        self.assertEqual(t.get_piece_length(), 0)

    @raises(TorrentDefNotFinalizedException)
    def test_get_infohash(self):
        t = TorrentDef()
        t.get_infohash()

    @raises(TorrentDefNotFinalizedException)
    def test_set_name(self):
        t = TorrentDef()
        t.set_name("lorem ipsum")
        t.get_name()

    def test_load_from_dict(self):
        metainfo = {"info": {"name": "my_torrent", "piece length": 12345, "pieces": "12345678901234567890",
                                   "files": []}}
        torrent = TorrentDef.load_from_dict(metainfo)
        self.assertTrue(valid_torrent_file(torrent.get_metainfo()))

    @raises(TorrentDefNotFinalizedException)
    def test_no_valid_metainfo(self):
        t = TorrentDef()
        t.get_metainfo()

    def test_initial_peers(self):
        t = TorrentDef()
        self.assertFalse(t.get_initial_peers())

    def test_set_initial_peers(self):
        t = TorrentDef()
        t.set_initial_peers([("127.0.0.1", 1234)])
        self.assertEqual(t.get_initial_peers(), [("127.0.0.1", 1234)])

    def test_torrent_no_metainfo(self):
        torrent = TorrentDefNoMetainfo("12345678901234567890", self.VIDEO_FILE_NAME, "http://google.com")
        self.assertEqual(torrent.get_name(), self.VIDEO_FILE_NAME)
        self.assertEqual(torrent.get_infohash(), "12345678901234567890")
        self.assertEqual(torrent.get_length(), 0) # there are no files
        self.assertFalse(torrent.get_metainfo())
        self.assertEqual(torrent.get_url(), "http://google.com")
        self.assertFalse(torrent.is_multifile_torrent())
        self.assertEqual(torrent.get_name_as_unicode(), unicode(self.VIDEO_FILE_NAME))
        self.assertFalse(torrent.get_files())
        self.assertFalse(torrent.get_files_with_length())
        self.assertFalse(torrent.get_trackers_as_single_tuple())
        self.assertFalse(torrent.is_private())

        torrent2 = TorrentDefNoMetainfo("12345678901234567890", self.VIDEO_FILE_NAME, "magnet:")
        self.assertFalse(torrent2.get_trackers_as_single_tuple())

    def general_check(self, metainfo):
        self.assertTrue(valid_torrent_file(metainfo))
        self.assertEqual(metainfo['announce'], TRACKER)

    def test_get_index(self):
        t = TorrentDef()
        t.metainfo_valid = True
        t.metainfo = {'info': {'files': [{'path': ['a.txt'], 'length': 123}]}}
        self.assertEqual(t.get_index_of_file_in_files('a.txt'), 0)
        self.assertRaises(ValueError, t.get_index_of_file_in_files, 'b.txt')
        self.assertRaises(ValueError, t.get_index_of_file_in_files, None)

        t.metainfo = {'info': {'files': [{'path': ['a.txt'], 'path.utf-8': ['b.txt'], 'length': 123}]}}
        self.assertEqual(t.get_index_of_file_in_files('b.txt'), 0)
