"""Tests verifying graceful behavior when psutil functions fail or are unavailable.

These tests ensure that s-tui source classes handle situations where psutil
functions raise AttributeError (missing on platform), return None / empty,
or raise IOError/OSError during sensor reads.  None of these should crash.
"""

from collections import OrderedDict
from unittest.mock import MagicMock

from s_tui.sources.fan_source import FanSource
from s_tui.sources.freq_source import FreqSource
from s_tui.sources.rapl_power_source import RaplPowerSource
from s_tui.sources.temp_source import TempSource
from s_tui.sources.util_source import UtilSource
from tests.conftest import SensorTemperature

# ---------------------------------------------------------------------------
# UtilSource failure modes
# ---------------------------------------------------------------------------


class TestUtilSourceFailures:
    def test_cpu_percent_missing_attribute(self, mocker):
        """If psutil has no cpu_percent, UtilSource should be unavailable."""
        mocker.patch("psutil.cpu_count", return_value=4)
        mocker.patch("psutil.cpu_percent", side_effect=AttributeError)
        # The __init__ checks hasattr, not calls it directly in the normal path,
        # but we still shouldn't crash
        src = UtilSource()
        # Source may or may not be available depending on hasattr check,
        # but it should not raise
        assert isinstance(src, UtilSource)


# ---------------------------------------------------------------------------
# FreqSource failure modes
# ---------------------------------------------------------------------------


class TestFreqSourceFailures:
    def test_cpu_freq_returns_none(self, mocker):
        """If cpu_freq returns None (no frequency info), handle gracefully."""
        import contextlib

        mocker.patch("psutil.cpu_freq", return_value=None)
        # This may raise or set unavailable - should not crash with unhandled exception
        with contextlib.suppress(TypeError, AttributeError):
            FreqSource()

    def test_cpu_freq_empty_percpu(self, mocker):
        """If cpu_freq(percpu=True) returns empty list."""

        def _cpu_freq(percpu=False):
            if percpu:
                return []
            return None

        import contextlib

        mocker.patch("psutil.cpu_freq", side_effect=_cpu_freq)
        with contextlib.suppress(TypeError, ValueError, ZeroDivisionError):
            FreqSource()


# ---------------------------------------------------------------------------
# TempSource failure modes
# ---------------------------------------------------------------------------


class TestTempSourceFailures:
    def test_sensors_temperatures_returns_none(self, mocker):
        """sensors_temperatures() returning None should mark unavailable."""
        mocker.patch("psutil.sensors_temperatures", return_value=None)
        src = TempSource()
        assert src.get_is_available() is False

    def test_sensors_temperatures_returns_empty(self, mocker):
        """sensors_temperatures() returning {} should mark unavailable."""
        mocker.patch("psutil.sensors_temperatures", return_value={})
        src = TempSource()
        assert src.get_is_available() is False

    def test_sensors_temperatures_attribute_error(self, mocker):
        """sensors_temperatures not available on platform."""
        mocker.patch("psutil.sensors_temperatures", side_effect=AttributeError)
        src = TempSource()
        assert src.get_is_available() is False

    def test_sensors_temperatures_ioerror_on_init(self, mocker):
        """First call raises IOError — should mark unavailable."""
        mocker.patch("psutil.sensors_temperatures", side_effect=IOError)
        src = TempSource()
        assert src.get_is_available() is False

    def test_sensors_temperatures_ioerror_on_update(self, mocker):
        """sensors_temperatures works during init but fails during update.

        The implementation catches the error and keeps stale data.
        """
        sensors = [
            SensorTemperature(label="Core 0", current=55.0, high=80.0, critical=100.0),
        ]
        temps = OrderedDict([("coretemp", sensors)])

        call_count = [0]

        def _temps_side_effect():
            call_count[0] += 1
            if call_count[0] <= 2:  # init calls it twice
                return temps
            raise OSError("sensor read failed")

        mocker.patch("psutil.sensors_temperatures", side_effect=_temps_side_effect)
        src = TempSource()
        assert src.get_is_available() is True

        # Capture readings before the failure
        old_measurement = list(src.last_measurement)

        # update() should not raise — it catches IOError and keeps stale data
        src.update()
        assert src.last_measurement == old_measurement


# ---------------------------------------------------------------------------
# FanSource failure modes
# ---------------------------------------------------------------------------


