import logging
import os
import re
import sys
import unittest
from argparse import Namespace
from io import BytesIO
from pathlib import Path, PosixPath, WindowsPath
from textwrap import dedent
from unittest.mock import Mock, call, patch

import freezegun
import pytest

import streamlink_cli.main
import tests.resources
from streamlink.exceptions import PluginError, StreamError, StreamlinkDeprecationWarning
from streamlink.session import Streamlink
from streamlink.stream.stream import Stream
from streamlink_cli.compat import stdout
from streamlink_cli.main import (
    Formatter,
    NoPluginError,
    create_output,
    format_valid_streams,
    handle_stream,
    handle_url,
    output_stream,
    resolve_stream_name,
)
from streamlink_cli.output import FileOutput, PlayerOutput
from tests.plugin.testplugin import TestPlugin as _TestPlugin


# TODO: rewrite the entire mess


class FakePlugin(_TestPlugin):
    __module__ = "fake"
    _streams = {}  # type: ignore

    def streams(self, *args, **kwargs):
        return self._streams

    def _get_streams(self):  # pragma: no cover
        pass


class TestCLIMain(unittest.TestCase):
    def test_resolve_stream_name(self):
        a = Mock()
        b = Mock()
        c = Mock()
        d = Mock()
        e = Mock()
        streams = {
            "160p": a,
            "360p": b,
            "480p": c,
            "720p": d,
            "1080p": e,
            "worst": b,
            "best": d,
            "worst-unfiltered": a,
            "best-unfiltered": e,
        }

        assert resolve_stream_name(streams, "unknown") == "unknown"
        assert resolve_stream_name(streams, "160p") == "160p"
        assert resolve_stream_name(streams, "360p") == "360p"
        assert resolve_stream_name(streams, "480p") == "480p"
        assert resolve_stream_name(streams, "720p") == "720p"
        assert resolve_stream_name(streams, "1080p") == "1080p"
        assert resolve_stream_name(streams, "worst") == "360p"
        assert resolve_stream_name(streams, "best") == "720p"
        assert resolve_stream_name(streams, "worst-unfiltered") == "160p"
        assert resolve_stream_name(streams, "best-unfiltered") == "1080p"

    def test_format_valid_streams(self):
        a = Mock()
        b = Mock()
        c = Mock()

        streams = {
            "audio": a,
            "720p": b,
            "1080p": c,
            "worst": b,
            "best": c,
        }
        assert format_valid_streams(_TestPlugin, streams) == ", ".join([
            "audio",
            "720p (worst)",
            "1080p (best)",
        ])

        streams = {
            "audio": a,
            "720p": b,
            "1080p": c,
            "worst-unfiltered": b,
            "best-unfiltered": c,
        }
        assert format_valid_streams(_TestPlugin, streams) == ", ".join([
            "audio",
            "720p (worst-unfiltered)",
            "1080p (best-unfiltered)",
        ])


class TestCLIMainHandleUrl:
    @pytest.mark.parametrize(("side_effect", "expected"), [
        (NoPluginError("foo"), "No plugin can handle URL: fakeurl"),
        (PluginError("bar"), "bar"),
    ])
    def test_error(self, side_effect, expected):
        with patch("streamlink_cli.main.args", Mock(url="fakeurl")), \
             patch("streamlink_cli.main.streamlink", resolve_url=Mock(side_effect=side_effect)), \
             patch("streamlink_cli.main.console", exit=Mock(side_effect=SystemExit)) as mock_console:
            with pytest.raises(SystemExit):
                handle_url()
            assert mock_console.exit.mock_calls == [call(expected)]


