"""Tests for ``ilum upgrade`` command."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest
from typer.testing import CliRunner

from ilum.cli.main import app
from ilum.core.helm import HelmResult
from ilum.core.release import ReleaseInfo, ReleasePlan
from ilum.core.safety import DriftResult, ValuesDiff
from ilum.errors import ReleaseNotFoundError


@pytest.fixture()
def runner() -> CliRunner:
    return CliRunner()


@pytest.fixture()
def mock_mgr() -> MagicMock:
    from ilum.core.modules import ModuleResolver
    from ilum.core.release import ReleaseManager

    mgr = MagicMock(spec=ReleaseManager)
    mgr.resolver = ModuleResolver()
    mgr.get_enabled_modules.return_value = ["core", "ui"]
    mgr.is_stuck.return_value = False
    mgr.get_release_info.return_value = ReleaseInfo(
        name="ilum",
        namespace="default",
        status="deployed",
        chart="ilum",
        chart_version="6.7.0",
        revision=1,
        last_deployed="2024-01-15",
    )
    mgr.plan_upgrade.return_value = ReleasePlan(
        action="upgrade",
        release="ilum",
        namespace="default",
        chart="ilum/ilum",
        set_flags=["custom=true"],
        modules=[],
        warnings=[],
        computed_values={"custom": True},
        effective_diff=ValuesDiff(changed={"custom": (False, True)}),
        drift=DriftResult(has_drift=False, snapshot_exists=True, diff=ValuesDiff(), live_values={}),
    )
    mgr.execute.return_value = HelmResult(
        returncode=0,
        stdout="upgraded",
        stderr="",
        command=[],
    )
    mgr.fetch_computed_values.return_value = {
        "ilum-core": {"enabled": True},
        "ilum-ui": {"enabled": True},
        "mongodb": {"enabled": True},
    }
    mgr.resolve_latest_version.return_value = "6.8.0"
    return mgr


class TestUpgradeCommand:
    def test_upgrade_help(self, runner: CliRunner) -> None:
        result = runner.invoke(app, ["upgrade", "--help"])
        assert result.exit_code == 0
        assert "Upgrade an existing Ilum installation" in result.output

    def test_upgrade_dry_run(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run"])
        assert result.exit_code == 0
        assert "Dry-run" in result.output
        mock_mgr.execute.assert_not_called()

    def test_upgrade_with_yes(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--yes"])
        assert result.exit_code == 0
        assert "upgraded successfully" in result.output

    def test_upgrade_not_found(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        mock_mgr.plan_upgrade.side_effect = ReleaseNotFoundError(
            "Not found", suggestion="Use 'ilum install'"
        )
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--yes"])
        assert result.exit_code == 1

    def test_upgrade_stuck_without_force(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        mock_mgr.is_stuck.return_value = True
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--yes"])
        assert result.exit_code == 1
        assert "stuck" in result.output

    def test_upgrade_stuck_with_force_rollback(
        self, runner: CliRunner, mock_mgr: MagicMock
    ) -> None:
        mock_mgr.is_stuck.return_value = True
        mock_mgr.rollback_if_stuck.return_value = True
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--yes", "--force-rollback"])
        assert result.exit_code == 0
        mock_mgr.rollback_if_stuck.assert_called_once()

    def test_upgrade_shows_drift(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        mock_mgr.plan_upgrade.return_value = ReleasePlan(
            action="upgrade",
            release="ilum",
            namespace="default",
            chart="ilum/ilum",
            set_flags=[],
            modules=[],
            warnings=[],
            computed_values={},
            effective_diff=ValuesDiff(),
            drift=DriftResult(
                has_drift=True,
                snapshot_exists=True,
                diff=ValuesDiff(added={"external.change": "foo"}),
                live_values={"external": {"change": "foo"}},
            ),
        )
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--yes"])
        assert "External changes" in result.output or "Drift" in result.output

    def test_upgrade_shows_values_diff(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run"])
        assert "Values Changes" in result.output

    def test_upgrade_with_version(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            runner.invoke(app, ["upgrade", "--yes", "--version", "6.8.0"])
        call_kwargs = mock_mgr.plan_upgrade.call_args.kwargs
        assert call_kwargs["version"] == "6.8.0"

    def test_upgrade_summary_shows_version_transition(
        self, runner: CliRunner, mock_mgr: MagicMock
    ) -> None:
        mock_mgr.get_release_info.return_value = ReleaseInfo(
            name="ilum",
            namespace="default",
            status="deployed",
            chart="ilum",
            chart_version="6.7.0",
            revision=1,
            last_deployed="2024-01-15",
        )
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run", "--version", "6.8.0"])
        assert "6.7.0" in result.output
        assert "6.8.0" in result.output

    def test_upgrade_summary_shows_release_and_namespace(
        self, runner: CliRunner, mock_mgr: MagicMock
    ) -> None:
        mock_mgr.get_release_info.return_value = ReleaseInfo(
            name="ilum",
            namespace="default",
            status="deployed",
            chart="ilum",
            chart_version="6.7.0",
            revision=1,
            last_deployed="2024-01-15",
        )
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run"])
        assert "Upgrade Summary" in result.output
        assert "ilum" in result.output

    def test_upgrade_summary_resolves_latest_version(
        self, runner: CliRunner, mock_mgr: MagicMock
    ) -> None:
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run"])
        # Should show resolved version, not "latest"
        assert "6.7.0" in result.output
        assert "6.8.0" in result.output
        assert "\u2192" in result.output

    def test_upgrade_summary_shows_active_modules(
        self, runner: CliRunner, mock_mgr: MagicMock
    ) -> None:
        mock_mgr.fetch_computed_values.return_value = {
            "ilum-core": {"enabled": True},
            "ilum-ui": {"enabled": True},
            "ilum-jupyter": {"enabled": True},
        }
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run"])
        assert "core" in result.output
        assert "ui" in result.output
        assert "jupyter" in result.output

    def test_upgrade_summary_shows_no_changes_message(
        self, runner: CliRunner, mock_mgr: MagicMock
    ) -> None:
        mock_mgr.plan_upgrade.return_value = ReleasePlan(
            action="upgrade",
            release="ilum",
            namespace="default",
            chart="ilum/ilum",
            set_flags=[],
            modules=[],
            warnings=[],
            computed_values={},
            effective_diff=ValuesDiff(),
            drift=DriftResult(
                has_drift=False, snapshot_exists=True, diff=ValuesDiff(), live_values={}
            ),
        )
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run"])
        assert "No value changes" in result.output

    def test_upgrade_shows_upgrade_notes_link(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run"])
        assert "https://ilum.cloud/docs/upgrade-notes/" in result.output

    def test_upgrade_nothing_to_upgrade(self, runner: CliRunner, mock_mgr: MagicMock) -> None:
        """When already on latest version with no value changes, tell user nothing to do."""
        mock_mgr.resolve_latest_version.return_value = "6.7.0"
        mock_mgr.plan_upgrade.return_value = ReleasePlan(
            action="upgrade",
            release="ilum",
            namespace="default",
            chart="ilum/ilum",
            set_flags=[],
            modules=[],
            warnings=[],
            computed_values={},
            effective_diff=ValuesDiff(),
            drift=DriftResult(
                has_drift=False, snapshot_exists=True, diff=ValuesDiff(), live_values={}
            ),
        )
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--dry-run"])
        assert "Nothing to upgrade" in result.output
        assert "6.7.0" in result.output
        mock_mgr.execute.assert_not_called()

    def test_upgrade_loads_profile_modules_when_no_module_flag(
        self, runner: CliRunner, mock_mgr: MagicMock
    ) -> None:
        """After ``ilum init`` with a different preset, upgrade should detect value changes."""
        # Profile has modules that differ from what's deployed — simulates a
        # preset change via ``ilum init``.
        mock_mgr.get_enabled_modules.return_value = ["core", "ui", "jupyter"]
        mock_mgr.resolve_latest_version.return_value = "6.7.0"

        # plan_upgrade returns a diff when profile modules are passed
        mock_mgr.plan_upgrade.return_value = ReleasePlan(
            action="upgrade",
            release="ilum",
            namespace="default",
            chart="ilum/ilum",
            set_flags=["ilum-jupyter.enabled=true"],
            modules=["core", "ui", "jupyter"],
            warnings=[],
            computed_values={"ilum-jupyter": {"enabled": True}},
            effective_diff=ValuesDiff(
                changed={"ilum-jupyter.enabled": (False, True)},
            ),
            drift=DriftResult(
                has_drift=False, snapshot_exists=True, diff=ValuesDiff(), live_values={}
            ),
        )

        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--yes"])

        # Should NOT hit the "Nothing to upgrade" early exit
        assert "Nothing to upgrade" not in result.output
        assert "upgraded successfully" in result.output
        # Verify profile modules were passed to plan_upgrade
        call_kwargs = mock_mgr.plan_upgrade.call_args.kwargs
        assert call_kwargs["modules"] == ["core", "ui", "jupyter"]

    def test_upgrade_proceeds_with_value_changes_same_version(
        self, runner: CliRunner, mock_mgr: MagicMock
    ) -> None:
        """When on latest version but value changes exist, proceed with upgrade."""
        mock_mgr.resolve_latest_version.return_value = "6.7.0"
        with patch("ilum.cli.upgrade_cmd._build_manager", return_value=mock_mgr):
            result = runner.invoke(app, ["upgrade", "--yes"])
        assert "upgraded successfully" in result.output
        mock_mgr.execute.assert_called_once()
