import io
from pathlib import Path
from types import SimpleNamespace

import aitop.core.system.cpu as cpu_module
import aitop.core.system.memory as memory_module
from aitop.core.system.cpu import CPUStats
from aitop.core.system.memory import MemoryStats, SystemMemoryMonitor


def test_get_memory_stats_returns_safe_defaults_on_failure(
    monkeypatch: object,
) -> None:
    def _raise_runtime_error() -> object:
        raise RuntimeError("memory unavailable")

    monkeypatch.setattr(memory_module.psutil, "virtual_memory", _raise_runtime_error)

    stats = SystemMemoryMonitor.get_memory_stats()

    assert stats == MemoryStats(
        total=0,
        used=0,
        free=0,
        percent=0.0,
        swap_total=0,
        swap_used=0,
        swap_free=0,
        swap_percent=0.0,
    )


def test_get_memory_by_type_uses_zero_for_missing_optional_fields(
    monkeypatch: object,
) -> None:
    vm = SimpleNamespace(available=10_000)

    monkeypatch.setattr(memory_module.psutil, "virtual_memory", lambda: vm)

    memory_types = SystemMemoryMonitor.get_memory_by_type()

    assert memory_types == {
        "available": 10_000,
        "buffers": 0,
        "cached": 0,
        "shared": 0,
        "slab": 0,
    }


def test_get_formatted_stats_converts_bytes_to_gb(monkeypatch: object) -> None:
    monitor = SystemMemoryMonitor()
    gib = 1024**3

    monkeypatch.setattr(
        monitor,
        "get_memory_stats",
        lambda: MemoryStats(
            total=16 * gib,
            used=8 * gib,
            free=8 * gib,
            percent=50.0,
            swap_total=4 * gib,
            swap_used=1 * gib,
            swap_free=3 * gib,
            swap_percent=25.0,
        ),
    )
    monkeypatch.setattr(
        monitor,
        "get_memory_by_type",
        lambda: {"available": 9 * gib, "cached": 2 * gib},
    )

    formatted = monitor.get_formatted_stats()

    assert formatted["ram"]["total_gb"] == 16.0
    assert formatted["ram"]["used_gb"] == 8.0
    assert formatted["ram"]["details"]["available"] == 9.0
    assert formatted["ram"]["details"]["cached"] == 2.0
    assert formatted["swap"]["used_gb"] == 1.0
    assert formatted["swap"]["percent"] == 25.0


def test_cpu_stats_uses_safe_fallbacks_for_partial_platform_failures(
    monkeypatch: object,
) -> None:
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_percent",
        lambda interval=None, percpu=True: [20.0, 80.0],
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_freq",
        lambda percpu=False: SimpleNamespace(current=None, min=None, max=None),
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "sensors_temperatures",
        lambda: (_ for _ in ()).throw(AttributeError("not supported")),
    )
    monkeypatch.setattr(cpu_module.psutil, "cpu_count", lambda logical=True: None)
    monkeypatch.setattr(cpu_module.psutil, "getloadavg", lambda: (1.0, 2.0))
    monkeypatch.setattr(cpu_module.platform, "processor", lambda: "fallback-cpu")
    monkeypatch.setattr(
        CPUStats,
        "_collect_linux_cpufreq_metrics",
        staticmethod(lambda: {}),
    )
    monkeypatch.setattr(
        CPUStats,
        "_pick_temperature_from_linux_hwmon",
        staticmethod(lambda vendor: (0.0, "", -1000)),
    )
    monkeypatch.setattr(
        CPUStats,
        "_pick_temperature_from_linux_thermal_zone",
        staticmethod(lambda vendor: (0.0, "", -1000)),
    )
    monkeypatch.setattr(
        "builtins.open",
        lambda *args, **kwargs: (_ for _ in ()).throw(IOError("no cpuinfo")),
    )

    stats = CPUStats.get_stats()

    assert stats.total_percent == 50.0
    assert stats.core_percents == [20.0, 80.0]
    assert stats.current_freq == 0.0
    assert stats.min_freq == 0.0
    assert stats.max_freq == 0.0
    assert stats.temperature == 0.0
    assert stats.load_1min == 0.0
    assert stats.load_5min == 0.0
    assert stats.load_15min == 0.0
    assert stats.vendor == "Unknown"
    assert stats.model == "fallback-cpu"


