from __future__ import annotations

from unittest.mock import MagicMock, patch

from yslow.checks.wifi import run, summary, _parse_bitrate, _freq_from_scan
from yslow.diagnosis import _analyze_wifi
from yslow.models import WifiBand, WifiResult, Severity


# --- WifiBand ---


class TestWifiBand:
    def test_2_4_ghz(self):
        assert WifiBand.from_frequency(2412) == WifiBand.BAND_2_4
        assert WifiBand.from_frequency(2484) == WifiBand.BAND_2_4

    def test_5_ghz(self):
        assert WifiBand.from_frequency(5180) == WifiBand.BAND_5
        assert WifiBand.from_frequency(5825) == WifiBand.BAND_5

    def test_6_ghz(self):
        assert WifiBand.from_frequency(5955) == WifiBand.BAND_6
        assert WifiBand.from_frequency(7115) == WifiBand.BAND_6


# --- summary ---


class TestSummary:
    def test_none(self):
        assert "not available" in summary(None)

    def test_with_bitrate(self):
        result = WifiResult(
            interface="wlp1s0",
            signal_dbm=-52,
            signal_avg_dbm=-51,
            frequency_mhz=5180,
            band=WifiBand.BAND_5,
            tx_bitrate_mbps=866.7,
            rx_bitrate_mbps=400.0,
        )
        text = summary(result)
        assert "-52 dBm" in text
        assert "5 GHz" in text
        assert "867 Mbps TX" in text
        assert "400 Mbps RX" in text

    def test_without_bitrate(self):
        result = WifiResult(
            interface="wlp1s0",
            signal_dbm=-65,
            signal_avg_dbm=-64,
            frequency_mhz=2437,
            band=WifiBand.BAND_2_4,
            tx_bitrate_mbps=None,
            rx_bitrate_mbps=None,
        )
        text = summary(result)
        assert "-65 dBm" in text
        assert "2.4 GHz" in text
        assert "TX" not in text


# --- _parse_bitrate ---


class TestParseBitrate:
    def test_none_nla(self):
        assert _parse_bitrate(None) is None

    def test_bitrate32(self):
        nla = MagicMock()
        nla.get_attr.side_effect = lambda k: (
            8667 if k == "NL80211_RATE_INFO_BITRATE32" else None
        )
        assert _parse_bitrate(nla) == 866.7

    def test_bitrate16_fallback(self):
        nla = MagicMock()

        def _get(k):
            if k == "NL80211_RATE_INFO_BITRATE32":
                return None
            if k == "NL80211_RATE_INFO_BITRATE":
                return 540
            return None

        nla.get_attr.side_effect = _get
        assert _parse_bitrate(nla) == 54.0


# --- helpers for mocking nl80211 messages ---


def _make_nla(attrs: dict):
    """Create a mock NLA object whose get_attr returns values from *attrs*."""
    m = MagicMock()
    m.get_attr.side_effect = lambda k: attrs.get(k)
    return m


def _make_sta(
    signal: int | None = -52,
    signal_avg: int | None = -51,
    tx_bitrate: int = 8667,
    rx_bitrate: int = 4000,
):
    """Build a mock station info NLA."""
    tx = MagicMock()
    tx.get_attr.side_effect = lambda k: (
        tx_bitrate if k == "NL80211_RATE_INFO_BITRATE32" else None
    )
    rx = MagicMock()
    rx.get_attr.side_effect = lambda k: (
        rx_bitrate if k == "NL80211_RATE_INFO_BITRATE32" else None
    )
    return _make_nla(
        {
            "NL80211_STA_INFO_SIGNAL": signal,
            "NL80211_STA_INFO_SIGNAL_AVG": signal_avg,
            "NL80211_STA_INFO_TX_BITRATE": tx,
            "NL80211_STA_INFO_RX_BITRATE": rx,
        }
    )


def _make_station_msg(sta):
    return _make_nla({"NL80211_ATTR_STA_INFO": sta})


def _make_iw(
    freq: int | None = 5180,
    sta=None,
    scan_results=None,
):
    """Build a mock IW context manager.

    *freq*: value for NL80211_ATTR_WIPHY_FREQ on the interface message (None to omit).
    *sta*: a mock station info NLA (built via _make_sta); defaults to a good one.
    *scan_results*: list of mock scan result NLAs for nlm_request fallback.
    """
    if sta is None:
        sta = _make_sta()

    iface_msg = _make_nla({"NL80211_ATTR_WIPHY_FREQ": freq})
    station_msg = _make_station_msg(sta)

    iw = MagicMock()
    iw.__enter__ = MagicMock(return_value=iw)
    iw.__exit__ = MagicMock(return_value=False)
    iw.get_interface_by_ifindex.return_value = [iface_msg]
    iw.get_stations.return_value = iter([station_msg])
    if scan_results:
        bss_msg = scan_results[0]
    else:
        bss_msg = None
    iw.get_associated_bss.return_value = bss_msg
    return iw


# --- _freq_from_scan ---


class TestFreqFromScan:
    def test_returns_freq(self):
        bss = _make_nla({"NL80211_BSS_FREQUENCY": 5260})
        bss_msg = _make_nla({"NL80211_ATTR_BSS": bss})
        iw = MagicMock()
        iw.get_associated_bss.return_value = bss_msg
        assert _freq_from_scan(iw, 2) == 5260

    def test_not_associated(self):
        iw = MagicMock()
        iw.get_associated_bss.return_value = None
        assert _freq_from_scan(iw, 2) is None

    def test_error(self):
        iw = MagicMock()
        iw.get_associated_bss.side_effect = OSError("netlink error")
        assert _freq_from_scan(iw, 2) is None


