"""Tests for ``ilum.cli.progress`` — live pod monitoring during Helm operations."""

from __future__ import annotations

import time
from unittest.mock import MagicMock

import pytest

from ilum.cli.output import IlumConsole, SpinnerHandle
from ilum.cli.progress import (
    _fetch_active_pods,
    _is_all_ready,
    _summarise_active,
    _summarise_pods,
    execute_with_progress,
)
from ilum.core.helm import HelmResult
from ilum.core.kubernetes import KubeClient, PodStatus
from ilum.core.release import ReleasePlan
from ilum.errors import HelmError


def _pod(
    name: str,
    phase: str = "Running",
    ready: bool = True,
    restart_count: int = 0,
) -> PodStatus:
    """Shorthand factory for PodStatus."""
    return PodStatus(
        name=name,
        namespace="default",
        phase=phase,
        ready=ready,
        restart_count=restart_count,
    )


# ---------------------------------------------------------------------------
# SpinnerHandle / progress_spinner (existing tests preserved)
# ---------------------------------------------------------------------------


class TestSpinnerHandle:
    def test_update_with_none_status(self) -> None:
        """SpinnerHandle with None status (quiet mode) should not raise."""
        handle = SpinnerHandle(None)
        handle.update("new message")  # Should not raise

    def test_update_with_mock_status(self) -> None:
        mock_status = MagicMock()
        handle = SpinnerHandle(mock_status)
        handle.update("processing...")
        mock_status.update.assert_called_once_with("processing...")


class TestProgressSpinner:
    def test_spinner_in_quiet_mode(self) -> None:
        console = IlumConsole()
        console.quiet = True
        with console.progress_spinner("test") as spinner:
            spinner.update("step 2")

    def test_spinner_returns_handle(self) -> None:
        console = IlumConsole()
        console.quiet = True
        with console.progress_spinner("Working...") as handle:
            assert isinstance(handle, SpinnerHandle)


class TestExistingStatusSpinner:
    def test_existing_spinner_still_works(self) -> None:
        console = IlumConsole()
        console._quiet = True
        assert hasattr(console, "status_spinner")


# ---------------------------------------------------------------------------
# _fetch_active_pods / _summarise_active / _is_all_ready
# ---------------------------------------------------------------------------


class TestFetchActivePods:
    def test_filters_completed_jobs(self) -> None:
        k8s = MagicMock(spec=KubeClient)
        k8s.list_pods.return_value = [
            _pod("app"),
            _pod("job", phase="Succeeded", ready=False),
            _pod("failed", phase="Failed", ready=False),
        ]
        active = _fetch_active_pods(k8s, "default")
        assert active is not None
        assert len(active) == 1
        assert active[0].name == "app"

    def test_returns_none_on_error(self) -> None:
        k8s = MagicMock(spec=KubeClient)
        k8s.list_pods.side_effect = RuntimeError("connection refused")
        assert _fetch_active_pods(k8s, "default") is None


class TestSummariseActive:
    def test_all_ready(self) -> None:
        active = [_pod("a"), _pod("b"), _pod("c")]
        assert _summarise_active(active) == "Pods: 3/3 ready"

    def test_mixed_state(self) -> None:
        active = [
            _pod("a"),
            _pod("b", phase="Pending", ready=False),
            _pod("c", phase="Running", ready=False),
        ]
        result = _summarise_active(active)
        assert "1/3 ready" in result
        assert "1 pending" in result
        assert "1 initialising" in result

    def test_with_restarts(self) -> None:
        active = [_pod("a", restart_count=3), _pod("b", restart_count=1)]
        result = _summarise_active(active)
        assert "4 restarts" in result

    def test_single_restart(self) -> None:
        active = [_pod("a", restart_count=1)]
        result = _summarise_active(active)
        assert "1 restart" in result
        assert "restarts" not in result

    def test_empty_list(self) -> None:
        assert _summarise_active([]) == "Pods: waiting for pods to start"


class TestIsAllReady:
    def test_all_running_and_ready(self) -> None:
        assert _is_all_ready([_pod("a"), _pod("b")]) is True

    def test_one_pending(self) -> None:
        assert _is_all_ready([_pod("a"), _pod("b", phase="Pending", ready=False)]) is False

    def test_one_initialising(self) -> None:
        assert _is_all_ready([_pod("a"), _pod("b", phase="Running", ready=False)]) is False

    def test_empty_list(self) -> None:
        assert _is_all_ready([]) is False


# ---------------------------------------------------------------------------
# _summarise_pods (public convenience wrapper)
# ---------------------------------------------------------------------------


