from aitop.core.gpu.base import GPUInfo
from aitop.ui.components.process_panel import ProcessPanel


class _DisplayStub:
    height = 40
    width = 140

    def safe_addstr(self, _y: int, _x: int, _text: str, _attr: int = 0) -> None:
        return


class _RecordingDisplay:
    height = 40
    width = 180

    def __init__(self) -> None:
        self.calls: list[tuple[int, int, str, int]] = []

    def safe_addstr(self, y: int, x: int, text: str, attr: int = 0) -> None:
        self.calls.append((y, x, text, attr))


def test_get_selected_process_tree_mode_respects_hierarchy_order() -> None:
    panel = ProcessPanel(_DisplayStub())
    processes = [
        {"pid": 10, "ppid": 1, "name": "parent", "cpu_percent": 40.0},
        {"pid": 11, "ppid": 10, "name": "child", "cpu_percent": 10.0},
        {"pid": 20, "ppid": 1, "name": "other-root", "cpu_percent": 50.0},
    ]

    # Tree order with CPU sort: root(20), root(10), child(11)
    selected = panel.get_selected_process(
        processes,
        selected_index=2,
        sort_by="cpu_percent",
        sort_reverse=True,
        tree_mode=True,
    )

    assert selected is not None
    assert selected["pid"] == 11


def test_get_selected_process_flat_mode_stays_sort_based() -> None:
    panel = ProcessPanel(_DisplayStub())
    processes = [
        {"pid": 10, "ppid": 1, "name": "parent", "cpu_percent": 40.0},
        {"pid": 11, "ppid": 10, "name": "child", "cpu_percent": 10.0},
        {"pid": 20, "ppid": 1, "name": "other-root", "cpu_percent": 50.0},
    ]

    # Flat order with CPU sort: 20, 10, 11
    selected = panel.get_selected_process(
        processes,
        selected_index=1,
        sort_by="cpu_percent",
        sort_reverse=True,
        tree_mode=False,
    )

    assert selected is not None
    assert selected["pid"] == 10


def test_tree_mode_handles_parent_cycles_without_dropping_processes() -> None:
    panel = ProcessPanel(_DisplayStub())
    processes = [
        {"pid": 1, "ppid": 2, "name": "cycle-a", "cpu_percent": 30.0},
        {"pid": 2, "ppid": 1, "name": "cycle-b", "cpu_percent": 20.0},
        {"pid": 3, "ppid": 999, "name": "root", "cpu_percent": 40.0},
    ]

    selected_pids = []
    for idx in range(4):
        selected = panel.get_selected_process(
            processes,
            selected_index=idx,
            sort_by="cpu_percent",
            sort_reverse=True,
            tree_mode=True,
        )
        if selected:
            selected_pids.append(selected["pid"])

    assert set(selected_pids) == {1, 2, 3}
    assert len(selected_pids) == 3


def test_tree_mode_handles_invalid_parent_types_as_roots() -> None:
    panel = ProcessPanel(_DisplayStub())
    processes = [
        {"pid": 10, "ppid": "bad", "name": "broken-parent", "cpu_percent": 12.0},
        {"pid": 11, "ppid": None, "name": "no-parent", "cpu_percent": 15.0},
    ]

    first = panel.get_selected_process(
        processes,
        selected_index=0,
        sort_by="cpu_percent",
        sort_reverse=True,
        tree_mode=True,
    )
    second = panel.get_selected_process(
        processes,
        selected_index=1,
        sort_by="cpu_percent",
        sort_reverse=True,
        tree_mode=True,
    )

    assert first is not None
    assert second is not None
    assert {first["pid"], second["pid"]} == {10, 11}


def test_calculate_gpu_metrics_uses_sm_util_when_available() -> None:
    panel = ProcessPanel(_DisplayStub())
    process = {"pid": 55}
    gpus = [
        GPUInfo(
            index=0,
            name="NVIDIA H100",
            utilization=90.0,
            memory_used=0.0,
            memory_total=80000.0,
            temperature=0.0,
            power_draw=0.0,
            power_limit=0.0,
            processes=[{"pid": 55, "memory": 16000.0, "sm_util": 37.0}],
        )
    ]

    gpu_percent, vram_percent = panel._calculate_gpu_metrics(process, gpus)

    assert gpu_percent == 37.0
    assert round(vram_percent, 1) == 20.0


