"""Tests for stuck-release detection and recovery."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest
from fastapi.testclient import TestClient

from ilum.core.release import ReleaseInfo
from ilum.errors import HelmError


class TestRecoverEndpoint:
    """POST /api/v1/release/recover."""

    def test_recover_when_stuck(self, api_client: TestClient, mock_api_manager: MagicMock) -> None:
        mock_api_manager.get_release_info.return_value = ReleaseInfo(
            name="ilum",
            namespace="default",
            status="pending-upgrade",
            chart="ilum",
            chart_version="6.7.0",
            revision=10,
            last_deployed="2024-01-01T00:00:00Z",
        )

        # After rollback, return a deployed state
        def side_effect(release: str) -> ReleaseInfo:
            return ReleaseInfo(
                name="ilum",
                namespace="default",
                status="deployed",
                chart="ilum",
                chart_version="6.7.0",
                revision=11,
                last_deployed="2024-01-01T00:01:00Z",
            )

        mock_api_manager.get_release_info.side_effect = [
            # First call: stuck
            ReleaseInfo(
                name="ilum",
                namespace="default",
                status="pending-upgrade",
                chart="ilum",
                chart_version="6.7.0",
                revision=10,
                last_deployed="2024-01-01T00:00:00Z",
            ),
            # Second call: after rollback
            ReleaseInfo(
                name="ilum",
                namespace="default",
                status="deployed",
                chart="ilum",
                chart_version="6.7.0",
                revision=11,
                last_deployed="2024-01-01T00:01:00Z",
            ),
        ]

        resp = api_client.post("/api/v1/release/recover")
        assert resp.status_code == 200
        data = resp.json()
        assert data["recovered"] is True
        assert data["previous_status"] == "pending-upgrade"
        assert data["rolled_back_to"] == 11
        mock_api_manager.helm.rollback.assert_called_once_with("ilum")

    def test_recover_when_not_stuck(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.get_release_info.return_value = ReleaseInfo(
            name="ilum",
            namespace="default",
            status="deployed",
            chart="ilum",
            chart_version="6.7.0",
            revision=10,
            last_deployed="2024-01-01T00:00:00Z",
        )

        resp = api_client.post("/api/v1/release/recover")
        assert resp.status_code == 200
        data = resp.json()
        assert data["recovered"] is False
        assert data["previous_status"] == "deployed"
        mock_api_manager.helm.rollback.assert_not_called()

    def test_recover_pending_install(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.get_release_info.side_effect = [
            ReleaseInfo(
                name="ilum",
                namespace="default",
                status="pending-install",
                chart="ilum",
                chart_version="6.7.0",
                revision=1,
                last_deployed="2024-01-01T00:00:00Z",
            ),
            ReleaseInfo(
                name="ilum",
                namespace="default",
                status="deployed",
                chart="ilum",
                chart_version="6.7.0",
                revision=2,
                last_deployed="2024-01-01T00:01:00Z",
            ),
        ]

        resp = api_client.post("/api/v1/release/recover")
        assert resp.status_code == 200
        data = resp.json()
        assert data["recovered"] is True
        assert data["previous_status"] == "pending-install"

    def test_recover_pending_rollback(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.get_release_info.side_effect = [
            ReleaseInfo(
                name="ilum",
                namespace="default",
                status="pending-rollback",
                chart="ilum",
                chart_version="6.7.0",
                revision=5,
                last_deployed="2024-01-01T00:00:00Z",
            ),
            ReleaseInfo(
                name="ilum",
                namespace="default",
                status="deployed",
                chart="ilum",
                chart_version="6.7.0",
                revision=6,
                last_deployed="2024-01-01T00:01:00Z",
            ),
        ]

        resp = api_client.post("/api/v1/release/recover")
        assert resp.status_code == 200
        data = resp.json()
        assert data["recovered"] is True
        assert data["previous_status"] == "pending-rollback"

    def test_recover_rollback_failure_returns_502(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.get_release_info.return_value = ReleaseInfo(
            name="ilum",
            namespace="default",
            status="pending-upgrade",
            chart="ilum",
            chart_version="6.7.0",
            revision=10,
            last_deployed="2024-01-01T00:00:00Z",
        )
        mock_api_manager.helm.rollback.side_effect = HelmError("rollback failed")

        resp = api_client.post("/api/v1/release/recover")
        assert resp.status_code == 502

    def test_recover_requires_auth(
        self, unauthed_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        resp = unauthed_client.post("/api/v1/release/recover")
        assert resp.status_code == 401


class TestAutoRecoveryGuard:
    """Ensure mutating endpoints auto-recover stuck releases."""

    def test_enable_auto_recovers_stuck_release(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.is_stuck.return_value = True
        resp = api_client.post("/api/v1/modules/jupyter/enable")
        assert resp.status_code == 202
        mock_api_manager.helm.rollback.assert_called_once_with("ilum")

    def test_enable_skips_recovery_when_not_stuck(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.is_stuck.return_value = False
        resp = api_client.post("/api/v1/modules/jupyter/enable")
        assert resp.status_code == 202
        mock_api_manager.helm.rollback.assert_not_called()

    def test_disable_auto_recovers_stuck_release(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.is_stuck.return_value = True
        mock_api_manager.fetch_computed_values.return_value = {"ilum-jupyter": {"enabled": True}}
        resp = api_client.post("/api/v1/modules/jupyter/disable")
        assert resp.status_code == 202
        mock_api_manager.helm.rollback.assert_called_once_with("ilum")

    def test_values_update_auto_recovers_stuck_release(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.is_stuck.return_value = True
        resp = api_client.put(
            "/api/v1/values",
            json={"set_flags": ["ilum-core.replicaCount=3"]},
        )
        assert resp.status_code == 202
        mock_api_manager.helm.rollback.assert_called_once_with("ilum")

    def test_auto_recovery_failure_returns_409(
        self, api_client: TestClient, mock_api_manager: MagicMock
    ) -> None:
        mock_api_manager.is_stuck.return_value = True
        mock_api_manager.helm.rollback.side_effect = HelmError("rollback failed")

        resp = api_client.post("/api/v1/modules/jupyter/enable")
        assert resp.status_code == 409
        assert "stuck" in resp.json()["detail"].lower()


class TestStartupRecovery:
    """Verify auto_connect() recovers stuck releases on boot."""

    def test_startup_recovers_stuck_release(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("ILUM_RELEASE_NAME", "ilum")
        monkeypatch.setenv("ILUM_NAMESPACE", "default")

        mock_helm = MagicMock()
        mock_helm.namespace = "default"
        mock_helm.kubecontext = ""

        mock_mgr = MagicMock()
        mock_mgr.helm = mock_helm
        mock_mgr.is_stuck.return_value = True

        with (
            patch("ilum.api.startup.ReleaseManager", return_value=mock_mgr),
            patch("ilum.api.startup.HelmClient", return_value=mock_helm),
            patch("ilum.api.startup.KubeClient"),
            patch("ilum.api.startup.ModuleResolver"),
            patch("ilum.api.startup.ConfigManager"),
            patch("ilum.api.startup.IlumPaths") as mock_paths_cls,
            patch("ilum.api.startup.set_manager"),
            patch("ilum.api.job_runner.has_any_helm_job", return_value=False),
        ):
            mock_paths = MagicMock()
            mock_paths_cls.default.return_value = mock_paths

            from ilum.api.startup import auto_connect

            auto_connect()

        mock_mgr.is_stuck.assert_called_once_with("ilum")
        mock_mgr.helm.rollback.assert_called_once_with("ilum")

    def test_startup_skips_recovery_when_not_stuck(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("ILUM_RELEASE_NAME", "ilum")
        monkeypatch.setenv("ILUM_NAMESPACE", "default")

        mock_helm = MagicMock()
        mock_helm.namespace = "default"
        mock_helm.kubecontext = ""

        mock_mgr = MagicMock()
        mock_mgr.helm = mock_helm
        mock_mgr.is_stuck.return_value = False

        with (
            patch("ilum.api.startup.ReleaseManager", return_value=mock_mgr),
            patch("ilum.api.startup.HelmClient", return_value=mock_helm),
            patch("ilum.api.startup.KubeClient"),
            patch("ilum.api.startup.ModuleResolver"),
            patch("ilum.api.startup.ConfigManager"),
            patch("ilum.api.startup.IlumPaths") as mock_paths_cls,
            patch("ilum.api.startup.set_manager"),
            patch("ilum.api.job_runner.has_any_helm_job", return_value=False),
        ):
            mock_paths = MagicMock()
            mock_paths_cls.default.return_value = mock_paths

            from ilum.api.startup import auto_connect

            auto_connect()

        mock_mgr.is_stuck.assert_called_once_with("ilum")
        mock_mgr.helm.rollback.assert_not_called()

    def test_startup_skips_recovery_when_helm_job_exists(
        self, monkeypatch: pytest.MonkeyPatch
    ) -> None:
        """When any ilum-api helm Job exists, skip auto-recovery even if release is stuck."""
        monkeypatch.setenv("ILUM_RELEASE_NAME", "ilum")
        monkeypatch.setenv("ILUM_NAMESPACE", "default")

        mock_helm = MagicMock()
        mock_helm.namespace = "default"
        mock_helm.kubecontext = ""

        mock_mgr = MagicMock()
        mock_mgr.helm = mock_helm
        mock_mgr.is_stuck.return_value = True  # Would normally trigger rollback

        with (
            patch("ilum.api.startup.ReleaseManager", return_value=mock_mgr),
            patch("ilum.api.startup.HelmClient", return_value=mock_helm),
            patch("ilum.api.startup.KubeClient"),
            patch("ilum.api.startup.ModuleResolver"),
            patch("ilum.api.startup.ConfigManager"),
            patch("ilum.api.startup.IlumPaths") as mock_paths_cls,
            patch("ilum.api.startup.set_manager"),
            patch(
                "ilum.api.job_runner.has_any_helm_job",
                return_value=True,
            ),
        ):
            mock_paths = MagicMock()
            mock_paths_cls.default.return_value = mock_paths

            from ilum.api.startup import auto_connect

            auto_connect()

        # is_stuck should NOT be called when a Job is active
        mock_mgr.is_stuck.assert_not_called()
        mock_mgr.helm.rollback.assert_not_called()

    def test_startup_recovery_failure_does_not_crash(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """Startup should log the error but not prevent the API from booting."""
        monkeypatch.setenv("ILUM_RELEASE_NAME", "ilum")
        monkeypatch.setenv("ILUM_NAMESPACE", "default")

        mock_helm = MagicMock()
        mock_helm.namespace = "default"
        mock_helm.kubecontext = ""
        mock_helm.rollback.side_effect = HelmError("rollback failed")

        mock_mgr = MagicMock()
        mock_mgr.helm = mock_helm
        mock_mgr.is_stuck.return_value = True

        with (
            patch("ilum.api.startup.ReleaseManager", return_value=mock_mgr),
            patch("ilum.api.startup.HelmClient", return_value=mock_helm),
            patch("ilum.api.startup.KubeClient"),
            patch("ilum.api.startup.ModuleResolver"),
            patch("ilum.api.startup.ConfigManager"),
            patch("ilum.api.startup.IlumPaths") as mock_paths_cls,
            patch("ilum.api.startup.set_manager"),
            patch("ilum.api.job_runner.has_any_helm_job", return_value=False),
        ):
            mock_paths = MagicMock()
            mock_paths_cls.default.return_value = mock_paths

            from ilum.api.startup import auto_connect

            # Should NOT raise — failure is logged, not propagated
            auto_connect()