class TestSummarisePods:
    def test_all_ready(self) -> None:
        k8s = MagicMock(spec=KubeClient)
        k8s.list_pods.return_value = [_pod("a"), _pod("b"), _pod("c")]
        assert _summarise_pods(k8s, "default") == "Pods: 3/3 ready"

    def test_completed_jobs_excluded(self) -> None:
        k8s = MagicMock(spec=KubeClient)
        k8s.list_pods.return_value = [
            _pod("app-1"),
            _pod("app-2"),
            _pod("init-job", phase="Succeeded", ready=False),
        ]
        assert "2/2 ready" in _summarise_pods(k8s, "default")

    def test_api_error_returns_empty(self) -> None:
        k8s = MagicMock(spec=KubeClient)
        k8s.list_pods.side_effect = RuntimeError("connection refused")
        assert _summarise_pods(k8s, "default") == ""

    def test_no_pods(self) -> None:
        k8s = MagicMock(spec=KubeClient)
        k8s.list_pods.return_value = []
        assert _summarise_pods(k8s, "default") == "Pods: waiting for pods to start"

    def test_all_jobs_completed(self) -> None:
        k8s = MagicMock(spec=KubeClient)
        k8s.list_pods.return_value = [
            _pod("job-a", phase="Succeeded", ready=False),
        ]
        assert _summarise_pods(k8s, "default") == "Pods: waiting for pods to start"


# ---------------------------------------------------------------------------
# execute_with_progress
# ---------------------------------------------------------------------------


