# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""Tests for network administration skills: Netdata, Uptime Kuma, and network tools."""

import os
from unittest.mock import MagicMock, patch

import pytest

# ---------------------------------------------------------------------------
# Netdata tests
# ---------------------------------------------------------------------------


class TestNetdataAvailability:
    def test_netdata_available_true_by_default(self):
        from familiar.skills.netdata.skill import _netdata_available

        # Default URL is set, so should return True
        assert _netdata_available() is True


class TestGetHostMetrics:
    def test_get_host_metrics_degrades_on_connection_error(self):
        import httpx

        from familiar.skills.netdata.skill import get_host_metrics

        with patch("httpx.Client") as mock_client_cls:
            mock_client = MagicMock()
            mock_client_cls.return_value.__enter__.return_value = mock_client
            mock_client.get.side_effect = httpx.ConnectError("Connection refused")

            result = get_host_metrics()
            assert isinstance(result, dict)
            assert result.get("format") == "degraded" or "hostname" in result

    def test_get_host_metrics_returns_expected_keys(self):
        from familiar.skills.netdata.skill import get_host_metrics

        info_response = {"hostname": "testhost", "version": "1.0", "uptime": 86400}
        chart_response = {
            "labels": ["time", "user", "system"],
            "data": [[1000, 20.5, 5.5]],
        }

        def _mock_api_get(path):
            if "/api/v1/info" in path:
                return info_response
            return chart_response

        with patch("familiar.skills.netdata.skill._api_get", side_effect=_mock_api_get):
            result = get_host_metrics()
            assert isinstance(result, dict)
            assert "hostname" in result
            assert result["hostname"] == "testhost"
            assert "cpu_pct" in result


class TestGetTopProcesses:
    def test_get_top_processes_degrades_on_error(self):
        from familiar.skills.netdata.skill import get_top_processes

        with patch("familiar.skills.netdata.skill._api_get", side_effect=Exception("timeout")):
            result = get_top_processes()
            assert isinstance(result, dict)
            assert result.get("format") == "degraded"

    def test_get_top_processes_returns_lists(self):
        from familiar.skills.netdata.skill import get_top_processes

        cpu_data = {
            "labels": ["time", "nginx", "python", "sshd"],
            "data": [[1000, 15.2, 8.4, 0.5]],
        }
        mem_data = {
            "labels": ["time", "nginx", "python", "sshd"],
            "data": [[1000, 256.0, 512.0, 8.0]],
        }

        call_count = 0

        def _mock_api_get(path):
            nonlocal call_count
            call_count += 1
            if "apps.cpu" in path:
                return cpu_data
            return mem_data

        with patch("familiar.skills.netdata.skill._api_get", side_effect=_mock_api_get):
            result = get_top_processes()
            assert isinstance(result, dict)
            assert "top_cpu" in result
            assert "top_ram" in result


class TestGetInterfaceTraffic:
    def test_get_interface_traffic_unknown_interface(self):
        from familiar.skills.netdata.skill import get_interface_traffic

        empty_data = {"labels": ["time"], "data": []}

        with patch("familiar.skills.netdata.skill._api_get", return_value=empty_data):
            result = get_interface_traffic("nonexistent0")
            assert isinstance(result, dict)
            # Either has error key or has rx_mbps (zero)
            assert "error" in result or "rx_mbps" in result or result.get("format") == "degraded"

    def test_get_interface_traffic_returns_metrics(self):
        from familiar.skills.netdata.skill import get_interface_traffic

        traffic_data = {
            "labels": ["time", "received", "sent"],
            "data": [[1000, 1024.0, -512.0]],
        }

        def _mock_api_get(path):
            return traffic_data

        with patch("familiar.skills.netdata.skill._api_get", side_effect=_mock_api_get):
            result = get_interface_traffic("eth0")
            assert isinstance(result, dict)
            # rx_mbps = 1024 kbps / 1024 = 1.0 Mbps
            assert "rx_mbps" in result
            assert result["rx_mbps"] > 0


