#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Copyright 2022-2025 Mike Fährmann
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 as
# published by the Free Software Foundation.

import os
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from gallery_dl import ytdl, util, config, exception  # noqa E402
from gallery_dl.extractor.ytdl import YoutubeDLExtractor  # noqa E402


class Test_CommandlineArguments(unittest.TestCase):
    module_name = "youtube_dl"

    @classmethod
    def setUpClass(cls):
        try:
            cls.module = __import__(cls.module_name)
        except (ImportError, SyntaxError):
            raise unittest.SkipTest(
                f"cannot import module '{cls.module_name}'")
        cls.default = ytdl.parse_command_line(cls.module, [])
        cls.ytdlp = hasattr(cls.module, "cookies")

    def test_ignore_errors(self):
        self._("--ignore-errors" , "ignoreerrors", True)
        self._("--abort-on-error", "ignoreerrors", False)

    def test_default_search(self):
        self._(["--default-search", "foo"] , "default_search", "foo")

    def test_mark_watched(self):
        self._("--mark-watched"   , "mark_watched", True)
        self._("--no-mark-watched", "mark_watched", False)

    def test_proxy(self):
        self._(["--proxy", "socks5://127.0.0.1:1080/"],
               "proxy", "socks5://127.0.0.1:1080/")
        self._(["--geo-verification-proxy", "127.0.0.1"],
               "geo_verification_proxy", "127.0.0.1")

    def test_network_options(self):
        self._(["--socket-timeout", "3.5"],
               "socket_timeout", 3.5)
        self._(["--source-address", "127.0.0.1"],
               "source_address", "127.0.0.1")
        self._("-4"          , "source_address", "0.0.0.0")
        self._("--force-ipv4", "source_address", "0.0.0.0")
        self._("-6"          , "source_address", "::")
        self._("--force-ipv6", "source_address", "::")

    def test_thumbnail_options(self):
        self._("--write-thumbnail", "writethumbnail", True)
        self._("--write-all-thumbnails", "write_all_thumbnails", True)

    def test_authentication_options(self):
        self._(["-u"        , "foo"], "username", "foo")
        self._(["--username", "foo"], "username", "foo")

        self._(["-p"        , "bar"], "password", "bar")
        self._(["--password", "bar"], "password", "bar")

        self._(["--ap-mso"     , "mso"], "ap_mso", "mso")
        self._(["--ap-username", "foo"], "ap_username", "foo")
        self._(["--ap-password", "bar"], "ap_password", "bar")

        self._(["-2"         , "pass"], "twofactor", "pass")
        self._(["--twofactor", "pass"], "twofactor", "pass")

        self._(["--video-password", "pass"], "videopassword", "pass")

        self._("-n"     , "usenetrc", True)
        self._("--netrc", "usenetrc", True)

    def test_subtitle_options(self):
        self._("--write-sub"     , "writesubtitles"   , True)
        self._("--write-auto-sub", "writeautomaticsub", True)

        self._(["--sub-format", "best"], "subtitlesformat", "best")
        self._(["--sub-langs", "en,ru"], "subtitleslangs", ["en", "ru"])

    def test_retries(self):
        inf = float("inf")

        self._(["--retries", "5"], "retries", 5)
        self._(["--retries", "inf"], "retries", inf)
        self._(["--retries", "infinite"], "retries", inf)
        self._(["--fragment-retries", "8"], "fragment_retries", 8)
        self._(["--fragment-retries", "inf"], "fragment_retries", inf)
        self._(["--fragment-retries", "infinite"], "fragment_retries", inf)

    def test_geo_bypass(self):
        self._("--geo-bypass", "geo_bypass", True)
        self._("--no-geo-bypass", "geo_bypass", False)
        self._(["--geo-bypass-country", "EN"], "geo_bypass_country", "EN")
        self._(["--geo-bypass-ip-block", "198.51.100.14/24"],
               "geo_bypass_ip_block", "198.51.100.14/24")

    def test_headers(self):
        try:
            headers = self.module.utils.networking.std_headers
        except AttributeError:
            headers = self.module.std_headers

        self.assertNotEqual(headers["User-Agent"], "Foo/1.0")
        self._(["--user-agent", "Foo/1.0"])
        self.assertEqual(headers["User-Agent"], "Foo/1.0")

        self.assertNotIn("Referer", headers)
        self._(["--referer", "http://example.org/"])
        self.assertEqual(headers["Referer"], "http://example.org/")

        self.assertNotEqual(headers["Accept"], "*/*")
        self.assertNotIn("DNT", headers)
        self._([
            "--add-header", "accept:*/*",
            "--add-header", "dnt:1",
        ])
        self.assertEqual(headers["accept"], "*/*")
        self.assertEqual(headers["dnt"], "1")

    def test_extract_audio(self):
        opts = self._(["--extract-audio"])
        self.assertEqual(opts["postprocessors"][0], {
            "key": "FFmpegExtractAudio",
            "preferredcodec": "best",
            "preferredquality": "5",
            "nopostoverwrites": False,
        })

        opts = self._([
            "--extract-audio",
            "--audio-format", "opus",
            "--audio-quality", "9",
            "--no-post-overwrites",
        ])
        self.assertEqual(opts["postprocessors"][0], {
            "key": "FFmpegExtractAudio",
            "preferredcodec": "opus",
            "preferredquality": "9",
            "nopostoverwrites": True,
        })

    def test_recode_video(self):
        opts = self._(["--recode-video", " mkv "])
        self.assertEqual(opts["postprocessors"][0], {
            "key": "FFmpegVideoConvertor",
            "preferedformat": "mkv",
        })

    def test_subs(self):
        opts = self._(["--convert-subs", "srt"])
        conv = {"key": "FFmpegSubtitlesConvertor", "format": "srt"}
        if self.ytdlp:
            conv["when"] = "before_dl"
        self.assertEqual(opts["postprocessors"][0], conv)

    def test_embed(self):
        subs = {"key": "FFmpegEmbedSubtitle"}
        thumb = {"key": "EmbedThumbnail", "already_have_thumbnail": False}
        if self.ytdlp:
            subs["already_have_subtitle"] = False

        opts = self._(["--embed-subs", "--embed-thumbnail"])
        self.assertEqual(opts["postprocessors"][:2], [subs, thumb])

        thumb["already_have_thumbnail"] = True
        if self.ytdlp:
            subs["already_have_subtitle"] = True
            thumb["already_have_thumbnail"] = "all"

        opts = self._([
            "--embed-thumbnail",
            "--embed-subs",
            "--write-sub",
            "--write-all-thumbnails",
        ])
        self.assertEqual(opts["postprocessors"][:2], [subs, thumb])

    def test_metadata(self):
        opts = self._("--add-metadata")
        self.assertEqual(opts["postprocessors"][0], {"key": "FFmpegMetadata"})

    def test_metadata_from_title(self):
        opts = self._(["--metadata-from-title", "%(artist)s - %(title)s"])
        self.assertEqual(opts["postprocessors"][0], {
            "key": "MetadataFromTitle",
            "titleformat": "%(artist)s - %(title)s",
        })

    def test_xattr(self):
        opts = self._("--xattrs")
        self.assertEqual(opts["postprocessors"][0], {"key": "XAttrMetadata"})

    def test_noop(self):
        cmdline = [
            "--update",
            "--dump-user-agent",
            "-F",
            "--list-formats",
            "--list-extractors",
            "--list-thumbnails",
            "--list-subs",
            "--ap-list-mso",
            "--extractor-descriptions",
            "--ignore-config",
        ]

        if not self.ytdlp:
            cmdline.extend((
                "--dump-json",
                "--dump-single-json",
                "--config-location", "~",
            ))

        result = self._(cmdline)
        result["daterange"] = self.default["daterange"]
        self.assertEqual(result, self.default)

    def _(self, cmdline, option=util.SENTINEL, expected=None):
        if isinstance(cmdline, str):
            cmdline = [cmdline]
        result = ytdl.parse_command_line(self.module, cmdline)
        if option is not util.SENTINEL:
            self.assertEqual(result[option], expected, option)
        return result


