"""Tests for qpip.cli."""

from __future__ import annotations

import io
import unittest
from contextlib import redirect_stdout
from pathlib import Path
import tempfile
from unittest.mock import patch

from qpip.cli import CLIUsageError, HELP_TEXT, main, parse_cli_args


class ParseCliArgsTests(unittest.TestCase):
    def test_supports_positional_mirror(self) -> None:
        options, warning = parse_cli_args(["tsinghua", "install", "numpy"])

        self.assertIsNone(warning)
        self.assertEqual(options.mirror, "tsinghua")
        self.assertEqual(options.pip_args, ("install", "numpy"))

    def test_supports_named_mirror(self) -> None:
        options, _ = parse_cli_args(["--mirror", "douban", "install", "pandas"])

        self.assertEqual(options.mirror, "douban")
        self.assertEqual(options.pip_args, ("install", "pandas"))

    def test_supports_pip_alias_as_positional_mirror(self) -> None:
        options, warning = parse_cli_args(["pip", "install", "numpy"])

        self.assertIsNone(warning)
        self.assertEqual(options.mirror, "pypi")
        self.assertEqual(options.pip_args, ("install", "numpy"))

    def test_supports_positional_mirror_after_qpip_flags(self) -> None:
        options, warning = parse_cli_args(["--dry-run", "pip", "install", "numpy"])

        self.assertIsNone(warning)
        self.assertTrue(options.dry_run)
        self.assertEqual(options.mirror, "pypi")
        self.assertEqual(options.pip_args, ("install", "numpy"))

    def test_respects_separator_for_pip_options(self) -> None:
        options, _ = parse_cli_args(["--", "--version"])

        self.assertEqual(options.pip_args, ("--version",))

    def test_detects_conflicting_mirrors(self) -> None:
        with self.assertRaises(CLIUsageError):
            parse_cli_args(["ali", "--mirror", "douban", "install", "requests"])

    def test_recognizes_qpip_flags_before_pip_args(self) -> None:
        options, _ = parse_cli_args(["--dry-run", "--list-mirrors"])

        self.assertTrue(options.dry_run)
        self.assertTrue(options.list_mirrors)
        self.assertEqual(options.pip_args, ())

    def test_help_text_covers_current_commands(self) -> None:
        self.assertIn("使用更快的软件包镜像运行 pip", HELP_TEXT)
        self.assertIn("项目:", HELP_TEXT)
        self.assertIn("脚本与执行:", HELP_TEXT)
        self.assertIn("环境:", HELP_TEXT)
        self.assertIn("镜像:", HELP_TEXT)
        self.assertIn("说明:", HELP_TEXT)
        self.assertIn("qpip pip install numpy", HELP_TEXT)
        self.assertIn("qpip add requests", HELP_TEXT)
        self.assertIn("qpip remove requests", HELP_TEXT)
        self.assertIn("qpip exec python -V", HELP_TEXT)
        self.assertIn("qpip info", HELP_TEXT)
        self.assertIn("qpip doctor", HELP_TEXT)
        self.assertIn("qpip update requests", HELP_TEXT)
        self.assertIn("qpip uninstall requests", HELP_TEXT)
        self.assertIn("qpip init", HELP_TEXT)
        self.assertIn("qpip run test", HELP_TEXT)
        self.assertIn("qpip env create", HELP_TEXT)
        self.assertIn("qpip env use", HELP_TEXT)
        self.assertIn("qpip env activated", HELP_TEXT)
        self.assertIn("qpip env deactivate", HELP_TEXT)
        self.assertIn("qpip mirror current", HELP_TEXT)
        self.assertIn("qpip mirror use tsinghua", HELP_TEXT)
        self.assertIn("qpip config get mirror", HELP_TEXT)
        self.assertNotIn("qpip --dry-run install fastapi", HELP_TEXT)

    def test_dry_run_supports_install_alias(self) -> None:
        buffer = io.StringIO()

        with redirect_stdout(buffer):
            exit_code = main(["--dry-run", "i", "requests"])

        self.assertEqual(exit_code, 0)
        self.assertIn("-m pip install requests", buffer.getvalue())

    def test_dry_run_supports_update_alias(self) -> None:
        buffer = io.StringIO()

        with redirect_stdout(buffer):
            exit_code = main(["--dry-run", "up", "requests"])

        self.assertEqual(exit_code, 0)
        self.assertIn("-m pip install --upgrade requests", buffer.getvalue())

    def test_dry_run_supports_uninstall_alias(self) -> None:
        buffer = io.StringIO()

        with redirect_stdout(buffer):
            exit_code = main(["--dry-run", "un", "requests"])

        self.assertEqual(exit_code, 0)
        self.assertIn("-m pip uninstall requests", buffer.getvalue())

    def test_dry_run_supports_official_pypi_mirror_alias(self) -> None:
        buffer = io.StringIO()

        with redirect_stdout(buffer):
            exit_code = main(["--dry-run", "pip", "install", "numpy"])

        self.assertEqual(exit_code, 0)
        self.assertIn("-m pip install numpy", buffer.getvalue())
        self.assertIn("https://pypi.org/simple/", buffer.getvalue())

    def test_dry_run_uses_project_configured_mirror_when_not_explicitly_set(self) -> None:
        buffer = io.StringIO()

        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            (project_dir / "pyproject.toml").write_text(
                "[project]\nname = \"demo\"\nversion = \"0.1.0\"\n\n[tool.qpip]\nmirror = \"tsinghua\"\n",
                encoding="utf-8",
            )
            with patch("qpip.cli.Path.cwd", return_value=project_dir), redirect_stdout(buffer):
                exit_code = main(["--dry-run", "install", "numpy"])

        self.assertEqual(exit_code, 0)
        self.assertIn("https://pypi.tuna.tsinghua.edu.cn/simple/", buffer.getvalue())

    def test_main_dispatches_init_command(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch(
            "qpip.cli.run_init_command",
            return_value=0,
        ) as mocked:
            exit_code = main(["init", "-y"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("init", "-y"), cwd=demo_path, dry_run=False)

    def test_main_dispatches_run_command(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch(
            "qpip.cli.run_project_command",
            return_value=0,
        ) as mocked:
            exit_code = main(["--dry-run", "run", "test"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("run", "test"), cwd=demo_path, dry_run=True)

    def test_main_dispatches_exec_command(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch(
            "qpip.cli.run_exec_command",
            return_value=0,
        ) as mocked:
            exit_code = main(["exec", "python", "-V"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("exec", "python", "-V"), cwd=demo_path, dry_run=False)

    def test_main_dispatches_env_command(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch(
            "qpip.cli.run_env_command",
            return_value=0,
        ) as mocked:
            exit_code = main(["env", "use"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("env", "use"), cwd=demo_path, dry_run=False, json_output=False)

    def test_main_dispatches_info_command(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch(
            "qpip.cli.run_info_command",
            return_value=0,
        ) as mocked:
            exit_code = main(["info"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("info",), cwd=demo_path, json_output=False)

    def test_main_dispatches_info_command_with_json(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch(
            "qpip.cli.run_info_command",
            return_value=0,
        ) as mocked:
            exit_code = main(["--json", "info"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("info",), cwd=demo_path, json_output=True)

    def test_main_dispatches_doctor_command(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch(
            "qpip.cli.run_doctor_command",
            return_value=1,
        ) as mocked:
            exit_code = main(["doctor"])

        self.assertEqual(exit_code, 1)
        mocked.assert_called_once_with(("doctor",), cwd=demo_path, json_output=False)

    def test_main_dispatches_mirror_command(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch("qpip.cli.run_mirror_command", return_value=0) as mocked:
            exit_code = main(["mirror", "current"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("mirror", "current"), cwd=demo_path, json_output=False)

    def test_main_dispatches_mirror_command_with_json(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch("qpip.cli.run_mirror_command", return_value=0) as mocked:
            exit_code = main(["--json", "mirror", "current"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("mirror", "current"), cwd=demo_path, json_output=True)

    def test_main_dispatches_config_command(self) -> None:
        demo_path = Path("D:/tmp/demo")

        with patch("qpip.cli.Path.cwd", return_value=demo_path), patch(
            "qpip.cli.run_config_command",
            return_value=0,
        ) as mocked:
            exit_code = main(["config", "list"])

        self.assertEqual(exit_code, 0)
        mocked.assert_called_once_with(("config", "list"), cwd=demo_path, json_output=False)

    def test_main_rejects_legacy_environment_commands(self) -> None:
        self.assertEqual(main(["venv"]), 2)
        self.assertEqual(main(["activate"]), 2)
        self.assertEqual(main(["activated"]), 2)
        self.assertEqual(main(["deactivate"]), 2)


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