"""Tests for vr --update flag."""

import json
import subprocess

from pathlib import Path
from unittest.mock import patch, Mock

from typer.testing import CliRunner

from vr_cli.cli import app
from vr_cli.commands.update import (
    get_current_version,
    upgrade_package,
    run_update,
    PACKAGE_NAME,
)
from vr_cli.utils.config import get_setup_targets, save_setup_targets


runner = CliRunner()


class TestGetCurrentVersion:
    """Tests for get_current_version function."""

    def test_returns_version_string(self):
        """Should return the installed package version."""
        with patch("vr_cli.commands.update.version") as mock_version:
            mock_version.return_value = "1.1.0"

            result = get_current_version()

        assert result == "1.1.0"
        mock_version.assert_called_once_with(PACKAGE_NAME)

    def test_calls_with_correct_package_name(self):
        """Should query the correct package name."""
        with patch("vr_cli.commands.update.version") as mock_version:
            mock_version.return_value = "2.0.0"

            get_current_version()

        mock_version.assert_called_once_with("voicerun-cli")


class TestUpgradePackage:
    """Tests for upgrade_package function."""

    def test_successful_upgrade(self):
        """Should return 'upgraded' when a new version is installed."""
        with patch("vr_cli.commands.update.get_current_version") as mock_ver, \
             patch("subprocess.run") as mock_run, \
             patch("vr_cli.commands.update.print_info"), \
             patch("vr_cli.commands.update.print_success"):
            mock_ver.side_effect = ["1.0.0", "1.1.0"]
            mock_run.return_value = Mock(returncode=0, stdout="", stderr="")

            result = upgrade_package()

        assert result == "upgraded"
        mock_run.assert_called_once()
        cmd = mock_run.call_args[0][0]
        assert "--upgrade" in cmd
        assert "voicerun-cli" in cmd

    def test_already_latest(self):
        """Should return 'latest' when already on the latest version."""
        with patch("vr_cli.commands.update.get_current_version") as mock_ver, \
             patch("subprocess.run") as mock_run, \
             patch("vr_cli.commands.update.print_info"), \
             patch("vr_cli.commands.update.print_success") as mock_success:
            mock_ver.side_effect = ["1.1.0", "1.1.0"]
            mock_run.return_value = Mock(returncode=0, stdout="", stderr="")

            result = upgrade_package()

        assert result == "latest"
        # Should mention already on latest
        msg = mock_success.call_args[0][0]
        assert "latest" in msg.lower()
        assert "1.1.0" in msg

    def test_failed_upgrade(self):
        """Should return 'failed' when pip returns non-zero exit code."""
        with patch("vr_cli.commands.update.get_current_version") as mock_ver, \
             patch("subprocess.run") as mock_run, \
             patch("vr_cli.commands.update.print_info"), \
             patch("vr_cli.commands.update.print_error"):
            mock_ver.return_value = "1.0.0"
            mock_run.return_value = Mock(returncode=1, stdout="", stderr="Could not find version")

            result = upgrade_package()

        assert result == "failed"

    def test_exception_during_upgrade(self):
        """Should return 'failed' and print error on exception."""
        with patch("vr_cli.commands.update.get_current_version") as mock_ver, \
             patch("subprocess.run") as mock_run, \
             patch("vr_cli.commands.update.print_info"), \
             patch("vr_cli.commands.update.print_error") as mock_error:
            mock_ver.return_value = "1.0.0"
            mock_run.side_effect = OSError("pip not found")

            result = upgrade_package()

        assert result == "failed"
        mock_error.assert_called_once()

    def test_uses_sys_executable(self):
        """Should use sys.executable to invoke pip for correct Python."""
        with patch("vr_cli.commands.update.get_current_version") as mock_ver, \
             patch("subprocess.run") as mock_run, \
             patch("vr_cli.commands.update.sys") as mock_sys, \
             patch("vr_cli.commands.update.print_info"), \
             patch("vr_cli.commands.update.print_success"):
            mock_sys.executable = "/usr/bin/python3.12"
            mock_ver.side_effect = ["1.0.0", "1.1.0"]
            mock_run.return_value = Mock(returncode=0, stdout="", stderr="")

            upgrade_package()

        cmd = mock_run.call_args[0][0]
        assert cmd[0] == "/usr/bin/python3.12"