class TestGetSystemAlarms:
    def test_get_system_alarms_returns_empty_when_no_alarms(self):
        from familiar.skills.netdata.skill import get_system_alarms

        with patch("familiar.skills.netdata.skill._api_get", return_value={"alarms": {}}):
            result = get_system_alarms()
            assert isinstance(result, dict)
            assert result.get("count") == 0
            assert result.get("active") == []


# ---------------------------------------------------------------------------
# Uptime Kuma tests
# ---------------------------------------------------------------------------


class TestUptimeKumaAvailability:
    def test_uptime_kuma_available_true_by_default(self):
        from familiar.skills.uptime_kuma.skill import _uptime_kuma_available

        # Default URL is set, so should return True
        assert _uptime_kuma_available() is True


class TestGetMonitorStatus:
    def test_get_monitor_status_degrades_on_connection_error(self):
        import httpx

        from familiar.skills.uptime_kuma.skill import get_monitor_status

        with patch("httpx.Client") as mock_client_cls:
            mock_client = MagicMock()
            mock_client_cls.return_value.__enter__.return_value = mock_client
            mock_client.get.side_effect = httpx.ConnectError("Connection refused")

            result = get_monitor_status()
            assert isinstance(result, dict)
            assert result.get("format") == "degraded" or result.get("total") is not None

    def test_get_monitor_status_returns_counts(self):
        from familiar.skills.uptime_kuma.skill import get_monitor_status

        heartbeat_data = {
            "heartbeatList": {
                "1": [
                    {
                        "status": 1,
                        "monitorName": "My Site",
                        "time": 1000,
                        "ping": 100,
                    }
                ]
            },
            "uptimeList": {"1_24": 0.99},
        }

        # No API key so it hits the fallback path
        with patch.dict(os.environ, {"UPTIME_KUMA_API_KEY": ""}):
            with patch(
                "familiar.skills.uptime_kuma.skill._api_get",
                return_value=heartbeat_data,
            ):
                result = get_monitor_status()
                assert isinstance(result, dict)
                assert result.get("up_count") == 1

    def test_get_monitor_status_authenticated(self):
        from familiar.skills.uptime_kuma.skill import get_monitor_status

        monitor_list = [
            {
                "id": 1,
                "name": "site",
                "url": "http://site.com",
                "type": 1,
                "active": True,
                "heartbeat": {"status": 1, "time": 1000, "ping": 50},
            }
        ]

        with patch.dict(os.environ, {"UPTIME_KUMA_API_KEY": "test-key"}):
            with patch(
                "familiar.skills.uptime_kuma.skill._api_get",
                return_value=monitor_list,
            ):
                result = get_monitor_status()
                assert isinstance(result, dict)
                assert result.get("total") == 1


class TestGetHeartbeatHistory:
    def test_get_heartbeat_history_requires_api_key(self):
        from familiar.skills.uptime_kuma.skill import get_heartbeat_history

        with patch.dict(os.environ, {"UPTIME_KUMA_API_KEY": ""}):
            result = get_heartbeat_history(monitor_id=1)
            assert isinstance(result, dict)
            assert result.get("format") == "degraded"


class TestAddMonitor:
    def test_add_monitor_requires_api_key(self):
        from familiar.skills.uptime_kuma.skill import add_monitor

        with patch.dict(os.environ, {"UPTIME_KUMA_API_KEY": ""}):
            result = add_monitor(name="Test", url="http://example.com")
            assert isinstance(result, dict)
            assert result.get("format") == "degraded"

    def test_add_monitor_success(self):
        from familiar.skills.uptime_kuma.skill import add_monitor

        with patch.dict(os.environ, {"UPTIME_KUMA_API_KEY": "test-key"}):
            with patch(
                "familiar.skills.uptime_kuma.skill._api_post",
                return_value={"monitorID": 42},
            ):
                result = add_monitor(name="My Service", url="http://example.com")
                assert isinstance(result, dict)
                assert result.get("success") is True
                assert result.get("monitor_id") == 42


