# SPDX-License-Identifier: AGPL-3.0-or-later
# SPDX-FileCopyrightText: Copyright 2023 David Seaward and contributors

import io
from pathlib import Path
from unittest.mock import patch

import pytest

from tardyon import cli


def test_print_metadata():
    with (
        patch("tardyon.cli.metadata.version", return_value="5.0.0"),
        patch("sys.stdout", new_callable=io.StringIO) as mock_stdout,
    ):
        cli.print_metadata()
        output = mock_stdout.getvalue()
        assert "tardyon v5.0.0\n" in output
        assert 'Call "tardyon VERB NOUN"\n' in output


def test_print_metadata_unknown():
    from importlib import metadata

    with (
        patch(
            "tardyon.cli.metadata.version", side_effect=metadata.PackageNotFoundError
        ),
        patch("sys.stdout", new_callable=io.StringIO) as mock_stdout,
    ):
        cli.print_metadata()
        output = mock_stdout.getvalue()
        assert "tardyon vunknown\n" in output
        assert 'Call "tardyon VERB NOUN"\n' in output


def test_show_tardyon_folder_linux():
    mock_home = Path("/tmp/mock_home")
    target = mock_home / ".local" / "bin" / "tardy"
    with (
        patch("pathlib.Path.home", return_value=mock_home),
        patch("pathlib.Path.mkdir") as mock_mkdir,
        patch("sys.platform", "linux"),
        patch("subprocess.run") as mock_run,
    ):
        cli.show_tardyon_folder()
        mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
        mock_run.assert_called_once_with(["xdg-open", str(target)], check=True)


def test_show_tardyon_folder_windows():
    mock_home = Path("/tmp/mock_home")
    target = mock_home / ".local" / "bin" / "tardy"
    with (
        patch("pathlib.Path.home", return_value=mock_home),
        patch("pathlib.Path.mkdir") as mock_mkdir,
        patch("sys.platform", "win32"),
        patch("os.startfile", create=True) as mock_startfile,
    ):
        cli.show_tardyon_folder()
        mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
        mock_startfile.assert_called_once_with(target)


def test_show_tardyon_folder_macos():
    mock_home = Path("/tmp/mock_home")
    target = mock_home / ".local" / "bin" / "tardy"
    with (
        patch("pathlib.Path.home", return_value=mock_home),
        patch("pathlib.Path.mkdir") as mock_mkdir,
        patch("sys.platform", "darwin"),
        patch("subprocess.run") as mock_run,
    ):
        cli.show_tardyon_folder()
        mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
        mock_run.assert_called_once_with(["open", str(target)], check=True)


def test_show_tardyon_folder_fallback():
    mock_home = Path("/tmp/mock_home")
    target = mock_home / ".local" / "bin" / "tardy"
    with (
        patch("pathlib.Path.home", return_value=mock_home),
        patch("pathlib.Path.mkdir") as mock_mkdir,
        patch("sys.platform", "linux"),
        patch("subprocess.run", side_effect=FileNotFoundError),
        patch("sys.stdout", new_callable=io.StringIO) as mock_stdout,
    ):
        cli.show_tardyon_folder()
        mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
        assert str(target) in mock_stdout.getvalue()


def test_launch_tardyon_script_happy_path():
    mock_home = Path("/tmp/mock_home")
    expected_path = str(mock_home / ".local" / "bin" / "tardy" / "bar" / "foo.bar")
    with (
        patch("pathlib.Path.home", return_value=mock_home),
        patch("pathlib.Path.exists", return_value=True),
        patch("os.access", return_value=True),
        patch("os.execv", side_effect=SystemExit(0)) as mock_execv,
    ):
        with pytest.raises(SystemExit):
            cli.launch_tardyon_script("foo", "bar", ["extra"])
        mock_execv.assert_called_once_with(expected_path, [expected_path, "extra"])


def test_launch_tardyon_script_failure():
    mock_home = Path("/tmp/mock_home")
    with (
        patch("pathlib.Path.home", return_value=mock_home),
        patch("pathlib.Path.exists", return_value=True),
        patch("os.access", return_value=True),
        patch("os.execv", side_effect=OSError("denied")),
    ):
        with pytest.raises(OSError, match="denied"):
            cli.launch_tardyon_script("foo", "bar", [])