class TestCLIMainJsonAndStreamUrl(unittest.TestCase):
    @patch("streamlink_cli.main.args", json=True, stream_url=True, subprocess_cmdline=False)
    @patch("streamlink_cli.main.console")
    def test_handle_stream_with_json_and_stream_url(self, console, args):
        stream = Mock()
        streams = dict(best=stream)

        plugin = FakePlugin(Mock(), "")
        plugin._streams = streams

        handle_stream(plugin, streams, "best")
        assert console.msg.mock_calls == []
        assert console.msg_json.mock_calls == [call(
            stream,
            metadata=dict(
                id="test-id-1234-5678",
                author="Tѥst Āuƭhǿr",
                category=None,
                title="Test Title",
            ),
        )]
        assert console.error.mock_calls == []
        console.msg_json.mock_calls.clear()

        args.json = False
        handle_stream(plugin, streams, "best")
        assert console.msg.mock_calls == [call(stream.to_url())]
        assert console.msg_json.mock_calls == []
        assert console.error.mock_calls == []
        console.msg.mock_calls.clear()

        stream.to_url.side_effect = TypeError()
        handle_stream(plugin, streams, "best")
        assert console.msg.mock_calls == []
        assert console.msg_json.mock_calls == []
        assert console.exit.mock_calls == [call("The stream specified cannot be translated to a URL")]

    @patch("streamlink_cli.main.args", json=True, stream_url=True, stream=[], default_stream=[], retry_max=0, retry_streams=0)
    @patch("streamlink_cli.main.console")
    def test_handle_url_with_json_and_stream_url(self, console, args):
        stream = Mock()
        streams = dict(worst=Mock(), best=stream)

        class _FakePlugin(FakePlugin):
            __module__ = FakePlugin.__module__
            _streams = streams

        with patch("streamlink_cli.main.streamlink", resolve_url=Mock(return_value=("fake", _FakePlugin, ""))):
            handle_url()
            assert console.msg.mock_calls == []
            assert console.msg_json.mock_calls == [call(
                plugin="fake",
                metadata=dict(
                    id="test-id-1234-5678",
                    author="Tѥst Āuƭhǿr",
                    category=None,
                    title="Test Title",
                ),
                streams=streams,
            )]
            assert console.error.mock_calls == []
            console.msg_json.mock_calls.clear()

            args.json = False
            handle_url()
            assert console.msg.mock_calls == [call(stream.to_manifest_url())]
            assert console.msg_json.mock_calls == []
            assert console.error.mock_calls == []
            console.msg.mock_calls.clear()

            stream.to_manifest_url.side_effect = TypeError()
            handle_url()
            assert console.msg.mock_calls == []
            assert console.msg_json.mock_calls == []
            assert console.exit.mock_calls == [call("The stream specified cannot be translated to a URL")]
            console.exit.mock_calls.clear()


