import aitop.ui.components.overview as overview_module
from aitop.ui.components.overview import OverviewPanel


class _DisplayStub:
    pass


class _RecordingDisplay:
    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_sparkline_empty_values_returns_blank_width() -> None:
    panel = OverviewPanel(_DisplayStub())

    line = panel._sparkline([], width=12, max_value=100.0)

    assert len(line) == 12
    assert line == " " * 12


def test_sparkline_resamples_to_target_width() -> None:
    panel = OverviewPanel(_DisplayStub())
    values = [float(index) for index in range(100)]

    line = panel._sparkline(values, width=16, max_value=100.0)

    assert len(line) == 16
    assert all(char in " ▁▂▃▄▅▆▇█" for char in line)


def test_sparkline_adaptive_scaling_shows_small_trend_changes() -> None:
    panel = OverviewPanel(_DisplayStub())
    values = [45.0, 46.0, 47.0, 48.0, 49.0, 50.0, 51.0, 52.0]

    line = panel._sparkline(values, width=8, max_value=100.0)
    used_chars = {char for char in line if char != " "}

    assert len(line) == 8
    assert len(used_chars) > 1


def test_trend_arrow_points_up_down_and_flat() -> None:
    panel = OverviewPanel(_DisplayStub())

    assert panel._trend_arrow([1.0, 2.0, 3.0, 5.0, 7.0, 8.0]) == "↑"
    assert panel._trend_arrow([8.0, 7.0, 6.0, 4.0, 2.0, 1.0]) == "↓"
    assert panel._trend_arrow([10.0, 10.2, 10.1, 10.3, 10.1, 10.2]) == "→"


def test_render_history_overview_formats_lines_and_trends(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)
    history = {
        "cpu": [10.0, 20.0, 30.0, 40.0, 50.0, 60.0],
        "memory": [55.0, 54.0, 53.0, 52.0, 51.0, 50.0],
        "gpu": [33.0, 33.0, 33.0, 34.0, 33.0, 33.0],
    }

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)

    end_y = panel._render_history_overview(history, y=3, x=2, width=60)
    rendered_lines = [entry[2] for entry in display.calls]

    assert end_y == 8
    assert rendered_lines[0] == "Recent Activity (latest)"
    assert rendered_lines[1].startswith("CPU [")
    assert "]  60.0% ↑" in rendered_lines[1]
    assert rendered_lines[2].startswith("MEM [")
    assert "]  50.0% ↓" in rendered_lines[2]
    assert rendered_lines[3].startswith("GPU [")
    assert "]  33.0% →" in rendered_lines[3]


def test_render_job_overview_shows_jobs_and_stragglers(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)
    processes = [
        {
            "pid": 1,
            "job_id": "run-exp-a",
            "is_distributed_job": True,
            "is_job_straggler": False,
            "cpu_percent": 82.0,
            "gpu_util": 93.0,
        },
        {
            "pid": 2,
            "job_id": "run-exp-a",
            "is_distributed_job": True,
            "is_job_straggler": True,
            "cpu_percent": 3.0,
            "gpu_util": 1.0,
        },
        {
            "pid": 3,
            "job_id": "ppid-12-train.py",
            "is_distributed_job": False,
            "is_job_straggler": False,
            "cpu_percent": 45.0,
            "gpu_util": 20.0,
        },
    ]

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)

    end_y = panel._render_job_overview(processes, y=2, x=1, width=80, max_jobs=3)
    rendered_lines = [entry[2] for entry in display.calls]

    assert end_y == 6
    assert rendered_lines[0] == "AI Jobs"
    assert any(
        "run-exp-a | workers:2 | stragglers:1 | dist" in line for line in rendered_lines
    )
    assert any(
        "ppid-12-train.py | workers:1 | stragglers:0 | single" in line
        for line in rendered_lines
    )


def test_render_process_overview_uses_shared_gpu_metrics(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)

    processes = [
        {
            "pid": 123,
            "name": "python train.py",
            "cpu_percent": 45.0,
            "memory_percent": 10.0,
        }
    ]
    gpus = [
        overview_module.GPUInfo(
            index=0,
            name="NVIDIA A100",
            utilization=95.0,
            memory_used=30000.0,
            memory_total=40000.0,
            temperature=60.0,
            power_draw=250.0,
            power_limit=300.0,
            processes=[
                {"pid": 123, "name": "python", "memory": 20000.0, "sm_util": 41.0}
            ],
        )
    ]

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)

    panel._render_process_overview(
        processes=processes,
        gpus=gpus,
        y=0,
        x=0,
        width=120,
        max_processes=1,
        selected_index=0,
    )

    rendered_lines = [entry[2] for entry in display.calls]
    process_line = next(line for line in rendered_lines if "python train.py" in line)
    assert "   41.0 " in process_line
    assert "   50.0 " in process_line
    assert "GPU0:41%" in process_line