class Test_CommandlineArguments_YtDlp(Test_CommandlineArguments):
    module_name = "yt_dlp"

    def test_retries_extractor(self):
        inf = float("inf")

        self._(["--extractor-retries", "5"], "extractor_retries", 5)
        self._(["--extractor-retries", "inf"], "extractor_retries", inf)
        self._(["--extractor-retries", "infinite"], "extractor_retries", inf)

    def test_remuxs_video(self):
        opts = self._(["--remux-video", " mkv "])
        self.assertEqual(opts["postprocessors"][0], {
            "key": "FFmpegVideoRemuxer",
            "preferedformat": "mkv",
        })

    def test_metadata(self):
        opts = self._(["--embed-metadata",
                       "--no-embed-chapters",
                       "--embed-info-json"])
        self.assertEqual(opts["postprocessors"][0], {
            "key": "FFmpegMetadata",
            "add_chapters": False,
            "add_metadata": True,
            "add_infojson": True,
        })

    def test_metadata_from_title(self):
        opts = self._(["--metadata-from-title", "%(artist)s - %(title)s"])
        self.assertEqual(opts["postprocessors"][0], {
            "key"    : "MetadataParser",
            "when"   : "pre_process",
            "actions": [self.module.MetadataFromFieldPP.to_action(
                "title:%(artist)s - %(title)s")],
        })

    def test_geo_bypass(self):
        try:
            ytdl.parse_command_line(self.module, ["--xff", "default"])
        except Exception:
            # before --xff (c16644642)
            return Test_CommandlineArguments.test_geo_bypass(self)

        self._(["--xff", "default"],
               "geo_bypass", "default")
        self._(["--xff", "never"],
               "geo_bypass", "never")
        self._(["--xff", "EN"],
               "geo_bypass", "EN")
        self._(["--xff", "198.51.100.14/24"],
               "geo_bypass", "198.51.100.14/24")

        self._("--geo-bypass",
               "geo_bypass", "default")
        self._("--no-geo-bypass",
               "geo_bypass", "never")
        self._(["--geo-bypass-country", "EN"],
               "geo_bypass", "EN")
        self._(["--geo-bypass-ip-block", "198.51.100.14/24"],
               "geo_bypass", "198.51.100.14/24")

    def test_cookiesfrombrowser(self):
        self._(["--cookies-from-browser", "firefox"],
               "cookiesfrombrowser", ("firefox", None, None, None))
        self._(["--cookies-from-browser", "firefox:profile"],
               "cookiesfrombrowser", ("firefox", "profile", None, None))
        self._(["--cookies-from-browser", "firefox+keyring"],
               "cookiesfrombrowser", ("firefox", None, "KEYRING", None))
        self._(["--cookies-from-browser", "firefox::container"],
               "cookiesfrombrowser", ("firefox", None, None, "container"))
        self._(["--cookies-from-browser",
                "firefox+keyring:profile::container"],
               "cookiesfrombrowser",
               ("firefox", "profile", "KEYRING", "container"))