# TODO: don't use Mock() for mocking args, use a custom argparse.Namespace instead
class TestCLIMainCreateOutput(unittest.TestCase):
    @patch("streamlink_cli.main.args")
    @patch("streamlink_cli.main.console", Mock())
    @patch("streamlink_cli.main.DEFAULT_STREAM_METADATA", {"title": "bar"})
    def test_create_output_no_file_output_options(self, args: Mock):
        formatter = Formatter({
            "author": lambda: "foo",
        })
        args.output = None
        args.stdout = None
        args.record = None
        args.record_and_pipe = None
        args.player_fifo = False
        args.player_http = False
        args.title = None
        args.url = "URL"
        args.player = Path("mpv")
        args.player_args = ""
        args.player_env = None

        output = create_output(formatter)
        assert type(output) is PlayerOutput
        assert output.playerargs.title == "URL"
        assert output.env == {}

        args.title = "{author} - {title}"
        output = create_output(formatter)
        assert type(output) is PlayerOutput
        assert output.playerargs.title == "foo - bar"

    @patch("streamlink_cli.main.args")
    @patch("streamlink_cli.main.check_file_output")
    def test_create_output_file_output(self, mock_check_file_output: Mock, args: Mock):
        formatter = Formatter({})
        mock_check_file_output.side_effect = lambda path, force: FileOutput(path)
        args.output = "foo"
        args.stdout = None
        args.record = None
        args.record_and_pipe = None
        args.force = False
        args.fs_safe_rules = None

        output = create_output(formatter)
        assert mock_check_file_output.call_args_list == [call(Path("foo"), False)]
        assert type(output) is FileOutput
        assert output.filename == Path("foo")
        assert output.fd is None
        assert output.record is None

    @patch("streamlink_cli.main.args")
    def test_create_output_stdout(self, args: Mock):
        formatter = Formatter({})
        args.output = None
        args.stdout = True
        args.record = None
        args.record_and_pipe = None

        output = create_output(formatter)
        assert type(output) is FileOutput
        assert output.filename is None
        assert output.fd is stdout
        assert output.record is None

        args.output = "-"
        args.stdout = False
        output = create_output(formatter)
        assert type(output) is FileOutput
        assert output.filename is None
        assert output.fd is stdout
        assert output.record is None

    @patch("streamlink_cli.main.args")
    @patch("streamlink_cli.main.check_file_output")
    def test_create_output_record_and_pipe(self, mock_check_file_output: Mock, args: Mock):
        formatter = Formatter({})
        mock_check_file_output.side_effect = lambda path, force: FileOutput(path)
        args.output = None
        args.stdout = None
        args.record_and_pipe = "foo"
        args.force = False
        args.fs_safe_rules = None

        output = create_output(formatter)
        assert mock_check_file_output.call_args_list == [call(Path("foo"), False)]
        assert type(output) is FileOutput
        assert output.filename is None
        assert output.fd is stdout
        assert type(output.record) is FileOutput
        assert output.record.filename == Path("foo")
        assert output.record.fd is None
        assert output.record.record is None

    @patch("streamlink_cli.main.args")
    @patch("streamlink_cli.main.check_file_output")
    @patch("streamlink_cli.main.DEFAULT_STREAM_METADATA", {"title": "bar"})
    def test_create_output_record(self, mock_check_file_output: Mock, args: Mock):
        formatter = Formatter({
            "author": lambda: "foo",
        })
        mock_check_file_output.side_effect = lambda path, force: FileOutput(path)
        args.output = None
        args.stdout = None
        args.record = "foo"
        args.record_and_pipe = None
        args.force = False
        args.fs_safe_rules = None
        args.title = None
        args.url = "URL"
        args.player = Path("mpv")
        args.player_args = ""
        args.player_env = [("VAR1", "abc"), ("VAR2", "def")]
        args.player_fifo = None
        args.player_http = None

        output = create_output(formatter)
        assert type(output) is PlayerOutput
        assert output.playerargs.title == "URL"
        assert output.env == {"VAR1": "abc", "VAR2": "def"}
        assert type(output.record) is FileOutput
        assert output.record.filename == Path("foo")
        assert output.record.fd is None
        assert output.record.record is None

        args.title = "{author} - {title}"
        output = create_output(formatter)
        assert type(output) is PlayerOutput
        assert output.playerargs.title == "foo - bar"
        assert type(output.record) is FileOutput
        assert output.record.filename == Path("foo")
        assert output.record.fd is None
        assert output.record.record is None

    @patch("streamlink_cli.main.args")
    @patch("streamlink_cli.main.DEFAULT_STREAM_METADATA", {"title": "bar"})
    def test_create_output_record_stdout(self, args: Mock):
        formatter = Formatter({
            "author": lambda: "foo",
        })
        args.output = None
        args.stdout = None
        args.record = "-"
        args.record_and_pipe = None
        args.force = False
        args.fs_safe_rules = None
        args.title = "{author} - {title}"
        args.url = "URL"
        args.player = Path("mpv")
        args.player_args = ""
        args.player_env = [("VAR1", "abc"), ("VAR2", "def")]
        args.player_fifo = None
        args.player_http = None

        output = create_output(formatter)
        assert type(output) is PlayerOutput
        assert output.playerargs.title == "foo - bar"
        assert output.env == {"VAR1": "abc", "VAR2": "def"}
        assert type(output.record) is FileOutput
        assert output.record.filename is None
        assert output.record.fd is stdout
        assert output.record.record is None

    @patch("streamlink_cli.main.args")
    @patch("streamlink_cli.main.console")
    def test_create_output_record_and_other_file_output(self, console: Mock, args: Mock):
        formatter = Formatter({})
        args.output = None
        args.stdout = True
        args.record_and_pipe = True
        create_output(formatter)
        console.exit.assert_called_with("Cannot use record options with other file output options.")

    @patch("streamlink_cli.main.args")
    @patch("streamlink_cli.main.console")
    def test_create_output_no_default_player(self, console: Mock, args: Mock):
        formatter = Formatter({})
        args.output = None
        args.stdout = False
        args.record_and_pipe = False
        args.player = None
        console.exit.side_effect = SystemExit
        with pytest.raises(SystemExit):
            create_output(formatter)
        assert re.search(r"^The default player \(\w+\) does not seem to be installed\.", console.exit.call_args_list[0][0][0])