class TestFanSourceFailures:
    def test_sensors_fans_returns_none(self, mocker):
        """sensors_fans() returning None should mark unavailable."""
        mocker.patch("psutil.sensors_fans", return_value=None)
        src = FanSource()
        assert src.get_is_available() is False

    def test_sensors_fans_attribute_error(self, mocker):
        """sensors_fans not available on platform."""
        mocker.patch("psutil.sensors_fans", side_effect=AttributeError)
        src = FanSource()
        assert src.get_is_available() is False

    def test_sensors_fans_ioerror_on_init(self, mocker):
        """IOError during fan dict creation."""
        # First call succeeds (availability check), second raises
        call_count = [0]

        def _fans():
            call_count[0] += 1
            if call_count[0] == 1:
                return {"hw": []}  # truthy for availability check
            raise OSError("read failed")

        mocker.patch("psutil.sensors_fans", side_effect=_fans)
        src = FanSource()
        assert src.get_is_available() is False

    def test_sensors_fans_typeerror_on_init(self, mocker):
        """TypeError from psutil on init should mark unavailable (GH-255).

        psutil may raise TypeError internally when sysfs fan sensor files
        contain None (e.g. Intel xe GPU fans).
        """
        mocker.patch("psutil.sensors_fans", side_effect=TypeError)
        src = FanSource()
        assert src.get_is_available() is False

    def test_sensors_fans_typeerror_on_update(self, mocker):
        """TypeError from psutil during update keeps stale data (GH-256).

        When psutil.sensors_fans() raises TypeError mid-run, the source
        should keep its previous measurement rather than crashing.
        """
        from tests.conftest import make_fans_dict

        fans = make_fans_dict(count=1, current=1200)
        mocker.patch("psutil.sensors_fans", return_value=fans)
        src = FanSource()
        assert src.get_is_available() is True
        src.update()
        assert src.last_measurement == [1200]

        # Mid-run: psutil raises TypeError
        mocker.patch("psutil.sensors_fans", side_effect=TypeError)
        src.update()  # should not crash
        # Stale data preserved
        assert src.last_measurement == [1200]

    def test_sensors_fans_none_on_update(self, mocker):
        """sensors_fans() returning None mid-run keeps stale data.

        When psutil.sensors_fans() returns None during an update cycle,
        the source should keep its previous measurement.
        """
        from tests.conftest import make_fans_dict

        fans = make_fans_dict(count=1, current=900)
        mocker.patch("psutil.sensors_fans", return_value=fans)
        src = FanSource()
        assert src.get_is_available() is True
        src.update()
        assert src.last_measurement == [900]

        # Mid-run: returns None
        mocker.patch("psutil.sensors_fans", return_value=None)
        src.update()  # should not crash
        # Stale data preserved
        assert src.last_measurement == [900]


# ---------------------------------------------------------------------------
# RaplPowerSource failure modes
# ---------------------------------------------------------------------------


class TestRaplPowerSourceFailures:
    def test_no_reader_available(self, mocker):
        """No power reader available on platform."""
        mocker.patch(
            "s_tui.sources.rapl_power_source.get_power_reader", return_value=None
        )
        src = RaplPowerSource()
        assert src.get_is_available() is False

    def test_reader_fails_on_read(self, mocker):
        """Reader is available but fails during update — should not crash."""
        from tests.conftest import RaplStats

        reader = MagicMock()
        reader.read_power.return_value = [
            RaplStats(label="pkg", current=1000000.0, max=0.0),
        ]
        mocker.patch(
            "s_tui.sources.rapl_power_source.get_power_reader", return_value=reader
        )
        src = RaplPowerSource()
        assert src.get_is_available() is True

        # Now make reader fail on next read
        reader.read_power.side_effect = OSError("file disappeared")
        src.update()  # should not crash
        assert src.get_is_available() is True

    def test_reader_returns_none(self, mocker):
        """Reader returns None during update — should not crash."""
        from tests.conftest import RaplStats

        reader = MagicMock()
        reader.read_power.return_value = [
            RaplStats(label="pkg", current=1000000.0, max=0.0),
        ]
        mocker.patch(
            "s_tui.sources.rapl_power_source.get_power_reader", return_value=reader
        )
        src = RaplPowerSource()
        assert src.get_is_available() is True

        # Now reader returns None
        reader.read_power.return_value = None
        src.update()  # should not crash
        assert src.get_is_available() is True

    def test_update_skipped_when_unavailable(self, mocker):
        """update() should be a no-op when source is not available."""
        mocker.patch(
            "s_tui.sources.rapl_power_source.get_power_reader", return_value=None
        )
        src = RaplPowerSource()
        # Should not raise
        src.update()
