"""Tests for qpip.project_venv."""

from __future__ import annotations

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

from qpip.project_venv import (
    SHELL_LINUX,
    SHELL_MACOS,
    SHELL_WINDOWS_BASH,
    SHELL_WINDOWS_CMD,
    SHELL_WINDOWS_POWERSHELL,
    VenvUsageError,
    build_venv_command,
    detect_shell,
    run_active_command,
    run_activate_command,
    run_deactivate_command,
    run_venv_command,
)


class DetectShellTests(unittest.TestCase):
    def test_detects_linux_sessions(self) -> None:
        shell_kind = detect_shell(platform_system="Linux", os_name="posix")

        self.assertEqual(shell_kind, SHELL_LINUX)

    def test_detects_macos_sessions(self) -> None:
        shell_kind = detect_shell(platform_system="Darwin", os_name="posix")

        self.assertEqual(shell_kind, SHELL_MACOS)

    def test_detects_windows_powershell_from_parent_process(self) -> None:
        shell_kind = detect_shell(os_name="nt", parent_process_name="powershell.exe")

        self.assertEqual(shell_kind, SHELL_WINDOWS_POWERSHELL)

    def test_detects_windows_bash_from_parent_process(self) -> None:
        shell_kind = detect_shell(os_name="nt", parent_process_name="bash.exe")

        self.assertEqual(shell_kind, SHELL_WINDOWS_BASH)

    def test_falls_back_to_windows_cmd(self) -> None:
        shell_kind = detect_shell(env={}, os_name="nt", parent_process_name="")

        self.assertEqual(shell_kind, SHELL_WINDOWS_CMD)

    def test_invalid_shell_override_raises(self) -> None:
        with self.assertRaises(VenvUsageError):
            detect_shell(env={"QPIP_SHELL": "bad-shell"}, os_name="nt")


class RunVenvCommandTests(unittest.TestCase):
    def test_build_venv_command_uses_default_directory(self) -> None:
        command = build_venv_command(python_executable="python")

        self.assertEqual(command, ["python", "-m", "venv", ".venv"])

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

        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)

            with patch("qpip.project_venv.subprocess.run") as mocked:
                mocked.return_value.returncode = 0

                exit_code = run_venv_command(
                    ["venv"],
                    cwd=project_dir,
                    output=buffer,
                    python_executable="python",
                )

            self.assertEqual(exit_code, 0)
            self.assertIn("python -m venv .venv", buffer.getvalue())
            mocked.assert_called_once_with(
                ["python", "-m", "venv", ".venv"],
                cwd=str(project_dir),
                check=False,
            )
            self.assertEqual(
                (project_dir / ".gitignore").read_text(encoding="utf-8"),
                ".venv/\n",
            )

    def test_run_venv_command_appends_gitignore_entry_when_missing(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            gitignore_path = project_dir / ".gitignore"
            gitignore_path.write_text("dist/\n", encoding="utf-8")

            with patch("qpip.project_venv.subprocess.run") as mocked:
                mocked.return_value.returncode = 0

                exit_code = run_venv_command(["venv"], cwd=project_dir, output=io.StringIO())

            self.assertEqual(exit_code, 0)
            self.assertEqual(
                gitignore_path.read_text(encoding="utf-8"),
                "dist/\n.venv/\n",
            )

    def test_run_venv_command_does_not_duplicate_gitignore_entry(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            gitignore_path = project_dir / ".gitignore"
            gitignore_path.write_text("dist/\n.venv\n", encoding="utf-8")

            with patch("qpip.project_venv.subprocess.run") as mocked:
                mocked.return_value.returncode = 0

                exit_code = run_venv_command(["venv"], cwd=project_dir, output=io.StringIO())

            self.assertEqual(exit_code, 0)
            self.assertEqual(gitignore_path.read_text(encoding="utf-8"), "dist/\n.venv\n")

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

        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)

            exit_code = run_venv_command(
                ["venv"],
                cwd=project_dir,
                output=buffer,
                dry_run=True,
                python_executable="python",
            )

            self.assertFalse((project_dir / ".gitignore").exists())

        self.assertEqual(exit_code, 0)
        self.assertIn("python -m venv .venv", buffer.getvalue())

    def test_run_venv_command_respects_project_configured_venv_dir(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]\nvenv-dir = \".custom-venv\"\n",
                encoding="utf-8",
            )

            with patch("qpip.project_venv.subprocess.run") as mocked:
                mocked.return_value.returncode = 0

                exit_code = run_venv_command(
                    ["env", "create"],
                    cwd=project_dir,
                    output=buffer,
                    python_executable="python",
                )

            self.assertEqual(exit_code, 0)
            self.assertIn("python -m venv .custom-venv", buffer.getvalue())
            mocked.assert_called_once_with(
                ["python", "-m", "venv", ".custom-venv"],
                cwd=str(project_dir),
                check=False,
            )