def test_cpu_stats_parses_proc_cpuinfo_and_normalizes_loads(
    monkeypatch: object,
) -> None:
    cpuinfo = (
        "vendor_id\t: AuthenticAMD\n"
        "model name\t: AMD Ryzen Test\n"
        "cpu MHz\t\t: 3900.000\n"
    )

    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_percent",
        lambda interval=None, percpu=True: [30.0, 50.0],
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_freq",
        lambda percpu=False: SimpleNamespace(current=4000.0, min=2200.0, max=5100.0),
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "sensors_temperatures",
        lambda: {"k10temp": [SimpleNamespace(current=67.5)]},
    )
    monkeypatch.setattr(
        CPUStats,
        "_collect_linux_cpufreq_metrics",
        staticmethod(lambda: {}),
    )
    monkeypatch.setattr(
        CPUStats,
        "_pick_temperature_from_linux_hwmon",
        staticmethod(lambda vendor: (0.0, "", -1000)),
    )
    monkeypatch.setattr(
        CPUStats,
        "_pick_temperature_from_linux_thermal_zone",
        staticmethod(lambda vendor: (0.0, "", -1000)),
    )
    monkeypatch.setattr(cpu_module.psutil, "cpu_count", lambda logical=True: 4)
    monkeypatch.setattr(cpu_module.psutil, "getloadavg", lambda: (2.0, 4.0, 6.0))
    monkeypatch.setattr(cpu_module.platform, "processor", lambda: "unused-fallback")
    monkeypatch.setattr("builtins.open", lambda *args, **kwargs: io.StringIO(cpuinfo))

    stats = CPUStats.get_stats()

    assert stats.vendor == "AuthenticAMD"
    assert stats.model == "AMD Ryzen Test"
    assert stats.total_percent == 40.0
    assert stats.temperature == 67.5
    assert stats.load_1min == 0.5
    assert stats.load_5min == 1.0
    assert stats.load_15min == 1.5


def test_cpu_stats_maps_arm_implementer_and_hardware_model(
    monkeypatch: object,
) -> None:
    cpuinfo = (
        "CPU implementer\t: 0x51\n"
        "Hardware\t: Qualcomm Technologies, Inc Test SoC\n"
        "Processor\t: AArch64 Processor rev 2 (aarch64)\n"
    )

    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_percent",
        lambda interval=None, percpu=True: [25.0, 35.0],
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_freq",
        lambda percpu=False: SimpleNamespace(current=2200.0, min=800.0, max=3000.0),
    )
    monkeypatch.setattr(cpu_module.psutil, "sensors_temperatures", lambda: {})
    monkeypatch.setattr(cpu_module.psutil, "cpu_count", lambda logical=True: 8)
    monkeypatch.setattr(cpu_module.psutil, "getloadavg", lambda: (4.0, 8.0, 12.0))
    monkeypatch.setattr(cpu_module.platform, "processor", lambda: "unused")
    monkeypatch.setattr("builtins.open", lambda *args, **kwargs: io.StringIO(cpuinfo))

    stats = CPUStats.get_stats()

    assert stats.vendor == "Qualcomm"
    assert stats.model == "Qualcomm Technologies, Inc Test SoC"


def test_cpu_stats_uses_fallback_model_when_processor_field_is_numeric(
    monkeypatch: object,
) -> None:
    cpuinfo = "CPU implementer\t: 0xAB\n" "Processor\t: 0\n"

    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_percent",
        lambda interval=None, percpu=True: [10.0],
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_freq",
        lambda percpu=False: SimpleNamespace(current=1600.0, min=800.0, max=2000.0),
    )
    monkeypatch.setattr(cpu_module.psutil, "sensors_temperatures", lambda: {})
    monkeypatch.setattr(cpu_module.psutil, "cpu_count", lambda logical=True: 2)
    monkeypatch.setattr(cpu_module.psutil, "getloadavg", lambda: (0.5, 1.0, 1.5))
    monkeypatch.setattr(cpu_module.platform, "processor", lambda: "fallback-model")
    monkeypatch.setattr("builtins.open", lambda *args, **kwargs: io.StringIO(cpuinfo))

    stats = CPUStats.get_stats()

    assert stats.vendor == "Unknown (0xAB)"
    assert stats.model == "fallback-model"