class TestCLIMainHandleStream(unittest.TestCase):
    @patch("streamlink_cli.main.output_stream")
    @patch("streamlink_cli.main.args")
    def test_handle_stream_output_stream(self, args: Mock, mock_output_stream: Mock):
        args.json = False
        args.subprocess_cmdline = False
        args.stream_url = False
        args.output = False
        args.stdout = False
        args.player_passthrough = []
        args.player_external_http = False
        args.player_continuous_http = False
        mock_output_stream.return_value = True

        session = Mock()
        plugin = FakePlugin(session, "")
        stream = Stream(session)
        streams = {"best": stream}

        handle_stream(plugin, streams, "best")
        assert mock_output_stream.call_count == 1
        paramStream, paramFormatter = mock_output_stream.call_args[0]
        assert paramStream is stream
        assert isinstance(paramFormatter, Formatter)


class TestCLIMainOutputStream:
    def test_stream_failure_no_output_open(self, caplog: pytest.LogCaptureFixture):
        output = Mock()
        stream = Mock(
            __str__=lambda _: "fake-stream",
            open=Mock(side_effect=StreamError("failure")),
        )
        formatter = Mock()

        caplog.set_level(1, "streamlink.cli")

        with patch("streamlink_cli.main.args", Namespace(retry_open=2)), \
             patch("streamlink_cli.main.console") as mock_console, \
             patch("streamlink_cli.main.output"), \
             patch("streamlink_cli.main.create_output", return_value=output):
            output_stream(stream, formatter)

        assert [(record.levelname, record.module, record.message) for record in caplog.records] == [
            ("error", "main", "Try 1/2: Could not open stream fake-stream (Could not open stream: failure)"),
            ("error", "main", "Try 2/2: Could not open stream fake-stream (Could not open stream: failure)"),
        ]
        assert mock_console.exit.call_args_list == [
            call("Could not open stream fake-stream, tried 2 times, exiting"),
        ]
        assert not output.open.called, "Does not open the output on stream error"

    @pytest.mark.parametrize(
        ("args", "isatty", "deprecation", "expected"),
        [
            ({"progress": "yes", "force_progress": False}, True, False, True),
            ({"progress": "no", "force_progress": False}, True, False, False),
            ({"progress": "yes", "force_progress": False}, False, False, False),
            ({"progress": "no", "force_progress": False}, False, False, False),
            ({"progress": "force", "force_progress": False}, False, False, True),
            ({"progress": "yes", "force_progress": True}, False, True, True),
            ({"progress": "no", "force_progress": True}, False, True, True),
        ],
    )
    def test_show_progress(
        self,
        caplog: pytest.LogCaptureFixture,
        recwarn: pytest.WarningsRecorder,
        args: dict,
        isatty: bool,
        deprecation: bool,
        expected: bool,
    ):
        streamio = BytesIO(b"0" * 8192 * 2)
        stream = Mock(open=Mock(return_value=streamio))
        output = Mock()
        formatter = Mock()

        caplog.set_level(1, "streamlink.cli")

        with patch("streamlink_cli.main.sys.stderr.isatty", return_value=isatty), \
             patch("streamlink_cli.main.args", Namespace(retry_open=1, **args)), \
             patch("streamlink_cli.main.console") as mock_console, \
             patch("streamlink_cli.main.output"), \
             patch("streamlink_cli.main.create_output", return_value=output), \
             patch("streamlink_cli.main.StreamRunner") as mock_streamrunner:
            assert output_stream(stream, formatter)

        assert not mock_console.exit.called
        assert [(record.levelname, record.module, record.message) for record in caplog.records] == [
            ("debug", "main", "Pre-buffering 8192 bytes"),
            ("debug", "main", "Writing stream to output"),
        ]
        assert [(record.category, str(record.message)) for record in recwarn.list] == ([(
            StreamlinkDeprecationWarning,
            "The --force-progress option has been deprecated in favor of --progress=force",
        )] if deprecation else [])
        assert mock_streamrunner.call_args_list == [call(streamio, output, show_progress=expected)]


