"""Tests for qpip.project_cli and env subcommands."""

from __future__ import annotations

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

from qpip.project_cli import (
    ProjectCLIUsageError,
    run_config_command,
    run_doctor_command,
    run_exec_command,
    run_info_command,
    run_mirror_command,
)
from qpip.project_venv import run_env_command


class TTYBuffer(io.StringIO):
    def isatty(self) -> bool:
        return True


class RunEnvCommandTests(unittest.TestCase):
    def test_env_create_uses_current_directory_without_project_files(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            buffer = io.StringIO()

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

                exit_code = run_env_command(
                    ("env", "create"),
                    cwd=project_dir,
                    output=buffer,
                    python_executable="python",
                )

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

    def test_env_current_reports_status(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            buffer = io.StringIO()

            exit_code = run_env_command(("env", "current"), cwd=project_dir, output=buffer)

            self.assertEqual(exit_code, 0)
            self.assertIn("project:", buffer.getvalue())
            self.assertIn("status: inactive", buffer.getvalue())

    def test_env_current_supports_json_output(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            buffer = io.StringIO()

            exit_code = run_env_command(
                ("env", "current"),
                cwd=project_dir,
                output=buffer,
                json_output=True,
            )

            self.assertEqual(exit_code, 0)
            payload = json.loads(buffer.getvalue())
            self.assertEqual(payload["status"], "inactive")
            self.assertEqual(payload["project"], str(project_dir.resolve()))


class RunExecCommandTests(unittest.TestCase):
    def test_exec_requires_existing_environment(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            with self.assertRaises(ProjectCLIUsageError):
                run_exec_command(("exec", "python", "-V"), cwd=Path(tmp_dir), output=io.StringIO())

    def test_exec_runs_inside_project_directory(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            (project_dir / ".venv" / "bin").mkdir(parents=True)
            buffer = io.StringIO()

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

                exit_code = run_exec_command(
                    ("exec", "python", "-V"),
                    cwd=project_dir,
                    output=buffer,
                    env={"PATH": "/usr/bin"},
                )

            self.assertEqual(exit_code, 0)
            call = mocked.call_args
            self.assertEqual(call.args[0], ["python", "-V"])
            self.assertEqual(call.kwargs["cwd"], str(project_dir.resolve()))
            self.assertIn(".venv/bin", call.kwargs["env"]["PATH"])


class RunInfoCommandTests(unittest.TestCase):
    def test_info_handles_missing_pyproject(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            buffer = io.StringIO()

            exit_code = run_info_command(("info",), cwd=Path(tmp_dir), output=buffer)

            self.assertEqual(exit_code, 0)
            self.assertIn("Project:", buffer.getvalue())
            self.assertIn("pyproject: missing", buffer.getvalue())
            self.assertIn("exists: no", buffer.getvalue())

    def test_info_handles_invalid_scripts_config_without_crashing(self) -> None:
        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.scripts]\nbad = 123\n",
                encoding="utf-8",
            )
            buffer = io.StringIO()

            exit_code = run_info_command(("info",), cwd=project_dir, output=buffer, env={})

            self.assertEqual(exit_code, 0)
            self.assertIn("available: none", buffer.getvalue())
            self.assertIn("warning:", buffer.getvalue())
            self.assertIn("Scripts:", buffer.getvalue())

    def test_info_supports_json_output(self) -> None:
        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\"\nvenv-dir = \".custom-venv\"\n",
                encoding="utf-8",
            )
            buffer = io.StringIO()

            exit_code = run_info_command(("info",), cwd=project_dir, output=buffer, json_output=True)

            self.assertEqual(exit_code, 0)
            payload = json.loads(buffer.getvalue())
            self.assertEqual(payload["project"], str(project_dir.resolve()))
            self.assertIn("mirror", payload)
            self.assertEqual(payload["mirror"]["source"], "project")
            self.assertEqual(payload["config"]["venv_dirname"], ".custom-venv")

    def test_info_uses_tty_friendly_output_when_supported(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            buffer = TTYBuffer()

            exit_code = run_info_command(("info",), cwd=project_dir, output=buffer, env={})

            self.assertEqual(exit_code, 0)
            self.assertIn("\x1b[", buffer.getvalue())
            self.assertTrue("●" in buffer.getvalue() or "○" in buffer.getvalue())


class RunDoctorCommandTests(unittest.TestCase):
    def test_doctor_reports_missing_project_markers(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            buffer = io.StringIO()

            exit_code = run_doctor_command(("doctor",), cwd=Path(tmp_dir), output=buffer)

            self.assertEqual(exit_code, 1)
            self.assertIn("FAIL project_root", buffer.getvalue())
            self.assertIn("Summary:", buffer.getvalue())

    def test_doctor_does_not_fail_only_because_env_is_not_activated(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            (project_dir / ".git").mkdir()
            (project_dir / ".venv" / "bin").mkdir(parents=True)
            (project_dir / ".venv" / "bin" / "python").write_text("", encoding="utf-8")
            (project_dir / "pyproject.toml").write_text(
                "[project]\nname = \"demo\"\nversion = \"0.1.0\"\n\n[tool.qpip.scripts]\ntest = \"python -m unittest\"\n",
                encoding="utf-8",
            )
            buffer = io.StringIO()

            exit_code = run_doctor_command(("doctor",), cwd=project_dir, output=buffer, env={})

            self.assertEqual(exit_code, 0)
            self.assertIn("INFO activated", buffer.getvalue())
            self.assertIn("Summary:", buffer.getvalue())

    def test_doctor_supports_json_output(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            (project_dir / ".git").mkdir()
            (project_dir / ".custom-venv" / "bin").mkdir(parents=True)
            (project_dir / ".custom-venv" / "bin" / "python").write_text("", encoding="utf-8")
            (project_dir / "pyproject.toml").write_text(
                "[project]\nname = \"demo\"\nversion = \"0.1.0\"\n\n[tool.qpip]\nmirror = \"tsinghua\"\nvenv-dir = \".custom-venv\"\n\n[tool.qpip.scripts]\ntest = \"python -m unittest\"\n",
                encoding="utf-8",
            )
            buffer = io.StringIO()

            exit_code = run_doctor_command(("doctor",), cwd=project_dir, output=buffer, env={}, json_output=True)

            self.assertEqual(exit_code, 0)
            payload = json.loads(buffer.getvalue())
            self.assertTrue(payload["ok"])
            self.assertTrue(any(check["name"] == "activated" for check in payload["checks"]))
            self.assertTrue(any("source=project" in str(check["message"]) for check in payload["checks"] if check["name"] == "mirror"))

    def test_doctor_uses_tty_friendly_status_labels_when_supported(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            (project_dir / ".git").mkdir()
            (project_dir / ".venv" / "bin").mkdir(parents=True)
            (project_dir / ".venv" / "bin" / "python").write_text("", encoding="utf-8")
            (project_dir / "pyproject.toml").write_text(
                "[project]\nname = \"demo\"\nversion = \"0.1.0\"\n\n[tool.qpip.scripts]\ntest = \"python -m unittest\"\n",
                encoding="utf-8",
            )
            buffer = TTYBuffer()

            exit_code = run_doctor_command(("doctor",), cwd=project_dir, output=buffer, env={})

            self.assertEqual(exit_code, 0)
            self.assertIn("\x1b[", buffer.getvalue())
            self.assertIn("Summary:", buffer.getvalue())


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

        exit_code = run_mirror_command(("mirror", "current"), cwd=Path.cwd(), output=buffer, env={"QPIP_DEFAULT_MIRROR": "pypi"})

        self.assertEqual(exit_code, 0)
        self.assertIn("pypi https://pypi.org/simple/", buffer.getvalue())

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

        exit_code = run_mirror_command(
            ("mirror", "current"),
            cwd=Path.cwd(),
            output=buffer,
            env={"QPIP_DEFAULT_MIRROR": "pypi"},
            json_output=True,
        )

        self.assertEqual(exit_code, 0)
        payload = json.loads(buffer.getvalue())
        self.assertEqual(payload["name"], "pypi")
        self.assertEqual(payload["source"], "env")

    def test_mirror_reports_close_match_for_unknown_subcommand(self) -> None:
        with self.assertRaises(ProjectCLIUsageError) as exc_info:
            run_mirror_command(("mirror", "currnt"), cwd=Path.cwd())

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

    def test_mirror_use_updates_project_config(self) -> None:
        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",
                encoding="utf-8",
            )
            buffer = io.StringIO()

            exit_code = run_mirror_command(("mirror", "use", "tsinghua"), cwd=project_dir, output=buffer)

            self.assertEqual(exit_code, 0)
            self.assertIn("已将项目默认镜像设置为 tsinghua", buffer.getvalue())
            self.assertIn('[tool.qpip]\nmirror = "tsinghua"\n', (project_dir / "pyproject.toml").read_text(encoding="utf-8"))

    def test_mirror_use_rejects_invalid_mirror_without_writing_config(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            pyproject_path = project_dir / "pyproject.toml"
            pyproject_path.write_text(
                "[project]\nname = \"demo\"\nversion = \"0.1.0\"\n",
                encoding="utf-8",
            )

            with self.assertRaises(ProjectCLIUsageError):
                run_mirror_command(("mirror", "use", "bad-mirror"), cwd=project_dir, output=io.StringIO())

            self.assertNotIn("[tool.qpip]", pyproject_path.read_text(encoding="utf-8"))

    def test_mirror_use_preserves_unknown_keys_and_comments(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            pyproject_path = project_dir / "pyproject.toml"
            pyproject_path.write_text(
                "[project]\nname = \"demo\"\nversion = \"0.1.0\"\n\n[tool.qpip]\n# keep me\nvenv-dir = \".venv\"\ncustom = \"x\"\n",
                encoding="utf-8",
            )

            exit_code = run_mirror_command(("mirror", "use", "tsinghua"), cwd=project_dir, output=io.StringIO())

            self.assertEqual(exit_code, 0)
            updated = pyproject_path.read_text(encoding="utf-8")
            self.assertIn("# keep me", updated)
            self.assertIn('custom = "x"', updated)
            self.assertIn('venv-dir = ".venv"', updated)
            self.assertIn('mirror = "tsinghua"', updated)

    def test_mirror_use_appends_config_without_extra_blank_lines(self) -> None:
        with tempfile.TemporaryDirectory() as tmp_dir:
            project_dir = Path(tmp_dir)
            pyproject_path = project_dir / "pyproject.toml"
            pyproject_path.write_text(
                "[project]\nname = \"demo\"\nversion = \"0.1.0\"\n",
                encoding="utf-8",
            )

            exit_code = run_mirror_command(("mirror", "use", "tsinghua"), cwd=project_dir, output=io.StringIO())

            self.assertEqual(exit_code, 0)
            self.assertNotIn("\n\n\n[tool.qpip]", pyproject_path.read_text(encoding="utf-8"))


class RunConfigCommandTests(unittest.TestCase):
    def test_config_list_reads_project_config(self) -> None:
        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\"\nvenv-dir = \".custom-venv\"\n",
                encoding="utf-8",
            )
            buffer = io.StringIO()

            exit_code = run_config_command(("config", "list"), cwd=project_dir, output=buffer)

            self.assertEqual(exit_code, 0)
            self.assertIn("mirror = tsinghua", buffer.getvalue())
            self.assertIn("venv-dir = .custom-venv", buffer.getvalue())

    def test_config_get_supports_json_output(self) -> None:
        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",
            )
            buffer = io.StringIO()

            exit_code = run_config_command(("config", "get", "mirror"), cwd=project_dir, output=buffer, json_output=True)

            self.assertEqual(exit_code, 0)
            payload = json.loads(buffer.getvalue())
            self.assertEqual(payload["key"], "mirror")
            self.assertEqual(payload["value"], "tsinghua")

    def test_config_list_reports_parse_error_cleanly(self) -> None:
        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 = [1,2]\n",
                encoding="utf-8",
            )
            buffer = io.StringIO()

            exit_code = run_config_command(("config", "list"), cwd=project_dir, output=buffer)

            self.assertEqual(exit_code, 1)
            self.assertIn("配置解析失败:", buffer.getvalue())


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