class MockYtdlModule:
    class utils:
        class YoutubeDLError(Exception):
            pass


class MockYtdlInstance:
    def __init__(self, ignoreerrors):
        self.params = {"ignoreerrors": ignoreerrors}


class Test_YoutubeDLExtractor(unittest.TestCase):
    def setUp(self):
        class ExtractorMock(YoutubeDLExtractor):
            def __init__(self):
                self.exc = exception
        self.extr = ExtractorMock.__new__(ExtractorMock)
        self.extr.exc = exception

    def test_process_entries_restart_propagation(self):
        ytdl_module = MockYtdlModule()
        ytdl_instance = MockYtdlInstance(ignoreerrors=False)

        def mock_extract_info(url, dl, ie_key):
            raise exception.RestartExtraction()
        ytdl_instance.extract_info = mock_extract_info

        entries = [{"_type": "url", "url": "test_url"}]
        with self.assertRaises(exception.RestartExtraction):
            list(self.extr._process_entries(
                ytdl_module, ytdl_instance, entries))

    def test_process_entries_ignoreerrors_true(self):
        ytdl_module = MockYtdlModule()
        ytdl_instance = MockYtdlInstance(ignoreerrors=True)

        def mock_extract_info(url, dl, ie_key):
            raise ytdl_module.utils.YoutubeDLError("Some Error")
        ytdl_instance.extract_info = mock_extract_info

        entries = [
            {"_type": "url", "url": "test_url1"},
            {"_type": "url", "url": "test_url2"}
        ]
        results = list(self.extr._process_entries(
            ytdl_module, ytdl_instance, entries))
        self.assertEqual(results, [])

    def test_process_entries_ignoreerrors_false(self):
        ytdl_module = MockYtdlModule()
        ytdl_instance = MockYtdlInstance(ignoreerrors=False)

        def mock_extract_info(url, dl, ie_key):
            raise ytdl_module.utils.YoutubeDLError("Some Error")
        ytdl_instance.extract_info = mock_extract_info

        entries = [{"_type": "url", "url": "test_url"}]
        with self.assertRaises(exception.AbortExtraction):
            list(self.extr._process_entries(
                ytdl_module, ytdl_instance, entries))


if __name__ == "__main__":
    unittest.main(warnings="ignore")