# TODO: rewrite using pytest (caplog+capsys fixtures) and move to separate test module
class _TestCLIMainLogging(unittest.TestCase):
    # stop test execution at the setup_signals() call, as we're not interested in what comes afterwards
    class StopTest(Exception):
        pass

    @classmethod
    def subject(cls, argv, **kwargs):
        session = Streamlink(plugins_builtin=False)
        session.plugins.load_path(Path(tests.__path__[0]) / "plugin")

        with patch("streamlink_cli.main.os.geteuid", create=True, new=Mock(return_value=kwargs.get("euid", 1000))), \
             patch("streamlink_cli.main.streamlink", session), \
             patch("streamlink_cli.main.setup_signals", side_effect=cls.StopTest), \
             patch("streamlink_cli.main.CONFIG_FILES", []), \
             patch("streamlink_cli.main.setup_streamlink"), \
             patch("streamlink_cli.main.setup_plugins"), \
             patch("streamlink_cli.argparser.find_default_player"), \
             patch("sys.argv") as mock_argv:
            mock_argv.__getitem__.side_effect = lambda x: argv[x]
            try:
                streamlink_cli.main.main()
            except cls.StopTest:
                pass

    def tearDown(self):
        streamlink_cli.main.logger.root.handlers.clear()

    _write_call_log_cli_info = [call("[cli][info] foo\n")]
    _write_call_console_msg = [call("bar\n")]
    _write_call_console_msg_error = [call("error: bar\n")]
    _write_call_console_msg_json = [call("{\n  \"error\": \"bar\"\n}\n")]

    _write_calls = _write_call_log_cli_info + _write_call_console_msg

    def write_file_and_assert(self, mock_mkdir: Mock, mock_write: Mock, mock_stdout: Mock):
        streamlink_cli.main.log.info("foo")
        streamlink_cli.main.console.msg("bar")
        assert mock_mkdir.mock_calls == [call(parents=True, exist_ok=True)]
        assert mock_write.mock_calls == self._write_calls
        assert not mock_stdout.write.called