def test_launch_tardyon_script_not_found():
    mock_home = Path("/tmp/mock_home")
    with (
        patch("pathlib.Path.home", return_value=mock_home),
        patch("pathlib.Path.exists", return_value=False),
    ):
        with pytest.raises(FileNotFoundError, match="does not exist"):
            cli.launch_tardyon_script("foo", "bar", [])


def test_launch_tardyon_script_not_executable():
    mock_home = Path("/tmp/mock_home")
    with (
        patch("pathlib.Path.home", return_value=mock_home),
        patch("pathlib.Path.exists", return_value=True),
        patch("os.access", return_value=False),
    ):
        with pytest.raises(PermissionError, match="is not executable"):
            cli.launch_tardyon_script("foo", "bar", [])


def test_invoke_zero_arguments():
    with (
        patch("sys.argv", ["tardyon"]),
        patch("tardyon.cli.print_metadata") as mock_print,
        patch("sys.exit") as mock_exit,
    ):
        cli.invoke()
        mock_print.assert_called_once()
        mock_exit.assert_called_once_with(0)


def test_invoke_one_argument():
    with (
        patch("sys.argv", ["tardyon", "foo"]),
        patch("tardyon.cli.print_metadata") as mock_print,
        patch("sys.exit") as mock_exit,
    ):
        cli.invoke()
        mock_print.assert_called_once()
        mock_exit.assert_called_once_with(1)


def test_invoke_help_argument():
    with (
        patch("sys.argv", ["tardyon", "--help"]),
        patch("tardyon.cli.print_metadata") as mock_print,
        patch("sys.exit") as mock_exit,
    ):
        cli.invoke()
        mock_print.assert_called_once()
        mock_exit.assert_called_once_with(0)


def test_invoke_version_argument():
    with (
        patch("sys.argv", ["tardyon", "--version"]),
        patch("tardyon.cli.print_metadata") as mock_print,
        patch("sys.exit") as mock_exit,
    ):
        cli.invoke()
        mock_print.assert_called_once()
        mock_exit.assert_called_once_with(0)


def test_invoke_show_yourself():
    with (
        patch("sys.argv", ["tardyon", "show", "yourself"]),
        patch("tardyon.cli.show_tardyon_folder") as mock_show,
        patch("sys.exit") as mock_exit,
    ):
        cli.invoke()
        mock_show.assert_called_once()
        mock_exit.assert_called_once_with(0)


def test_invoke_launch_script():
    with (
        patch("sys.argv", ["tardyon", "foo", "bar", "extra"]),
        patch("tardyon.cli.launch_tardyon_script") as mock_launch,
    ):
        cli.invoke()
        mock_launch.assert_called_once_with("foo", "bar", ["extra"])


def test_invoke_launch_script_error():
    with (
        patch("sys.argv", ["tardyon", "foo", "bar"]),
        patch(
            "tardyon.cli.launch_tardyon_script",
            side_effect=FileNotFoundError("oops"),
        ),
        patch("sys.stderr", new_callable=io.StringIO) as mock_stderr,
        patch("sys.exit") as mock_exit,
    ):
        cli.invoke()
        assert "Script could not be invoked: oops" in mock_stderr.getvalue()
        mock_exit.assert_called_once_with(1)


def test_invoke_show_other():
    # If it's not "show yourself", it should try to launch a script
    # e.g. "tardyon show something" -> verb="show", noun="something"
    with (
        patch("sys.argv", ["tardyon", "show", "something"]),
        patch("tardyon.cli.launch_tardyon_script") as mock_launch,
    ):
        cli.invoke()
        mock_launch.assert_called_once_with("show", "something", [])


def test_invoke_multiple_args():
    with (
        patch("sys.argv", ["tardyon", "foo", "bar", "extra1", "extra2"]),
        patch("tardyon.cli.launch_tardyon_script") as mock_launch,
    ):
        cli.invoke()
        mock_launch.assert_called_once_with("foo", "bar", ["extra1", "extra2"])
