"""Tests for the WiFi network management module (src.system.network).

Uses mocking to avoid requiring actual WiFi hardware.

Run with:
    pytest tests/test_network.py -v
"""

from __future__ import annotations

import sys
from pathlib import Path
from unittest.mock import MagicMock, patch

import pytest

# Ensure src is on sys.path
SRC = Path(__file__).resolve().parent.parent / "src"
if str(SRC) not in sys.path:
    sys.path.insert(0, str(SRC))

from src.system.network import (
    wifi_scan,
    wifi_status,
    wifi_saved_networks,
    wifi_connect,
    wifi_disconnect,
    wifi_forget,
    hotspot_status,
    hotspot_enable,
    hotspot_disable,
    _has_nmcli,
)


# ── Fixtures ─────────────────────────────────────────────────────────────────


@pytest.fixture
def mock_subprocess():
    """Patch subprocess.run globally for all tests in this module."""
    with patch("src.system.network.subprocess.run") as mock:
        yield mock


@pytest.fixture
def mock_hostapd_conf(tmp_path):
    """Create a mock /etc/hostapd/hostapd.conf for hotspot tests."""
    conf = tmp_path / "hostapd.conf"
    conf.write_text("ssid=Pi-Pedal\nchannel=6\n")
    return conf


# ── _has_nmcli() tests ────────────────────────────────────────────────────────


def test_has_nmcli_true(mock_subprocess):
    """When 'which nmcli' succeeds, _has_nmcli returns True."""
    mock_subprocess.return_value = MagicMock(returncode=0)
    assert _has_nmcli() is True


def test_has_nmcli_false(mock_subprocess):
    """When 'which nmcli' fails, _has_nmcli returns False."""
    mock_subprocess.side_effect = FileNotFoundError("no nmcli")
    assert _has_nmcli() is False


# ── wifi_scan() tests ─────────────────────────────────────────────────────────


def test_wifi_scan_nmcli_networks(mock_subprocess):
    """wifi_scan returns parsed networks from nmcli."""
    mock_subprocess.return_value = MagicMock(
        stdout="Net1:75:WPA2\nNet2:50:WPA2\nNet3:90:Open\n",
        returncode=0,
    )
    networks = wifi_scan()
    assert len(networks) == 3
    assert networks[0]["ssid"] == "Net3"  # sorted by signal desc
    assert networks[0]["signal"] == 90
    assert networks[0]["encryption"] == "Open"


def test_wifi_scan_nmcli_empty(mock_subprocess):
    """wifi_scan returns empty list when no networks found."""
    mock_subprocess.return_value = MagicMock(stdout="", returncode=0)
    networks = wifi_scan()
    assert networks == []


def test_wifi_scan_nmcli_fallback_iwlist(mock_subprocess):
    """wifi_scan falls back to iwlist when nmcli fails."""
    # First call (nmcli) fails
    mock_subprocess.side_effect = [
        FileNotFoundError("no nmcli"),  # _has_nmcli check
        MagicMock(stdout="", returncode=0),  # First nmcli scan attempt
    ]
    # Actually, let's patch _has_nmcli to make this clearer
    with patch("src.system.network._has_nmcli", return_value=False):
        with patch("src.system.network._iwlist_scan", return_value=[
            {"ssid": "TestNet", "signal": 60, "encryption": "WPA2"}
        ]) as mock_iw:
            networks = wifi_scan()
            assert len(networks) == 1
            assert networks[0]["ssid"] == "TestNet"
            assert networks[0]["signal"] == 60
            mock_iw.assert_called_once()


def test_wifi_scan_handles_exception(mock_subprocess):
    """wifi_scan returns empty list on unexpected error."""
    with patch("src.system.network._has_nmcli", return_value=True):
        mock_subprocess.side_effect = RuntimeError("Unexpected")
        networks = wifi_scan()
        assert networks == []


# ── wifi_status() tests ───────────────────────────────────────────────────────