# --- run ---


class TestRun:
    @patch("yslow.checks.wifi.platform")
    def test_non_linux_returns_none(self, mock_platform):
        mock_platform.system.return_value = "Darwin"
        assert run() is None

    @patch("yslow.checks.wifi.get_wireless_interface", return_value=None)
    @patch("yslow.checks.wifi.platform")
    def test_no_wifi_interface_returns_none(self, mock_platform, mock_iface):
        mock_platform.system.return_value = "Linux"
        assert run() is None

    @patch("yslow.checks.wifi.get_wireless_interface", return_value="wlp1s0")
    @patch("yslow.checks.wifi.platform")
    def test_import_error_returns_none(self, mock_platform, mock_iface):
        mock_platform.system.return_value = "Linux"
        with patch.dict("sys.modules", {"pyroute2": None}):
            # Force re-import to trigger ImportError
            assert run() is None

    @patch("yslow.checks.wifi.socket.if_nametoindex", return_value=2)
    @patch("yslow.checks.wifi.get_wireless_interface", return_value="wlp1s0")
    @patch("yslow.checks.wifi.platform")
    def test_success_with_interface_freq(self, mock_plat, mock_iface, mock_idx):
        mock_plat.system.return_value = "Linux"
        iw = _make_iw(freq=5180)
        with patch("pyroute2.IW", return_value=iw):
            result = run()
        assert result is not None
        assert result.frequency_mhz == 5180
        assert result.band == WifiBand.BAND_5

    @patch("yslow.checks.wifi.socket.if_nametoindex", return_value=2)
    @patch("yslow.checks.wifi.get_wireless_interface", return_value="wlp1s0")
    @patch("yslow.checks.wifi.platform")
    def test_falls_back_to_bss_scan_for_freq(self, mock_plat, mock_iface, mock_idx):
        mock_plat.system.return_value = "Linux"
        bss = _make_nla({"NL80211_BSS_STATUS": 1, "NL80211_BSS_FREQUENCY": 5260})
        scan_msg = _make_nla({"NL80211_ATTR_BSS": bss})
        iw = _make_iw(freq=None, scan_results=[scan_msg])
        with patch("pyroute2.IW", return_value=iw):
            result = run()
        assert result is not None
        assert result.frequency_mhz == 5260
        assert result.band == WifiBand.BAND_5

    @patch("yslow.checks.wifi.socket.if_nametoindex", return_value=2)
    @patch("yslow.checks.wifi.get_wireless_interface", return_value="wlp1s0")
    @patch("yslow.checks.wifi.platform")
    def test_no_freq_anywhere_returns_none(self, mock_plat, mock_iface, mock_idx):
        mock_plat.system.return_value = "Linux"
        iw = _make_iw(freq=None, scan_results=[])
        with patch("pyroute2.IW", return_value=iw):
            assert run() is None

    @patch("yslow.checks.wifi.socket.if_nametoindex", return_value=2)
    @patch("yslow.checks.wifi.get_wireless_interface", return_value="wlp1s0")
    @patch("yslow.checks.wifi.platform")
    def test_missing_signal_dbm_returns_none(self, mock_plat, mock_iface, mock_idx):
        mock_plat.system.return_value = "Linux"
        sta = _make_sta(signal=None, signal_avg=-51)
        iw = _make_iw(freq=5180, sta=sta)
        with patch("pyroute2.IW", return_value=iw):
            assert run() is None

    @patch("yslow.checks.wifi.socket.if_nametoindex", return_value=2)
    @patch("yslow.checks.wifi.get_wireless_interface", return_value="wlp1s0")
    @patch("yslow.checks.wifi.platform")
    def test_missing_signal_avg_returns_none(self, mock_plat, mock_iface, mock_idx):
        mock_plat.system.return_value = "Linux"
        sta = _make_sta(signal=-52, signal_avg=None)
        iw = _make_iw(freq=5180, sta=sta)
        with patch("pyroute2.IW", return_value=iw):
            assert run() is None


# --- _analyze_wifi ---


class TestAnalyzeWifi:
    def _result(self, signal: int) -> WifiResult:
        return WifiResult(
            interface="wlp1s0",
            signal_dbm=signal,
            signal_avg_dbm=signal,
            frequency_mhz=5180,
            band=WifiBand.BAND_5,
            tx_bitrate_mbps=866.7,
            rx_bitrate_mbps=400.0,
        )

    def test_good_signal_no_issues(self):
        issues, alerts = _analyze_wifi(self._result(-50))
        assert issues == []
        assert alerts == []

    def test_weak_signal_warning(self):
        issues, alerts = _analyze_wifi(self._result(-72))
        assert len(issues) == 1
        assert issues[0].severity == Severity.WARNING
        assert issues[0].kind == "wifi-signal"
        assert len(alerts) == 1
        assert alerts[0].severity == Severity.WARNING

    def test_very_weak_signal_error(self):
        issues, alerts = _analyze_wifi(self._result(-80))
        assert len(issues) == 1
        assert issues[0].severity == Severity.ERROR
        assert issues[0].kind == "wifi-signal"