class TestRunUpdate:
    """Tests for run_update function."""

    def test_uses_persisted_setup_targets(self):
        """Should use targets saved by vr setup, not auto-detect."""
        with patch("vr_cli.commands.update.upgrade_package") as mock_upgrade, \
             patch("vr_cli.commands.update.get_setup_targets") as mock_targets, \
             patch("vr_cli.commands.update.install_skills") as mock_skills, \
             patch("vr_cli.commands.update.install_mcp_server") as mock_mcp, \
             patch("vr_cli.commands.update.console"):
            mock_upgrade.return_value = "upgraded"
            mock_targets.return_value = {
                "skills": ["claude-code", "codex"],
                "mcp": ["claude-code", "cursor"],
            }
            mock_skills.return_value = True
            mock_mcp.return_value = True

            run_update()

        mock_skills.assert_called_once_with(["claude-code", "codex"])
        mock_mcp.assert_called_once_with(["claude-code", "cursor"])

    def test_skips_all_when_no_setup_targets(self):
        """Should skip skills and MCP when setup has never been run."""
        with patch("vr_cli.commands.update.upgrade_package") as mock_upgrade, \
             patch("vr_cli.commands.update.get_setup_targets") as mock_targets, \
             patch("vr_cli.commands.update.install_skills") as mock_skills, \
             patch("vr_cli.commands.update.install_mcp_server") as mock_mcp, \
             patch("vr_cli.commands.update.console"), \
             patch("vr_cli.commands.update.print_info"), \
             patch("vr_cli.commands.update.print_success"):
            mock_upgrade.return_value = "upgraded"
            mock_targets.return_value = {}

            run_update()

        mock_skills.assert_not_called()
        mock_mcp.assert_not_called()

    def test_skips_skills_when_empty_list(self):
        """Should skip skills when setup saved an empty skills list."""
        with patch("vr_cli.commands.update.upgrade_package") as mock_upgrade, \
             patch("vr_cli.commands.update.get_setup_targets") as mock_targets, \
             patch("vr_cli.commands.update.install_skills") as mock_skills, \
             patch("vr_cli.commands.update.install_mcp_server") as mock_mcp, \
             patch("vr_cli.commands.update.console"), \
             patch("vr_cli.commands.update.print_info"):
            mock_upgrade.return_value = "upgraded"
            mock_targets.return_value = {
                "skills": [],
                "mcp": ["claude-code"],
            }
            mock_mcp.return_value = True

            run_update()

        mock_skills.assert_not_called()
        mock_mcp.assert_called_once_with(["claude-code"])

    def test_skips_mcp_when_empty_list(self):
        """Should skip MCP when setup saved an empty MCP list."""
        with patch("vr_cli.commands.update.upgrade_package") as mock_upgrade, \
             patch("vr_cli.commands.update.get_setup_targets") as mock_targets, \
             patch("vr_cli.commands.update.install_skills") as mock_skills, \
             patch("vr_cli.commands.update.install_mcp_server") as mock_mcp, \
             patch("vr_cli.commands.update.console"), \
             patch("vr_cli.commands.update.print_info"):
            mock_upgrade.return_value = "upgraded"
            mock_targets.return_value = {
                "skills": ["claude-code"],
                "mcp": [],
            }
            mock_skills.return_value = True

            run_update()

        mock_skills.assert_called_once_with(["claude-code"])
        mock_mcp.assert_not_called()

    def test_stops_on_upgrade_failure(self):
        """Should not install skills or MCP if upgrade fails."""
        with patch("vr_cli.commands.update.upgrade_package") as mock_upgrade, \
             patch("vr_cli.commands.update.get_setup_targets") as mock_targets, \
             patch("vr_cli.commands.update.install_skills") as mock_skills, \
             patch("vr_cli.commands.update.install_mcp_server") as mock_mcp, \
             patch("vr_cli.commands.update.console"):
            mock_upgrade.return_value = "failed"

            run_update()

        mock_targets.assert_not_called()
        mock_skills.assert_not_called()
        mock_mcp.assert_not_called()

    def test_only_skills_configured(self):
        """Should only install skills when MCP was not configured during setup."""
        with patch("vr_cli.commands.update.upgrade_package") as mock_upgrade, \
             patch("vr_cli.commands.update.get_setup_targets") as mock_targets, \
             patch("vr_cli.commands.update.install_skills") as mock_skills, \
             patch("vr_cli.commands.update.install_mcp_server") as mock_mcp, \
             patch("vr_cli.commands.update.console"), \
             patch("vr_cli.commands.update.print_info"):
            mock_upgrade.return_value = "upgraded"
            mock_targets.return_value = {
                "skills": ["openclaw"],
            }
            mock_skills.return_value = True

            run_update()

        mock_skills.assert_called_once_with(["openclaw"])
        mock_mcp.assert_not_called()