def test_render_includes_device_summary_and_device_column(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = ProcessPanel(display)
    processes = [
        {
            "pid": 9001,
            "name": "python train.py",
            "cpu_percent": 30.0,
            "memory_percent": 5.0,
            "status": "running",
        }
    ]
    gpu = GPUInfo(
        index=0,
        name="NVIDIA H100",
        utilization=90.0,
        memory_used=20000.0,
        memory_total=80000.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[{"pid": 9001, "memory": 12000.0, "sm_util": 42.0}],
    )
    npu = GPUInfo(
        index=0,
        name="Intel AI Boost",
        utilization=70.0,
        memory_used=0.0,
        memory_total=0.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[{"pid": 9001, "memory": 1.0, "gpu_util": 65.0}],
    )

    monkeypatch.setattr(
        "aitop.ui.components.process_panel.curses.color_pair", lambda _i: 0
    )
    monkeypatch.setattr("aitop.ui.components.process_panel.curses.A_BOLD", 0)
    monkeypatch.setattr("aitop.ui.components.process_panel.curses.A_REVERSE", 0)

    panel.render(
        processes=processes,
        gpus=[gpu, npu],
        start_y=0,
        indent=0,
        sort_by="cpu_percent",
        sort_reverse=True,
        scroll_position=0,
        selected_index=0,
        tree_mode=False,
        accelerator_info=[(gpu, "nvidia"), (npu, "intel_npu")],
    )

    lines = [entry[2] for entry in display.calls]
    assert any(line.startswith("By Device:") for line in lines)
    assert any("Device" in line and "ACC%" in line for line in lines)
    assert any("NPU0:65%,GPU0:42%" in line for line in lines)


def test_render_uses_global_gpu_ids_for_mixed_gpu_vendors(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = ProcessPanel(display)
    processes = [
        {
            "pid": 9010,
            "name": "python train.py",
            "cpu_percent": 35.0,
            "memory_percent": 6.0,
            "status": "running",
        }
    ]
    nvidia_gpu = GPUInfo(
        index=0,
        name="NVIDIA H100",
        utilization=85.0,
        memory_used=24000.0,
        memory_total=80000.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[{"pid": 9010, "memory": 12000.0, "sm_util": 42.0}],
    )
    amd_gpu = GPUInfo(
        index=0,
        name="AMD MI300",
        utilization=80.0,
        memory_used=12000.0,
        memory_total=64000.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[{"pid": 9010, "memory": 8000.0, "gpu_util": 65.0}],
    )

    monkeypatch.setattr(
        "aitop.ui.components.process_panel.curses.color_pair", lambda _i: 0
    )
    monkeypatch.setattr("aitop.ui.components.process_panel.curses.A_BOLD", 0)
    monkeypatch.setattr("aitop.ui.components.process_panel.curses.A_REVERSE", 0)

    panel.render(
        processes=processes,
        gpus=[nvidia_gpu, amd_gpu],
        start_y=0,
        indent=0,
        sort_by="cpu_percent",
        sort_reverse=True,
        scroll_position=0,
        selected_index=0,
        tree_mode=False,
        accelerator_info=[(nvidia_gpu, "nvidia"), (amd_gpu, "amd")],
    )

    lines = [entry[2] for entry in display.calls]
    assert any("GPU1:65%,GPU0:42%" in line for line in lines)


def test_render_process_line_handles_missing_pid(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = ProcessPanel(display)
    headers, _device_limit = panel._build_headers(display.width, total_accelerators=0)

    monkeypatch.setattr(
        "aitop.ui.components.process_panel.curses.color_pair", lambda _i: 0
    )
    monkeypatch.setattr("aitop.ui.components.process_panel.curses.A_REVERSE", 0)

    process = {
        "name": "unknown",
        "cpu_percent": 0.0,
        "memory_percent": 0.0,
        "status": "",
    }

    panel._render_process_line(
        process=process,
        gpu_util=0.0,
        vram_percent=0.0,
        device_display="CPU",
        headers=headers,
        y=0,
        indent=0,
        selected=False,
        tree_depth=0,
    )

    assert display.calls
    assert "unknown" in display.calls[0][2]


def test_render_process_line_clamps_accelerator_percentages(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = ProcessPanel(display)
    headers, _device_limit = panel._build_headers(display.width, total_accelerators=1)

    monkeypatch.setattr(
        "aitop.ui.components.process_panel.curses.color_pair", lambda _i: 0
    )
    monkeypatch.setattr("aitop.ui.components.process_panel.curses.A_REVERSE", 0)

    process = {
        "pid": 42,
        "name": "trainer",
        "cpu_percent": 12.0,
        "memory_percent": 3.0,
        "status": "running",
    }

    panel._render_process_line(
        process=process,
        gpu_util=1450.0,
        vram_percent=350.0,
        device_display="GPU0",
        headers=headers,
        y=0,
        indent=0,
        selected=False,
        tree_depth=0,
    )

    assert display.calls
    line = display.calls[0][2]
    assert " 100.0 " in line
    assert line.count("100.0") >= 2