class RunActivateCommandTests(unittest.TestCase):
    def test_activate_outputs_linux_command(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            activate_script = project_dir / ".venv" / "bin" / "activate"
            activate_script.parent.mkdir(parents=True)
            activate_script.write_text("", encoding="utf-8")
            buffer = io.StringIO()

            exit_code = run_activate_command(
                ["activate"],
                cwd=project_dir,
                output=buffer,
                platform_system="Linux",
                os_name="posix",
            )

            self.assertEqual(exit_code, 0)
            self.assertEqual(buffer.getvalue().strip(), "source .venv/bin/activate")

    def test_activate_outputs_windows_powershell_command(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            activate_script = project_dir / ".venv" / "Scripts" / "Activate.ps1"
            activate_script.parent.mkdir(parents=True)
            activate_script.write_text("", encoding="utf-8")
            buffer = io.StringIO()

            exit_code = run_activate_command(
                ["activate"],
                cwd=project_dir,
                output=buffer,
                os_name="nt",
                parent_process_name="pwsh.exe",
            )

            self.assertEqual(exit_code, 0)
            self.assertEqual(buffer.getvalue().strip(), "& .\\.venv\\Scripts\\Activate.ps1")

    def test_activate_outputs_windows_bash_command(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            activate_script = project_dir / ".venv" / "Scripts" / "activate"
            activate_script.parent.mkdir(parents=True)
            activate_script.write_text("", encoding="utf-8")
            buffer = io.StringIO()

            exit_code = run_activate_command(
                ["activate"],
                cwd=project_dir,
                output=buffer,
                os_name="nt",
                parent_process_name="bash.exe",
            )

            self.assertEqual(exit_code, 0)
            self.assertEqual(buffer.getvalue().strip(), "source .venv/Scripts/activate")

    def test_activate_requires_existing_virtual_environment(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            with self.assertRaises(VenvUsageError):
                run_activate_command(
                    ["activate"],
                    cwd=Path(tmp_dir),
                    output=io.StringIO(),
                    platform_system="Linux",
                    os_name="posix",
                )


class RunActiveCommandTests(unittest.TestCase):
    def test_activated_succeeds_when_project_venv_is_active(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            venv_path = project_dir / ".venv"
            buffer = io.StringIO()

            exit_code = run_active_command(
                ["activated"],
                cwd=project_dir,
                output=buffer,
                env={"VIRTUAL_ENV": str(venv_path)},
            )

            self.assertEqual(exit_code, 0)
            self.assertEqual(
                buffer.getvalue().strip(),
                f"当前项目虚拟环境已激活: {venv_path}",
            )

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

        exit_code = run_active_command(
            ["activated"],
            cwd=Path("D:/tmp/demo"),
            output=buffer,
            env={},
        )

        self.assertEqual(exit_code, 1)
        self.assertEqual(buffer.getvalue().strip(), "当前未激活任何虚拟环境")

    def test_activated_fails_when_other_virtual_environment_is_active(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            other_venv_path = project_dir.parent / "other-venv"
            buffer = io.StringIO()

            exit_code = run_active_command(
                ["activated"],
                cwd=project_dir,
                output=buffer,
                env={"VIRTUAL_ENV": str(other_venv_path)},
            )

            self.assertEqual(exit_code, 1)
            self.assertEqual(
                buffer.getvalue().strip(),
                f"当前激活的是其他虚拟环境: {other_venv_path}",
            )


class RunDeactivateCommandTests(unittest.TestCase):
    def test_deactivate_outputs_posix_command(self) -> None:
        buffer = io.StringIO()

        exit_code = run_deactivate_command(
            ["deactivate"],
            cwd=Path("D:/tmp/demo"),
            output=buffer,
            platform_system="Linux",
            os_name="posix",
        )

        self.assertEqual(exit_code, 0)
        self.assertEqual(
            buffer.getvalue().strip(),
            "if command -v deactivate >/dev/null 2>&1; then deactivate; fi",
        )

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

        exit_code = run_deactivate_command(
            ["deactivate"],
            cwd=Path("D:/tmp/demo"),
            output=buffer,
            os_name="nt",
            parent_process_name="powershell.exe",
        )

        self.assertEqual(exit_code, 0)
        self.assertEqual(
            buffer.getvalue().strip(),
            "if (Get-Command deactivate -ErrorAction SilentlyContinue) { deactivate }",
        )


class RunEnvCommandSuggestionTests(unittest.TestCase):
    def test_unknown_env_subcommand_suggests_close_match(self) -> None:
        with self.assertRaises(VenvUsageError) as exc_info:
            from qpip.project_venv import run_env_command

            run_env_command(("env", "curent"), cwd=Path("D:/tmp/demo"), output=io.StringIO())

        self.assertIn("你是不是想用: current", str(exc_info.exception))


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