class TestUpdateFlag:
    """Tests for the --update CLI flag."""

    def test_update_flag_triggers_update(self):
        """Should trigger run_update when --update flag is passed."""
        with patch("vr_cli.cli.run_update") as mock_update:
            result = runner.invoke(app, ["--update"])

        mock_update.assert_called_once()

    def test_update_short_flag(self):
        """Should trigger run_update when -U flag is passed."""
        with patch("vr_cli.cli.run_update") as mock_update:
            result = runner.invoke(app, ["-U"])

        mock_update.assert_called_once()

    def test_update_flag_exits_after(self):
        """Should exit after running update (not proceed to subcommands)."""
        with patch("vr_cli.cli.run_update"):
            result = runner.invoke(app, ["--update"])

        assert result.exit_code == 0

    def test_help_shows_update_option(self):
        """Should show --update in help output."""
        result = runner.invoke(app, ["--help"])

        assert "--update" in result.output
        assert "-U" in result.output


class TestSetupTargetsPersistence:
    """Tests for get_setup_targets and save_setup_targets config helpers."""

    def test_save_and_load_targets(self, tmp_path):
        """Should round-trip save and load setup targets."""
        config_file = tmp_path / "config.json"

        with patch("vr_cli.utils.config.CONTEXT_CONFIG_FILE", config_file), \
             patch("vr_cli.utils.config.VOICERUN_DIR", tmp_path):
            save_setup_targets(["claude-code", "codex"], ["claude-code", "cursor"])
            result = get_setup_targets()

        assert result == {
            "skills": ["claude-code", "codex"],
            "mcp": ["claude-code", "cursor"],
        }

    def test_load_returns_empty_when_no_config(self, tmp_path):
        """Should return empty dict when config file doesn't exist."""
        config_file = tmp_path / "nonexistent.json"

        with patch("vr_cli.utils.config.CONTEXT_CONFIG_FILE", config_file):
            result = get_setup_targets()

        assert result == {}

    def test_save_preserves_existing_config(self, tmp_path):
        """Should not overwrite other config keys when saving targets."""
        config_file = tmp_path / "config.json"
        config_file.write_text(json.dumps({
            "current_context": "staging",
            "organization_id": "org-123",
        }))

        with patch("vr_cli.utils.config.CONTEXT_CONFIG_FILE", config_file), \
             patch("vr_cli.utils.config.VOICERUN_DIR", tmp_path):
            save_setup_targets(["claude-code"], ["cursor"])

        saved = json.loads(config_file.read_text())
        assert saved["current_context"] == "staging"
        assert saved["organization_id"] == "org-123"
        assert saved["setup_targets"] == {
            "skills": ["claude-code"],
            "mcp": ["cursor"],
        }

    def test_save_overwrites_previous_targets(self, tmp_path):
        """Should overwrite previously saved targets on re-setup."""
        config_file = tmp_path / "config.json"

        with patch("vr_cli.utils.config.CONTEXT_CONFIG_FILE", config_file), \
             patch("vr_cli.utils.config.VOICERUN_DIR", tmp_path):
            save_setup_targets(["claude-code"], ["claude-code"])
            save_setup_targets(["codex", "openclaw"], ["cursor", "windsurf"])
            result = get_setup_targets()

        assert result == {
            "skills": ["codex", "openclaw"],
            "mcp": ["cursor", "windsurf"],
        }
