"""Tests for the ProgressReporter system.

Validates:
- ProgressEvent construction and fields
- RichProgressReporter rendering
- LogProgressReporter output
- NullProgressReporter collection
- Customer message mapping (no internal names leak)
- All MESSAGES entries are free of implementation details
"""

from __future__ import annotations

import logging

import pytest

from llmhosts.progress import (
    MESSAGES,
    LogProgressReporter,
    NullProgressReporter,
    ProgressEvent,
    ProgressPhase,
    RichProgressReporter,
    customer_message,
)

# ---------------------------------------------------------------------------
# ProgressEvent
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestProgressEvent:
    """ProgressEvent dataclass tests."""

    def test_basic_construction(self) -> None:
        event = ProgressEvent(
            phase=ProgressPhase.DETECT_HARDWARE,
            message="Checking hardware...",
        )
        assert event.phase == ProgressPhase.DETECT_HARDWARE
        assert event.message == "Checking hardware..."
        assert event.progress_pct is None
        assert event.detail is None

    def test_with_progress(self) -> None:
        event = ProgressEvent(
            phase=ProgressPhase.DOWNLOAD_MODEL,
            message="Downloading AI model... 50%",
            progress_pct=50.0,
        )
        assert event.progress_pct == 50.0

    def test_with_detail(self) -> None:
        event = ProgressEvent(
            phase=ProgressPhase.SETUP_ENGINE,
            message="Setting up inference engine...",
            detail="internal debug info",
        )
        assert event.detail == "internal debug info"

    def test_frozen(self) -> None:
        """ProgressEvent should be immutable."""
        event = ProgressEvent(phase=ProgressPhase.READY, message="Ready")
        with pytest.raises(AttributeError):
            event.message = "changed"  # type: ignore[misc]


# ---------------------------------------------------------------------------
# ProgressPhase enum
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestProgressPhase:
    """ProgressPhase enum tests."""

    def test_all_phases_exist(self) -> None:
        expected = {"DETECT_HARDWARE", "SETUP_ENGINE", "DOWNLOAD_MODEL", "START_PROXY", "CONNECT", "READY"}
        actual = {p.name for p in ProgressPhase}
        assert actual == expected


# ---------------------------------------------------------------------------
# Customer message mapping
# ---------------------------------------------------------------------------

# Internal names that must NEVER appear in customer-facing messages
_FORBIDDEN_STRINGS = ["ollama", "Ollama", "winget", "qwen", "Qwen"]


@pytest.mark.unit
class TestCustomerMessages:
    """Message mapping tests -- no internal names should leak."""

    def test_all_messages_free_of_forbidden_strings(self) -> None:
        """Every message template in MESSAGES must not contain internal names."""
        for key, template in MESSAGES.items():
            for forbidden in _FORBIDDEN_STRINGS:
                assert forbidden not in template, (
                    f"MESSAGES['{key}'] contains forbidden string '{forbidden}': {template}"
                )

    def test_customer_message_basic(self) -> None:
        msg = customer_message("check_engine")
        assert msg == "Checking for inference engine..."

    def test_customer_message_with_format(self) -> None:
        msg = customer_message("insufficient_disk", needed_gb=10.5, available_gb=3.2)
        assert "10.5GB" in msg
        assert "3.2GB" in msg
        assert "ollama" not in msg.lower()

    def test_customer_message_pull_progress(self) -> None:
        msg = customer_message("pull_model_progress", pct=62.0, completed=3.2, total=5.2)
        assert "62%" in msg
        assert "3.2" in msg
        assert "5.2" in msg
        assert "ollama" not in msg.lower()

    def test_customer_message_pull_complete_gpu(self) -> None:
        msg = customer_message("pull_model_complete_gpu", vram_gb=16.0)
        assert "16GB GPU" in msg
        assert "ollama" not in msg.lower()

    def test_customer_message_missing_key(self) -> None:
        """Missing keys should fall back gracefully."""
        msg = customer_message("nonexistent_key_xyz")
        assert msg == "nonexistent_key_xyz"

    def test_customer_message_stale_drivers(self) -> None:
        msg = customer_message("stale_drivers", version="535.104")
        assert "535.104" in msg
        assert "ollama" not in msg.lower()

    @pytest.mark.parametrize("key", list(MESSAGES.keys()))
    def test_each_message_no_internal_names(self, key: str) -> None:
        """Parameterised check: each message is free of all forbidden strings."""
        template = MESSAGES[key]
        for forbidden in _FORBIDDEN_STRINGS:
            assert forbidden not in template, f"MESSAGES['{key}'] contains '{forbidden}'"


# ---------------------------------------------------------------------------
# NullProgressReporter
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestNullProgressReporter:
    """NullProgressReporter should collect events silently."""

    def test_collects_events(self) -> None:
        reporter = NullProgressReporter()
        event = ProgressEvent(phase=ProgressPhase.READY, message="Ready!")
        reporter.report(event)
        assert len(reporter.events) == 1
        assert reporter.events[0] is event

    def test_multiple_events(self) -> None:
        reporter = NullProgressReporter()
        for i in range(5):
            reporter.report(ProgressEvent(phase=ProgressPhase.DOWNLOAD_MODEL, message=f"step {i}"))
        assert len(reporter.events) == 5

    def test_finish_is_noop(self) -> None:
        reporter = NullProgressReporter()
        reporter.finish()  # should not raise


# ---------------------------------------------------------------------------
# LogProgressReporter
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestLogProgressReporter:
    """LogProgressReporter should log messages at INFO level."""

    def test_logs_message(self, caplog: pytest.LogCaptureFixture) -> None:
        reporter = LogProgressReporter()
        with caplog.at_level(logging.INFO, logger="llmhosts.progress"):
            reporter.report(ProgressEvent(phase=ProgressPhase.SETUP_ENGINE, message="Setting up inference engine..."))
        assert "Setting up inference engine..." in caplog.text

    def test_finish_is_noop(self) -> None:
        reporter = LogProgressReporter()
        reporter.finish()  # should not raise


# ---------------------------------------------------------------------------
# RichProgressReporter
# ---------------------------------------------------------------------------


@pytest.mark.unit
class TestRichProgressReporter:
    """RichProgressReporter should render without errors."""

    def test_report_simple_event(self) -> None:
        """Should not raise on a simple event."""
        reporter = RichProgressReporter()
        reporter.report(ProgressEvent(phase=ProgressPhase.DETECT_HARDWARE, message="Checking hardware..."))

    def test_report_download_progress(self) -> None:
        """Should not raise on download progress events."""
        reporter = RichProgressReporter()
        reporter.report(
            ProgressEvent(
                phase=ProgressPhase.DOWNLOAD_MODEL,
                message="Downloading... 50%",
                progress_pct=50.0,
            )
        )
        reporter.report(
            ProgressEvent(
                phase=ProgressPhase.DOWNLOAD_MODEL,
                message="Downloading... 100%",
                progress_pct=100.0,
            )
        )

    def test_finish_after_download(self) -> None:
        """Finish should clean up after download progress."""
        reporter = RichProgressReporter()
        reporter.report(
            ProgressEvent(
                phase=ProgressPhase.DOWNLOAD_MODEL,
                message="Downloading... 50%",
                progress_pct=50.0,
            )
        )
        reporter.finish()  # should clean up dangling progress bar