class TestExecuteWithProgress:
    @staticmethod
    def _make_plan(namespace: str = "default") -> ReleasePlan:
        return ReleasePlan(
            action="install",
            release="ilum",
            namespace=namespace,
            chart="ilum/ilum",
        )

    @staticmethod
    def _make_result() -> HelmResult:
        return HelmResult(returncode=0, stdout="ok", stderr="", command=[])

    @staticmethod
    def _quiet_console() -> IlumConsole:
        """Return a console in quiet mode so Rich doesn't try to render."""
        c = IlumConsole()
        c.quiet = True
        return c

    def test_propagates_helm_error(self) -> None:
        mgr = MagicMock()
        mgr.execute.side_effect = HelmError("boom")
        mgr.k8s.list_pods.return_value = []

        with pytest.raises(HelmError, match="boom"):
            execute_with_progress(
                mgr,
                self._make_plan(),
                self._quiet_console(),
                message="Installing...",
                poll_interval=0.01,
                settle_timeout=0.01,
            )

    def test_fresh_install_waits_for_new_pods(self) -> None:
        """Phase 2a detects new pods, phase 2b waits for them to become ready."""
        mgr = MagicMock()
        mgr.execute.return_value = self._make_result()

        # Call 1 (baseline): no pods
        # Calls 2-3 (settle): new pods appear pending
        # Call 4+: pods become ready
        call_count = 0

        def evolving_pods(namespace: str):
            nonlocal call_count
            call_count += 1
            if call_count <= 1:
                return []  # baseline: empty
            if call_count <= 3:
                return [_pod("a", phase="Pending", ready=False)]
            return [_pod("a")]

        mgr.k8s.list_pods.side_effect = evolving_pods

        console = MagicMock()
        handle = MagicMock()
        console.progress_spinner.return_value.__enter__ = MagicMock(return_value=handle)
        console.progress_spinner.return_value.__exit__ = MagicMock(return_value=False)

        result = execute_with_progress(
            mgr,
            self._make_plan(),
            console,
            message="Installing...",
            poll_interval=0.01,
            settle_timeout=0.5,
        )
        assert result.returncode == 0
        assert mgr.k8s.list_pods.call_count >= 4

    def test_settle_detects_new_pod_in_module_enable(self) -> None:
        """Enabling a module adds a pod — settle waits for it to appear."""
        mgr = MagicMock()
        mgr.execute.return_value = self._make_result()

        call_count = 0

        def evolving_pods(namespace: str):
            nonlocal call_count
            call_count += 1
            baseline = [_pod("core"), _pod("ui")]
            if call_count <= 2:
                return baseline  # baseline + first settle check
            # New pod appears
            return baseline + [_pod("superset")]

        mgr.k8s.list_pods.side_effect = evolving_pods

        console = MagicMock()
        handle = MagicMock()
        console.progress_spinner.return_value.__enter__ = MagicMock(return_value=handle)
        console.progress_spinner.return_value.__exit__ = MagicMock(return_value=False)

        result = execute_with_progress(
            mgr,
            self._make_plan(),
            console,
            message="Enabling...",
            poll_interval=0.01,
            settle_timeout=0.5,
        )
        assert result.returncode == 0

    def test_settle_timeout_proceeds_to_readiness(self) -> None:
        """If no new pods appear within settle_timeout, proceed to readiness check."""
        mgr = MagicMock()
        mgr.execute.return_value = self._make_result()
        # Pods never change (e.g., config-only upgrade)
        mgr.k8s.list_pods.return_value = [_pod("a"), _pod("b")]

        result = execute_with_progress(
            mgr,
            self._make_plan(),
            self._quiet_console(),
            message="Upgrading...",
            poll_interval=0.01,
            settle_timeout=0.03,
            readiness_timeout=0.5,
        )
        assert result.returncode == 0

    def test_spinner_receives_updates_during_phase1(self) -> None:
        mgr = MagicMock()

        def slow_execute(plan: ReleasePlan) -> HelmResult:
            time.sleep(0.15)
            return self._make_result()

        mgr.execute.side_effect = slow_execute
        mgr.k8s.list_pods.return_value = [_pod("a"), _pod("b")]

        console = MagicMock()
        handle = MagicMock()
        console.progress_spinner.return_value.__enter__ = MagicMock(return_value=handle)
        console.progress_spinner.return_value.__exit__ = MagicMock(return_value=False)

        execute_with_progress(
            mgr,
            self._make_plan(),
            console,
            message="Installing...",
            poll_interval=0.05,
            settle_timeout=0.01,
        )

        assert handle.update.call_count >= 1
        update_text = handle.update.call_args[0][0]
        assert "Pods:" in update_text
        assert "Installing..." in update_text

    def test_readiness_timeout_warns(self) -> None:
        """When pods never become ready, a warning is shown but result is returned."""
        mgr = MagicMock()
        mgr.execute.return_value = self._make_result()

        call_count = 0

        def evolving_pods(namespace: str):
            nonlocal call_count
            call_count += 1
            if call_count <= 1:
                return []  # baseline: empty
            # New pod appears but stays pending forever
            return [_pod("a", phase="Pending", ready=False)]

        mgr.k8s.list_pods.side_effect = evolving_pods

        console = MagicMock()
        handle = MagicMock()
        console.progress_spinner.return_value.__enter__ = MagicMock(return_value=handle)
        console.progress_spinner.return_value.__exit__ = MagicMock(return_value=False)

        result = execute_with_progress(
            mgr,
            self._make_plan(),
            console,
            message="Installing...",
            poll_interval=0.01,
            settle_timeout=0.5,
            readiness_timeout=0.05,
        )
        assert result.returncode == 0
        console.warning.assert_called_once()
        warning_text = console.warning.call_args[0][0]
        assert "Timed out" in warning_text

    def test_poll_failure_is_silent(self) -> None:
        """If k8s polling fails throughout, it times out gracefully."""
        mgr = MagicMock()
        mgr.execute.return_value = self._make_result()
        mgr.k8s.list_pods.side_effect = RuntimeError("connection refused")

        console = MagicMock()
        handle = MagicMock()
        console.progress_spinner.return_value.__enter__ = MagicMock(return_value=handle)
        console.progress_spinner.return_value.__exit__ = MagicMock(return_value=False)

        result = execute_with_progress(
            mgr,
            self._make_plan(),
            console,
            message="Installing...",
            poll_interval=0.01,
            settle_timeout=0.03,
            readiness_timeout=0.05,
        )
        assert result.returncode == 0

    def test_single_api_call_per_readiness_iteration(self) -> None:
        """Each phase 2b iteration uses one list_pods call for both check and summary."""
        mgr = MagicMock()
        mgr.execute.return_value = self._make_result()

        # Baseline empty, then pods appear pending, then ready
        call_count = 0

        def evolving_pods(namespace: str):
            nonlocal call_count
            call_count += 1
            if call_count <= 1:
                return []
            if call_count <= 4:
                return [_pod("a", phase="Pending", ready=False)]
            return [_pod("a")]

        mgr.k8s.list_pods.side_effect = evolving_pods

        console = MagicMock()
        handle = MagicMock()
        console.progress_spinner.return_value.__enter__ = MagicMock(return_value=handle)
        console.progress_spinner.return_value.__exit__ = MagicMock(return_value=False)

        execute_with_progress(
            mgr,
            self._make_plan(),
            console,
            message="Installing...",
            poll_interval=0.01,
            settle_timeout=0.5,
        )

        # Verify no excessive API calls — each phase iteration should call
        # list_pods exactly once (not twice for check+summary).
        # With baseline(1) + settle(1-2) + readiness(2-3), expect roughly 5-6
        assert mgr.k8s.list_pods.call_count <= 10