def test_render_process_overview_sorts_devices_by_utilization(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)
    processes = [
        {
            "pid": 321,
            "name": "python train.py",
            "cpu_percent": 45.0,
            "memory_percent": 7.0,
        }
    ]
    gpu = overview_module.GPUInfo(
        index=0,
        name="NVIDIA A100",
        utilization=95.0,
        memory_used=30000.0,
        memory_total=40000.0,
        temperature=60.0,
        power_draw=250.0,
        power_limit=300.0,
        processes=[{"pid": 321, "name": "python", "memory": 12000.0, "sm_util": 41.0}],
    )
    npu = overview_module.GPUInfo(
        index=0,
        name="Intel AI Boost",
        utilization=85.0,
        memory_used=0.0,
        memory_total=0.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[{"pid": 321, "name": "python", "memory": 1.0, "gpu_util": 60.0}],
    )

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)

    panel._render_process_overview(
        processes=processes,
        gpus=[gpu, npu],
        y=0,
        x=0,
        width=160,
        max_processes=1,
        selected_index=0,
        accelerator_info=[(gpu, "nvidia"), (npu, "intel_npu")],
    )

    rendered_lines = [entry[2] for entry in display.calls]
    process_line = next(line for line in rendered_lines if "python train.py" in line)
    assert "NPU0:60%,GPU0:41%" in process_line


def test_render_process_overview_uses_global_gpu_ids_for_mixed_gpu_vendors(
    monkeypatch,
) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)
    processes = [
        {
            "pid": 322,
            "name": "python train.py",
            "cpu_percent": 40.0,
            "memory_percent": 8.0,
        }
    ]
    nvidia_gpu = overview_module.GPUInfo(
        index=0,
        name="NVIDIA A100",
        utilization=95.0,
        memory_used=30000.0,
        memory_total=40000.0,
        temperature=60.0,
        power_draw=250.0,
        power_limit=300.0,
        processes=[{"pid": 322, "name": "python", "memory": 12000.0, "sm_util": 41.0}],
    )
    amd_gpu = overview_module.GPUInfo(
        index=0,
        name="AMD MI300",
        utilization=85.0,
        memory_used=20000.0,
        memory_total=64000.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[{"pid": 322, "name": "python", "memory": 8000.0, "gpu_util": 60.0}],
    )

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)

    panel._render_process_overview(
        processes=processes,
        gpus=[nvidia_gpu, amd_gpu],
        y=0,
        x=0,
        width=160,
        max_processes=1,
        selected_index=0,
        accelerator_info=[(nvidia_gpu, "nvidia"), (amd_gpu, "amd")],
    )

    rendered_lines = [entry[2] for entry in display.calls]
    process_line = next(line for line in rendered_lines if "python train.py" in line)
    assert "GPU1:60%,GPU0:41%" in process_line


def test_render_process_overview_does_not_mutate_input_process_order(
    monkeypatch,
) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)
    processes = [
        {"pid": 10, "name": "low", "cpu_percent": 10.0, "memory_percent": 1.0},
        {"pid": 20, "name": "high", "cpu_percent": 90.0, "memory_percent": 2.0},
    ]
    original_order = [proc["pid"] for proc in processes]

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)

    panel._render_process_overview(
        processes=processes,
        gpus=[],
        y=0,
        x=0,
        width=120,
        max_processes=2,
        selected_index=0,
    )

    assert [proc["pid"] for proc in processes] == original_order


def test_render_gpu_overview_uses_npu_labels(monkeypatch) -> None:
    display = _RecordingDisplay()
    display.create_bar = lambda _value, width: (
        "#" * min(max(width, 0), 5),
        0,
    )  # type: ignore[attr-defined]
    display.get_color = lambda _value: 0  # type: ignore[attr-defined]
    panel = OverviewPanel(display)
    npu = overview_module.GPUInfo(
        index=0,
        name="Intel AI Boost (0x7d1d)",
        utilization=30.0,
        memory_used=128.0,
        memory_total=512.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[],
        driver_version="1.2.3",
        memory_model="shared",
    )

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)
    monkeypatch.setattr(overview_module.curses, "A_BOLD", 0)

    panel._render_gpu_overview(npu, vendor="intel_npu", y=0, x=0, width=120)

    rendered_lines = [entry[2] for entry in display.calls]
    assert any("NPU Status" in line for line in rendered_lines)
    assert any("NPU 0: Intel AI Boost (0x7d1d)" in line for line in rendered_lines)
    assert any("MEM~" in line for line in rendered_lines)