class TestPauseResumeMonitor:
    def test_pause_resume_monitor_success(self):
        from familiar.skills.uptime_kuma.skill import pause_monitor, resume_monitor

        with patch.dict(os.environ, {"UPTIME_KUMA_API_KEY": "test-key"}):
            with patch(
                "familiar.skills.uptime_kuma.skill._api_patch",
                return_value={},
            ):
                pause_result = pause_monitor(monitor_id=1)
                assert pause_result.get("success") is True
                assert pause_result.get("action") == "paused"

                resume_result = resume_monitor(monitor_id=1)
                assert resume_result.get("success") is True
                assert resume_result.get("action") == "resumed"


# ---------------------------------------------------------------------------
# Network tools tests
# ---------------------------------------------------------------------------


class TestScanSubnet:
    def test_scan_subnet_no_nmap(self):
        from familiar.skills.network.skill import scan_subnet

        with patch("shutil.which", return_value=None):
            result = scan_subnet("192.168.1.0/24")
            assert isinstance(result, dict)
            assert result.get("format") == "degraded"
            assert "nmap" in result.get("message", "").lower()

    def test_scan_subnet_parses_xml(self):
        from familiar.skills.network.skill import scan_subnet

        nmap_xml = """<?xml version="1.0"?>
<nmaprun>
  <host>
    <status state="up"/>
    <address addr="192.168.1.1" addrtype="ipv4"/>
    <hostnames><hostname name="router.local"/></hostnames>
    <ports>
      <port protocol="tcp" portid="80">
        <state state="open"/>
        <service name="http" product="nginx" version="1.20"/>
      </port>
    </ports>
  </host>
</nmaprun>"""

        with patch("shutil.which", return_value="/usr/bin/nmap"):
            with patch("familiar.skills.network.skill._run", return_value=(0, nmap_xml, "")):
                result = scan_subnet("192.168.1.0/24")
                assert isinstance(result, dict)
                assert result.get("hosts_up") == 1
                hosts = result.get("hosts", [])
                assert len(hosts) == 1
                assert len(hosts[0].get("open_ports", [])) == 1


class TestGetFirewallStatus:
    def test_get_firewall_status_no_tools(self):
        from familiar.skills.network.skill import get_firewall_status

        with patch("shutil.which", return_value=None):
            result = get_firewall_status()
            assert isinstance(result, dict)
            assert result["firewalld"].get("active") is False

    def test_get_firewall_status_with_firewalld(self):
        from familiar.skills.network.skill import get_firewall_status

        def _which(name):
            if name == "firewall-cmd":
                return "/usr/bin/firewall-cmd"
            return None

        zone_output = "public\n  interfaces: eth0\n"

        def _mock_run(cmd, timeout=60):
            return (0, zone_output, "")

        with patch("shutil.which", side_effect=_which):
            with patch("familiar.skills.network.skill._run", side_effect=_mock_run):
                result = get_firewall_status()
                assert isinstance(result, dict)
                assert "firewalld" in result
                assert result["firewalld"].get("active") is True
                assert result["firewalld"].get("zone_count", 0) >= 0


class TestDnsLookup:
    def test_dns_lookup_with_dig(self):
        from familiar.skills.network.skill import dns_lookup

        def _which(name):
            if name == "dig":
                return "/usr/bin/dig"
            return None

        # Make dnspython unavailable
        with patch.dict(
            "sys.modules", {"dns": None, "dns.resolver": None, "dns.reversename": None}
        ):
            with patch("shutil.which", side_effect=_which):
                with patch(
                    "familiar.skills.network.skill._run",
                    return_value=(0, "1.2.3.4\n", ""),
                ):
                    result = dns_lookup("example.com", "A")
                    assert isinstance(result, dict)
                    assert "records" in result
                    assert "1.2.3.4" in result["records"]


class TestTraceroute:
    def test_traceroute_no_tools(self):
        from familiar.skills.network.skill import traceroute

        with patch("shutil.which", return_value=None):
            result = traceroute("8.8.8.8")
            assert isinstance(result, dict)
            assert result.get("format") == "degraded"
            assert (
                "traceroute" in result.get("message", "").lower()
                or "not found" in result.get("message", "").lower()
            )