def test_wifi_status_connected(mock_subprocess):
    """wifi_status returns connected state with SSID and IP."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="wlan0:wifi:connected:HomeNet\neth0:ethernet:connected:eth0\n", returncode=0),
        MagicMock(stdout="GENERAL.CONNECTION:HomeNet\nIP4.ADDRESS:192.168.1.100/24\n", returncode=0),
        MagicMock(stdout="HomeNet:85\nOtherNet:30\n", returncode=0),
    ]
    with patch("src.system.network._has_nmcli", return_value=True):
        status = wifi_status()
        assert status["connected"] is True
        assert status["ssid"] == "HomeNet"
        assert status["ip"] == "192.168.1.100"
        assert status["signal"] == 85
        assert status["device"] == "wlan0"


def test_wifi_status_not_connected(mock_subprocess):
    """wifi_status returns disconnected when no WiFi connection."""
    mock_subprocess.side_effect = [
        MagicMock(stdout="wlan0:wifi:disconnected:\neth0:ethernet:connected:eth0\n", returncode=0),
    ]
    status = wifi_status()
    assert status["connected"] is False
    assert status["ssid"] is None


def test_wifi_status_no_wifi_device(mock_subprocess):
    """wifi_status returns disconnected when no WiFi device exists."""
    mock_subprocess.return_value = MagicMock(
        stdout="eth0:ethernet:connected:eth0\n",
        returncode=0,
    )
    status = wifi_status()
    assert status["connected"] is False


# ── wifi_connect() tests ──────────────────────────────────────────────────────


def test_wifi_connect_requires_params():
    """wifi_connect returns error when SSID or password missing."""
    result = wifi_connect("", "password")
    assert result["ok"] is False
    assert "SSID" in result["error"]

    result = wifi_connect("MyNet", "")
    assert result["ok"] is False
    assert "Password" in result["error"]


def test_wifi_connect_new_network(mock_subprocess):
    """wifi_connect calls nmcli to connect to a new network."""
    # Saved networks (nmcli connection show) returns empty for new
    # Has nmcli → True from first call check
    with patch("src.system.network._has_nmcli", return_value=True):
        mock_subprocess.side_effect = [
            # _nmcli_saved - empty list
            MagicMock(stdout="", returncode=0),
            # _nmcli_connect - success
            MagicMock(stdout="", returncode=0),
        ]
        result = wifi_connect("NewNet", "secret123")
        assert result["ok"] is True
        assert mock_subprocess.call_count >= 2


def test_wifi_connect_existing_network(mock_subprocess):
    """wifi_connect activates an already-saved network."""
    with patch("src.system.network._has_nmcli", return_value=True):
        mock_subprocess.side_effect = [
            # _nmcli_saved - existing network
            MagicMock(stdout="NewNet:abc-123:wireless\nOldNet:def-456:wireless\n", returncode=0),
            # _nmcli_connect - connection up
            MagicMock(stdout="", returncode=0),
        ]
        result = wifi_connect("NewNet", "secret123")
        assert result["ok"] is True


# ── wifi_disconnect() tests ───────────────────────────────────────────────────


def test_wifi_disconnect(mock_subprocess):
    """wifi_disconnect returns ok."""
    with patch("src.system.network._has_nmcli", return_value=True):
        mock_subprocess.side_effect = [
            # _nmcli_status
            MagicMock(stdout="wlan0:wifi:connected:HomeNet\n", returncode=0),
            MagicMock(stdout="GENERAL.CONNECTION:HomeNet\nIP4.ADDRESS:192.168.1.100/24\n", returncode=0),
            MagicMock(stdout="HomeNet:80\n", returncode=0),
            # _nmcli_disconnect
            MagicMock(stdout="", returncode=0),
        ]
        result = wifi_disconnect()
        assert result["ok"] is True


# ── wifi_forget() tests ──────────────────────────────────────────────────────


def test_wifi_forget_network(mock_subprocess):
    """wifi_forget deletes a saved network."""
    with patch("src.system.network._has_nmcli", return_value=True):
        mock_subprocess.side_effect = [
            # _nmcli_saved
            MagicMock(stdout="MyNet:abc-123:wireless\nOther:def-456:wireless\n", returncode=0),
            # _nmcli_forget - nmcli connection delete
            MagicMock(stdout="", returncode=0),
        ]
        result = wifi_forget("MyNet")
        assert result["ok"] is True


def test_wifi_forget_not_found(mock_subprocess):
    """wifi_forget returns ok=False when network not saved."""
    with patch("src.system.network._has_nmcli", return_value=True):
        mock_subprocess.return_value = MagicMock(
            stdout="Other:def-456:wireless\n",
            returncode=0,
        )
        result = wifi_forget("NonExistent")
        assert result["ok"] is False


# ── wifi_saved_networks() tests ────────────────────────────────────────────────


def test_wifi_saved_networks(mock_subprocess):
    """wifi_saved_networks returns saved WiFi connections."""
    with patch("src.system.network._has_nmcli", return_value=True):
        mock_subprocess.return_value = MagicMock(
            stdout="HomeNet:abc-def:wireless\nOfficeNet:ghi-jkl:wireless\neth0:xyz:ethernet\n",
            returncode=0,
        )
        saved = wifi_saved_networks()
        assert len(saved) == 2
        assert saved[0]["ssid"] == "HomeNet"
        assert saved[1]["ssid"] == "OfficeNet"


# ── hotspot_status() tests ────────────────────────────────────────────────────


def test_hotspot_status_active(mock_subprocess, mock_hostapd_conf):
    """hotspot_status returns active state when hostapd is running."""
    with patch("src.system.network.Path.exists", return_value=True):
        with patch("src.system.network.Path.read_text", return_value="ssid=Pi-Pedal\nchannel=6\n"):
            mock_subprocess.side_effect = [
                MagicMock(stdout="active\n", returncode=0),  # systemctl is-active
                MagicMock(stdout="Station aa:bb:cc:dd:ee:ff\n", returncode=0),  # iw station dump
            ]
            status = hotspot_status()
            assert status["active"] is True
            assert status["ssid"] == "Pi-Pedal"
            assert status["clients"] == 1
            assert status["ip"] == "192.168.4.1"


def test_hotspot_status_inactive(mock_subprocess):
    """hotspot_status returns inactive when hostapd is stopped."""
    mock_subprocess.return_value = MagicMock(stdout="inactive\n", returncode=0)
    status = hotspot_status()
    assert status["active"] is False
    assert status["clients"] == 0


# ── hotspot_enable() / hotspot_disable() tests ─────────────────────────────────


def test_hotspot_enable_short_password():
    """hotspot_enable rejects passwords shorter than 8 chars."""
    result = hotspot_enable("TestAP", "123")
    assert result["ok"] is False
    assert "at least 8 characters" in result["error"].lower()


def test_hotspot_enable_success(mock_subprocess):
    """hotspot_enable calls the setup script."""
    mock_subprocess.return_value = MagicMock(returncode=0)
    result = hotspot_enable("TestAP", "longpassword")
    assert result["ok"] is True


def test_hotspot_enable_failure(mock_subprocess):
    """hotspot_enable returns error on script failure."""
    mock_subprocess.return_value = MagicMock(returncode=1, stderr="Script failed")
    result = hotspot_enable("TestAP", "longpassword")
    assert result["ok"] is False


def test_hotspot_disable_success(mock_subprocess):
    """hotspot_disable calls the disable script."""
    mock_subprocess.return_value = MagicMock(returncode=0)
    result = hotspot_disable()
    assert result["ok"] is True


def test_hotspot_disable_failure(mock_subprocess):
    """hotspot_disable returns error on script failure."""
    mock_subprocess.return_value = MagicMock(returncode=1, stderr="Script failed")
    result = hotspot_disable()
    assert result["ok"] is False