def test_render_gpu_overview_can_show_global_gpu_index(monkeypatch) -> None:
    display = _RecordingDisplay()
    display.create_bar = lambda _value, width: (
        "#" * min(max(width, 0), 5),
        0,
    )  # type: ignore[attr-defined]
    display.get_color = lambda _value: 0  # type: ignore[attr-defined]
    panel = OverviewPanel(display)
    gpu = overview_module.GPUInfo(
        index=0,
        name="AMD MI300",
        utilization=20.0,
        memory_used=128.0,
        memory_total=512.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[],
    )

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)
    monkeypatch.setattr(overview_module.curses, "A_BOLD", 0)

    panel._render_gpu_overview(
        gpu,
        vendor="amd",
        y=0,
        x=0,
        width=120,
        device_display_index=1,
    )

    rendered_lines = [entry[2] for entry in display.calls]
    assert any("GPU 1: AMD MI300" in line for line in rendered_lines)


def test_render_gpu_overview_handles_missing_temperature_and_memory_total(
    monkeypatch,
) -> None:
    display = _RecordingDisplay()
    display.create_bar = lambda _value, width: (
        "#" * min(max(width, 0), 5),
        0,
    )  # type: ignore[attr-defined]
    display.get_color = lambda _value: 0  # type: ignore[attr-defined]
    panel = OverviewPanel(display)
    gpu = overview_module.GPUInfo(
        index=0,
        name="NVIDIA A100",
        utilization=0.0,
        memory_used=0.0,
        memory_total=0.0,
        temperature=0.0,
        power_draw=0.0,
        power_limit=0.0,
        processes=[],
    )

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)
    monkeypatch.setattr(overview_module.curses, "A_BOLD", 0)

    panel._render_gpu_overview(gpu, vendor="nvidia", y=0, x=0, width=120)

    rendered_lines = [entry[2] for entry in display.calls]
    assert any("VRAM" in line for line in rendered_lines)
    assert any("N/A" in line for line in rendered_lines)
    assert not any(line.startswith("Temp") for line in rendered_lines)


def test_render_process_overview_defaults_status_when_missing(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)
    processes = [
        {
            "pid": 42,
            "name": "idle",
            "cpu_percent": 0.0,
            "memory_percent": 1.0,
        }
    ]

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)
    monkeypatch.setattr(overview_module.curses, "A_BOLD", 0)

    panel._render_process_overview(
        processes=processes,
        gpus=[],
        y=0,
        x=0,
        width=120,
        max_processes=1,
        selected_index=0,
    )

    rendered_lines = [entry[2] for entry in display.calls]
    process_line = next(line for line in rendered_lines if "idle" in line)
    assert "sleeping" in process_line


def test_render_process_overview_uses_canonical_running_status(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)
    processes = [
        {
            "pid": 43,
            "name": "gpu_worker",
            "cpu_percent": 0.0,
            "memory_percent": 1.0,
            "status": "running",
            "raw_status": "sleeping",
        }
    ]

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)
    monkeypatch.setattr(overview_module.curses, "A_BOLD", 0)

    panel._render_process_overview(
        processes=processes,
        gpus=[],
        y=0,
        x=0,
        width=120,
        max_processes=1,
        selected_index=0,
    )

    rendered_lines = [entry[2] for entry in display.calls]
    process_line = next(line for line in rendered_lines if "gpu_worker" in line)
    assert "running" in process_line


def test_render_dependency_warnings_renders_block(monkeypatch) -> None:
    display = _RecordingDisplay()
    panel = OverviewPanel(display)
    warnings = [
        "Intel GPU detected but intel_gpu_top is missing (install intel-gpu-tools)."
    ]

    monkeypatch.setattr(overview_module.curses, "color_pair", lambda _index: 0)
    monkeypatch.setattr(overview_module.curses, "A_BOLD", 0)

    end_y = panel._render_dependency_warnings(
        dependency_warnings=warnings,
        y=3,
        x=2,
        width=80,
    )

    rendered_lines = [entry[2] for entry in display.calls]
    assert rendered_lines[0] == "Dependencies"
    assert "intel_gpu_top is missing" in rendered_lines[1]
    assert end_y == 6