def test_cpu_stats_prefers_linux_cpufreq_band_over_psutil_defaults(
    monkeypatch: object,
) -> None:
    monkeypatch.setattr(cpu_module.platform, "system", lambda: "Linux")
    monkeypatch.setattr(cpu_module.platform, "processor", lambda: "fallback-model")
    monkeypatch.setattr(
        CPUStats,
        "_collect_linux_identity",
        staticmethod(lambda fallback_model: ("AuthenticAMD", "AMD Ryzen APU Test")),
    )
    monkeypatch.setattr(
        CPUStats,
        "_collect_linux_cpufreq_metrics",
        staticmethod(
            lambda: {
                "current": 2750.0,
                "effective_min": 400.0,
                "effective_max": 4600.0,
                "policy_min": 400.0,
                "policy_max": 3800.0,
                "hardware_min": 400.0,
                "hardware_max": 4600.0,
                "source": "linux-cpufreq,driver=amd-pstate,boost=on",
            }
        ),
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_percent",
        lambda interval=None, percpu=True: [15.0, 35.0],
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_freq",
        lambda percpu=False: SimpleNamespace(current=1200.0, min=800.0, max=2200.0),
    )
    monkeypatch.setattr(cpu_module.psutil, "sensors_temperatures", lambda: {})
    monkeypatch.setattr(
        CPUStats,
        "_pick_temperature_from_linux_hwmon",
        staticmethod(lambda vendor: (0.0, "", -1000)),
    )
    monkeypatch.setattr(
        CPUStats,
        "_pick_temperature_from_linux_thermal_zone",
        staticmethod(lambda vendor: (0.0, "", -1000)),
    )
    monkeypatch.setattr(cpu_module.psutil, "cpu_count", lambda logical=True: 8)
    monkeypatch.setattr(cpu_module.psutil, "getloadavg", lambda: (2.0, 4.0, 6.0))

    stats = CPUStats.get_stats()

    assert stats.vendor == "AuthenticAMD"
    assert stats.model == "AMD Ryzen APU Test"
    assert stats.current_freq == 2750.0
    assert stats.min_freq == 400.0
    assert stats.max_freq == 4600.0
    assert stats.policy_min_freq == 400.0
    assert stats.policy_max_freq == 3800.0
    assert stats.hardware_min_freq == 400.0
    assert stats.hardware_max_freq == 4600.0
    assert "amd-pstate" in stats.frequency_source


def test_cpu_stats_temperature_selection_prefers_amd_tdie_label() -> None:
    temps = {
        "k10temp": [
            SimpleNamespace(current=71.5, label="Tctl"),
            SimpleNamespace(current=65.0, label="Tdie"),
        ],
        "acpitz": [SimpleNamespace(current=45.0, label="temp1")],
    }

    temp, source, _score = CPUStats._pick_temperature_from_psutil(temps, "AMD")

    assert temp == 65.0
    assert "k10temp" in source
    assert "Tdie" in source