class TestCLIMainLoggingStreams(_TestCLIMainLogging):
    _write_call_log_testcli_err = [call("[test_cli_main][error] baz\n")]

    def subject(self, argv, stream=None):
        super().subject(argv)
        childlogger = logging.getLogger("streamlink.test_cli_main")

        streamlink_cli.main.log.info("foo")
        childlogger.error("baz")
        with pytest.raises(SystemExit):
            streamlink_cli.main.console.exit("bar")

        assert streamlink_cli.main.log.parent.handlers[0].stream is stream
        assert childlogger.parent.handlers[0].stream is stream
        assert streamlink_cli.main.console.output is stream

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_stream_stdout(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--stdout"], mock_stderr)

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_stream_output_eq_file(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--output=foo"], mock_stdout)

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_stream_output_eq_dash(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--output=-"], mock_stderr)

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_stream_record_eq_file(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--record=foo"], mock_stdout)

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_stream_record_eq_dash(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--record=-"], mock_stderr)

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_stream_record_and_pipe(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--record-and-pipe=foo"], mock_stderr)

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_no_pipe_no_json(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink"], mock_stdout)
        assert mock_stdout.write.mock_calls == (
            self._write_call_log_cli_info
            + self._write_call_log_testcli_err
            + self._write_call_console_msg_error
        )
        assert mock_stderr.write.mock_calls == []

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_no_pipe_json(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--json"], mock_stdout)
        assert mock_stdout.write.mock_calls == self._write_call_console_msg_json
        assert mock_stderr.write.mock_calls == []

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_pipe_no_json(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--stdout"], mock_stderr)
        assert mock_stdout.write.mock_calls == []
        assert mock_stderr.write.mock_calls == (
            self._write_call_log_cli_info
            + self._write_call_log_testcli_err
            + self._write_call_console_msg_error
        )

    @patch("sys.stderr")
    @patch("sys.stdout")
    def test_pipe_json(self, mock_stdout: Mock, mock_stderr: Mock):
        self.subject(["streamlink", "--stdout", "--json"], mock_stderr)
        assert mock_stdout.write.mock_calls == []
        assert mock_stderr.write.mock_calls == self._write_call_console_msg_json


class TestCLIMainLoggingInfos(_TestCLIMainLogging):
    @pytest.mark.posix_only()
    @patch("streamlink_cli.main.log")
    def test_log_root_warning(self, mock_log):
        self.subject(["streamlink"], euid=0)
        assert mock_log.info.mock_calls == [call("streamlink is running as root! Be careful!")]

    @patch("streamlink_cli.main.log")
    @patch("streamlink_cli.main.streamlink_version", "streamlink")
    @patch("streamlink_cli.main.importlib.metadata")
    @patch("streamlink_cli.main.log_current_arguments", Mock(side_effect=_TestCLIMainLogging.StopTest))
    @patch("platform.python_version", Mock(return_value="python"))
    @patch("ssl.OPENSSL_VERSION", "OPENSSL_VERSION")
    def test_log_current_versions(self, mock_importlib_metadata: Mock, mock_log: Mock):
        class FakePackageNotFoundError(Exception):
            pass

        def version(dist):
            if dist == "foo":
                return "1.2.3"
            if dist == "bar-baz":
                return "2.0.0"
            raise FakePackageNotFoundError()

        mock_importlib_metadata.PackageNotFoundError = FakePackageNotFoundError
        mock_importlib_metadata.requires.return_value = ["foo>1", "bar-baz==2", "qux~=3"]
        mock_importlib_metadata.version.side_effect = version

        self.subject(["streamlink", "--loglevel", "info"])
        assert mock_log.debug.mock_calls == [], "Doesn't log anything if not debug logging"

        with patch("sys.platform", "linux"), \
             patch("platform.platform", Mock(return_value="linux")):
            self.subject(["streamlink", "--loglevel", "debug"])
            assert mock_importlib_metadata.requires.mock_calls == [call("streamlink")]
            assert mock_log.debug.mock_calls == [
                call("OS:         linux"),
                call("Python:     python"),
                call("OpenSSL:    OPENSSL_VERSION"),
                call("Streamlink: streamlink"),
                call("Dependencies:"),
                call(" foo: 1.2.3"),
                call(" bar-baz: 2.0.0"),
            ]
            mock_importlib_metadata.requires.reset_mock()
            mock_log.debug.reset_mock()

        with patch("sys.platform", "darwin"), \
             patch("platform.mac_ver", Mock(return_value=["0.0.0"])):
            self.subject(["streamlink", "--loglevel", "debug"])
            assert mock_importlib_metadata.requires.mock_calls == [call("streamlink")]
            assert mock_log.debug.mock_calls == [
                call("OS:         macOS 0.0.0"),
                call("Python:     python"),
                call("OpenSSL:    OPENSSL_VERSION"),
                call("Streamlink: streamlink"),
                call("Dependencies:"),
                call(" foo: 1.2.3"),
                call(" bar-baz: 2.0.0"),
            ]
            mock_importlib_metadata.requires.reset_mock()
            mock_log.debug.reset_mock()

        with patch("sys.platform", "win32"), \
             patch("platform.system", Mock(return_value="Windows")), \
             patch("platform.release", Mock(return_value="0.0.0")):
            self.subject(["streamlink", "--loglevel", "debug"])
            assert mock_importlib_metadata.requires.mock_calls == [call("streamlink")]
            assert mock_log.debug.mock_calls == [
                call("OS:         Windows 0.0.0"),
                call("Python:     python"),
                call("OpenSSL:    OPENSSL_VERSION"),
                call("Streamlink: streamlink"),
                call("Dependencies:"),
                call(" foo: 1.2.3"),
                call(" bar-baz: 2.0.0"),
            ]
            mock_importlib_metadata.requires.reset_mock()
            mock_log.debug.reset_mock()

    @patch("streamlink_cli.main.log")
    def test_log_current_arguments(self, mock_log):
        self.subject([
            "streamlink",
            "--loglevel", "info",
        ])
        assert mock_log.debug.mock_calls == [], "Doesn't log anything if not debug logging"

        self.subject([
            "streamlink",
            "--loglevel", "debug",
            "-p", "custom",
            "--testplugin-bool",
            "--testplugin-password=secret",
            "test.se/channel",
            "best,worst",
        ])
        assert mock_log.debug.mock_calls[-7:] == [
            call("Arguments:"),
            call(" url=test.se/channel"),
            call(" stream=['best', 'worst']"),
            call(" --loglevel=debug"),
            call(" --player=custom"),
            call(" --testplugin-bool=True"),
            call(" --testplugin-password=********"),
        ]


class TestCLIMainLoggingLogfile(_TestCLIMainLogging):
    @patch("sys.stdout")
    @patch("builtins.open")
    def test_logfile_no_logfile(self, mock_open, mock_stdout):
        self.subject(["streamlink"])
        streamlink_cli.main.log.info("foo")
        streamlink_cli.main.console.msg("bar")
        assert streamlink_cli.main.console.output == sys.stdout
        assert not mock_open.called
        assert mock_stdout.write.mock_calls == self._write_calls

    @patch("sys.stdout")
    @patch("builtins.open")
    def test_logfile_loglevel_none(self, mock_open, mock_stdout):
        self.subject(["streamlink", "--loglevel", "none", "--logfile", "foo"])
        streamlink_cli.main.log.info("foo")
        streamlink_cli.main.console.msg("bar")
        assert streamlink_cli.main.console.output == sys.stdout
        assert not mock_open.called
        assert mock_stdout.write.mock_calls == [call("bar\n")]

    @patch("sys.stdout")
    @patch("builtins.open")
    @patch("pathlib.Path.mkdir", Mock())
    def test_logfile_path_relative(self, mock_open, mock_stdout):
        path = Path("foo").resolve()
        self.subject(["streamlink", "--logfile", "foo"])
        self.write_file_and_assert(
            mock_mkdir=path.mkdir,
            mock_write=mock_open(str(path), "a").write,
            mock_stdout=mock_stdout,
        )


@pytest.mark.posix_only()
class TestCLIMainLoggingLogfilePosix(_TestCLIMainLogging):
    @patch("sys.stdout")
    @patch("builtins.open")
    @patch("pathlib.Path.mkdir", Mock())
    def test_logfile_path_absolute(self, mock_open, mock_stdout):
        self.subject(["streamlink", "--logfile", "/foo/bar"])
        self.write_file_and_assert(
            mock_mkdir=PosixPath("/foo").mkdir,
            mock_write=mock_open("/foo/bar", "a").write,
            mock_stdout=mock_stdout,
        )

    @patch("sys.stdout")
    @patch("builtins.open")
    @patch("pathlib.Path.mkdir", Mock())
    def test_logfile_path_expanduser(self, mock_open, mock_stdout):
        with patch.dict(os.environ, {"HOME": "/foo"}):
            self.subject(["streamlink", "--logfile", "~/bar"])
        self.write_file_and_assert(
            mock_mkdir=PosixPath("/foo").mkdir,
            mock_write=mock_open("/foo/bar", "a").write,
            mock_stdout=mock_stdout,
        )

    @patch("sys.stdout")
    @patch("builtins.open")
    @patch("pathlib.Path.mkdir", Mock())
    @freezegun.freeze_time("2000-01-02T03:04:05Z")
    def test_logfile_path_auto(self, mock_open, mock_stdout):
        with patch("streamlink_cli.constants.LOG_DIR", PosixPath("/foo")):
            self.subject(["streamlink", "--logfile", "-"])
        self.write_file_and_assert(
            mock_mkdir=PosixPath("/foo").mkdir,
            mock_write=mock_open("/foo/2000-01-02_03-04-05.log", "a").write,
            mock_stdout=mock_stdout,
        )


@pytest.mark.windows_only()
class TestCLIMainLoggingLogfileWindows(_TestCLIMainLogging):
    @patch("sys.stdout")
    @patch("builtins.open")
    @patch("pathlib.Path.mkdir", Mock())
    def test_logfile_path_absolute(self, mock_open, mock_stdout):
        self.subject(["streamlink", "--logfile", "C:\\foo\\bar"])
        self.write_file_and_assert(
            mock_mkdir=WindowsPath("C:\\foo").mkdir,
            mock_write=mock_open("C:\\foo\\bar", "a").write,
            mock_stdout=mock_stdout,
        )

    @patch("sys.stdout")
    @patch("builtins.open")
    @patch("pathlib.Path.mkdir", Mock())
    def test_logfile_path_expanduser(self, mock_open, mock_stdout):
        with patch.dict(os.environ, {"USERPROFILE": "C:\\foo"}):
            self.subject(["streamlink", "--logfile", "~\\bar"])
        self.write_file_and_assert(
            mock_mkdir=WindowsPath("C:\\foo").mkdir,
            mock_write=mock_open("C:\\foo\\bar", "a").write,
            mock_stdout=mock_stdout,
        )

    @patch("sys.stdout")
    @patch("builtins.open")
    @patch("pathlib.Path.mkdir", Mock())
    @freezegun.freeze_time("2000-01-02T03:04:05Z")
    def test_logfile_path_auto(self, mock_open, mock_stdout):
        with patch("streamlink_cli.constants.LOG_DIR", WindowsPath("C:\\foo")):
            self.subject(["streamlink", "--logfile", "-"])
        self.write_file_and_assert(
            mock_mkdir=WindowsPath("C:\\foo").mkdir,
            mock_write=mock_open("C:\\foo\\2000-01-02_03-04-05.log", "a").write,
            mock_stdout=mock_stdout,
        )


class TestCLIMainPrint(unittest.TestCase):
    def subject(self):
        with patch.object(Streamlink, "resolve_url") as mock_resolve_url, \
             patch.object(Streamlink, "resolve_url_no_redirect") as mock_resolve_url_no_redirect:
            session = Streamlink(plugins_builtin=False)
            session.plugins.load_path(Path(tests.__path__[0]) / "plugin")
            with patch("streamlink_cli.main.os.geteuid", create=True, new=Mock(return_value=1000)), \
                 patch("streamlink_cli.main.streamlink", session), \
                 patch("streamlink_cli.main.CONFIG_FILES", []), \
                 patch("streamlink_cli.main.setup_streamlink"), \
                 patch("streamlink_cli.main.setup_plugins"), \
                 patch("streamlink_cli.main.setup_signals"):
                with pytest.raises(SystemExit) as cm:
                    streamlink_cli.main.main()
                assert cm.value.code == 0
                mock_resolve_url.assert_not_called()
                mock_resolve_url_no_redirect.assert_not_called()

    def tearDown(self):
        streamlink_cli.main.logger.root.handlers.clear()

    @staticmethod
    def get_stdout(mock_stdout):
        return "".join([call_arg[0][0] for call_arg in mock_stdout.write.call_args_list])

    @patch("sys.stdout")
    @patch("sys.argv", ["streamlink"])
    def test_print_usage(self, mock_stdout):
        self.subject()
        assert self.get_stdout(mock_stdout) == dedent("""
            usage: streamlink [OPTIONS] <URL> [STREAM]

            Use -h/--help to see the available options or read the manual at https://streamlink.github.io
        """).lstrip()

    @patch("sys.stdout")
    @patch("sys.argv", ["streamlink", "--help"])
    def test_print_help(self, mock_stdout):
        self.subject()
        output = self.get_stdout(mock_stdout)
        assert "usage: streamlink [OPTIONS] <URL> [STREAM]" in output
        assert dedent("""
            Streamlink is a command-line utility that extracts streams from various
            services and pipes them into a video player of choice.
        """) in output
        assert dedent("""
            For more in-depth documentation see:
              https://streamlink.github.io

            Please report broken plugins or bugs to the issue tracker on Github:
              https://github.com/streamlink/streamlink/issues
        """) in output

    @patch("sys.stdout")
    @patch("sys.argv", ["streamlink", "--plugins"])
    def test_print_plugins(self, mock_stdout):
        self.subject()
        assert self.get_stdout(mock_stdout) == "Available plugins: testplugin\n"

    @patch("sys.stdout")
    @patch("sys.argv", ["streamlink", "--plugins", "--json"])
    def test_print_plugins_json(self, mock_stdout):
        self.subject()
        assert self.get_stdout(mock_stdout) == '[\n  "testplugin"\n]\n'
