"""Tests for ``ilum rollback`` 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
from ilum.errors import HelmError


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


@pytest.fixture()
def mock_helm() -> MagicMock:
    from ilum.core.helm import HelmClient

    mock = MagicMock(spec=HelmClient)
    mock.namespace = "default"
    mock.kubecontext = ""
    mock.history.return_value = HelmResult(
        returncode=0,
        stdout="",
        stderr="",
        json_data=[
            {
                "revision": 1,
                "status": "superseded",
                "chart": "ilum-6.6.0",
                "updated": "2024-01-10",
            },
            {
                "revision": 2,
                "status": "deployed",
                "chart": "ilum-6.7.0",
                "updated": "2024-01-15",
            },
        ],
    )
    mock.get_values_at_revision.return_value = HelmResult(
        returncode=0,
        stdout="",
        stderr="",
        json_data={"ilum-core": {"enabled": True}},
    )
    mock.rollback.return_value = HelmResult(
        returncode=0, stdout="Rollback was a success!", stderr=""
    )
    return mock


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

    mgr = MagicMock(spec=ReleaseManager)
    mgr.resolver = ModuleResolver()
    mgr.helm = mock_helm
    mgr.fetch_live_values.return_value = {"ilum-core": {"enabled": True}}
    mgr.fetch_computed_values.return_value = {"ilum-core": {"enabled": True}}
    mgr.get_release_info.return_value = ReleaseInfo(
        name="ilum",
        namespace="default",
        status="deployed",
        chart="ilum",
        chart_version="6.7.0",
        revision=2,
        last_deployed="2024-01-15 12:00:00",
    )
    return mgr


def _patch_rollback(mock_mgr: MagicMock, mock_helm: MagicMock):
    """Patch both ReleaseManager and HelmClient constructors."""
    return patch.multiple(
        "ilum.cli.rollback_cmd",
        ReleaseManager=MagicMock(return_value=mock_mgr),
        HelmClient=MagicMock(return_value=mock_helm),
    )


class TestRollbackCommand:
    def test_rollback_help(self, runner: CliRunner) -> None:
        result = runner.invoke(app, ["rollback", "--help"])
        assert result.exit_code == 0
        assert "Roll back" in result.output

    def test_rollback_dry_run(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["rollback", "--dry-run"])
        assert result.exit_code == 0
        assert "Dry run" in result.output or "Rollback Plan" in result.output
        mock_helm.rollback.assert_not_called()

    def test_rollback_yes_flag(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["rollback", "--yes"])
        assert result.exit_code == 0
        assert "Rolled back" in result.output
        mock_helm.rollback.assert_called_once_with("ilum", 1)

    def test_rollback_specific_revision(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["rollback", "--revision", "1", "--yes"])
        assert result.exit_code == 0
        mock_helm.rollback.assert_called_once_with("ilum", 1)

    def test_rollback_invalid_revision(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["rollback", "--revision", "99", "--yes"])
        assert result.exit_code == 1

    def test_rollback_only_one_revision(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        mock_helm.history.return_value = HelmResult(
            returncode=0,
            stdout="",
            stderr="",
            json_data=[{"revision": 1, "status": "deployed"}],
        )
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["rollback", "--yes"])
        assert result.exit_code == 1
        assert "Only one revision" in result.output

    def test_rollback_json_output(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["--output", "json", "rollback", "--dry-run"])
        assert result.exit_code == 0
        assert "current_revision" in result.output

    def test_rollback_helm_error(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        mock_helm.rollback.side_effect = HelmError("rollback failed", error_code="ILUM-073")
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["rollback", "--yes"])
        assert result.exit_code == 1

    def test_rollback_cancelled(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        with _patch_rollback(mock_mgr, mock_helm):
            # Without --yes, CliRunner sends 'n' by default via input
            result = runner.invoke(app, ["rollback"], input="n\n")
        assert result.exit_code == 0
        mock_helm.rollback.assert_not_called()

    def test_rollback_diff_shown(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        """When values differ between revisions, diff should be shown."""
        mock_helm.get_values_at_revision.side_effect = [
            HelmResult(
                returncode=0,
                stdout="",
                stderr="",
                json_data={"ilum-core": {"enabled": True, "extra": "new"}},
            ),
            HelmResult(
                returncode=0,
                stdout="",
                stderr="",
                json_data={"ilum-core": {"enabled": True}},
            ),
        ]
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["rollback", "--dry-run"])
        assert result.exit_code == 0

    def test_rollback_default_targets_previous(
        self, runner: CliRunner, mock_mgr: MagicMock, mock_helm: MagicMock
    ) -> None:
        """When revision=0 (default), targets current_rev - 1."""
        with _patch_rollback(mock_mgr, mock_helm):
            result = runner.invoke(app, ["rollback", "--yes"])
        assert result.exit_code == 0
        # revision 2 is current, so target should be 1
        mock_helm.rollback.assert_called_once_with("ilum", 1)