def test_cpu_stats_uses_linux_hwmon_when_psutil_temperatures_are_empty(
    monkeypatch: object,
) -> None:
    monkeypatch.setattr(cpu_module.platform, "system", lambda: "Linux")
    monkeypatch.setattr(cpu_module.platform, "processor", lambda: "fallback-model")
    monkeypatch.setattr(
        CPUStats,
        "_collect_linux_identity",
        staticmethod(lambda fallback_model: ("Intel", "Intel Core Test")),
    )
    monkeypatch.setattr(
        CPUStats,
        "_collect_linux_cpufreq_metrics",
        staticmethod(lambda: {}),
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_percent",
        lambda interval=None, percpu=True: [20.0],
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_freq",
        lambda percpu=False: SimpleNamespace(current=2000.0, min=1000.0, max=3000.0),
    )
    monkeypatch.setattr(cpu_module.psutil, "sensors_temperatures", lambda: {})
    monkeypatch.setattr(
        CPUStats,
        "_pick_temperature_from_linux_hwmon",
        staticmethod(lambda vendor: (61.5, "hwmon:coretemp/Package id 0", 150)),
    )
    monkeypatch.setattr(
        CPUStats,
        "_pick_temperature_from_linux_thermal_zone",
        staticmethod(lambda vendor: (59.0, "thermal_zone:x86_pkg_temp", 130)),
    )
    monkeypatch.setattr(cpu_module.psutil, "cpu_count", lambda logical=True: 4)
    monkeypatch.setattr(cpu_module.psutil, "getloadavg", lambda: (1.0, 1.5, 2.0))

    stats = CPUStats.get_stats()

    assert stats.temperature == 61.5
    assert stats.temperature_source == "hwmon:coretemp/Package id 0"


def test_collect_darwin_identity_defaults_to_apple_on_arm() -> None:
    original_system = cpu_module.platform.system
    original_machine = cpu_module.platform.machine
    original_reader = CPUStats._read_sysctl_value
    try:
        cpu_module.platform.system = lambda: "Darwin"  # type: ignore[assignment]
        cpu_module.platform.machine = lambda: "arm64"  # type: ignore[assignment]
        CPUStats._read_sysctl_value = staticmethod(lambda key: "")
        vendor, model = CPUStats._collect_darwin_identity("fallback-model")
    finally:
        cpu_module.platform.system = original_system  # type: ignore[assignment]
        cpu_module.platform.machine = original_machine  # type: ignore[assignment]
        CPUStats._read_sysctl_value = original_reader

    assert vendor == "Apple"
    assert model == "fallback-model"


def test_parse_cpu_list_count_handles_ranges_and_singletons() -> None:
    count = CPUStats._parse_cpu_list_count("0-3,6,8-9")
    assert count == 7


def test_read_khz_file_mhz_handles_khz_and_mhz_units(tmp_path: Path) -> None:
    khz_file = tmp_path / "scaling_max_freq"
    khz_file.write_text("2151000\n", encoding="utf-8")
    assert CPUStats._read_khz_file_mhz(khz_file) == 2151.0

    mhz_quirk_file = tmp_path / "amd_pstate_max_freq"
    mhz_quirk_file.write_text("2151\n", encoding="utf-8")
    assert CPUStats._read_khz_file_mhz(mhz_quirk_file) == 2151.0


def test_cpu_stats_marks_darwin_psutil_frequency_as_nominal(
    monkeypatch: object,
) -> None:
    monkeypatch.setattr(cpu_module.platform, "system", lambda: "Darwin")
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_percent",
        lambda interval=None, percpu=True: [12.0, 18.0],
    )
    monkeypatch.setattr(
        cpu_module.psutil,
        "cpu_freq",
        lambda percpu=False: SimpleNamespace(current=3200.0, min=0.0, max=0.0),
    )
    monkeypatch.setattr(cpu_module.psutil, "sensors_temperatures", lambda: {})
    monkeypatch.setattr(cpu_module.psutil, "cpu_count", lambda logical=True: 8)
    monkeypatch.setattr(cpu_module.psutil, "getloadavg", lambda: (1.0, 2.0, 3.0))
    monkeypatch.setattr(
        CPUStats,
        "_collect_darwin_freq_metrics",
        staticmethod(lambda: {}),
    )
    monkeypatch.setattr(
        CPUStats,
        "_collect_identity",
        staticmethod(lambda fallback_model: ("Apple", "Apple M-Test")),
    )

    stats = CPUStats.get_stats()

    assert stats.current_freq == 3200.0
    assert stats.frequency_source == "psutil-nominal"
