"""Snapshot tests for UI rendering.

Calls the real render() function with a plain-text Console (no ANSI codes),
captures output, and compares against golden files.
Uses freezegun to freeze datetime.now() for deterministic timestamps.

Run with --snapshot-update to regenerate: pytest tests/test_snapshots.py --snapshot-update
"""

from __future__ import annotations

import io
from datetime import datetime
from pathlib import Path

import pytest
from freezegun import freeze_time
from rich.console import Console

from yslow.models import (
    CheckResult,
    DnsResult,
    Issue,
    MetricAlert,
    ProbeResult,
    Severity,
)
from yslow.ui.watch import WatchState, render

SNAPSHOT_DIR = Path(__file__).parent / "snapshots"

# Fixed time base for deterministic output
T0 = datetime(2026, 3, 21, 14, 30, 0)


@pytest.fixture()
def _plain_console(monkeypatch):
    """Replace the shared console with a plain-text one writing to a StringIO."""
    buf = io.StringIO()
    plain = Console(file=buf, highlight=False, color_system=None, width=120)
    import yslow.ui.common as common
    import yslow.ui.watch as watch_mod

    monkeypatch.setattr(common, "console", plain)
    monkeypatch.setattr(watch_mod, "console", plain)
    return buf


def _gateway(
    host: str = "192.168.1.1",
    rtts: list[float] | None = None,
    sent: int = 5,
    lost: int = 0,
) -> ProbeResult:
    if rtts is None:
        rtts = [1.2, 1.1, 1.3, 1.0, 1.4]
    return ProbeResult(host, f"Gateway ({host})", sent, lost, rtts)


def _internet(
    rtts: list[list[float]] | None = None,
    sent: int = 5,
    lost: list[int] | None = None,
) -> list[ProbeResult]:
    targets = [
        ("1.1.1.1", "Cloudflare DNS"),
        ("8.8.8.8", "Google DNS"),
        ("208.67.222.222", "OpenDNS"),
    ]
    if rtts is None:
        rtts = [
            [18.5, 19.0, 18.8, 19.2, 18.5],
            [22.1, 21.5, 22.0, 21.8, 22.6],
            [25.0, 24.5, 25.2, 24.8, 25.5],
        ]
    if lost is None:
        lost = [0, 0, 0]
    return [
        ProbeResult(host, label, sent, lost[i], rtts[i])
        for i, (host, label) in enumerate(targets)
    ]


def _dns() -> list[DnsResult]:
    return [
        DnsResult("10.0.0.1", "10.0.0.1", 3.0, 45.0, is_configured=True),
        DnsResult("8.8.8.8", "Google DNS", 25.0, 60.0),
        DnsResult("208.67.222.222", "OpenDNS", 35.0, 42.0),
        DnsResult("1.1.1.1", "Cloudflare DNS", 9.0, 40.0),
    ]


def _result(
    timestamp: datetime = T0,
    gw: ProbeResult | None = None,
    inet: list[ProbeResult] | None = None,
    dns_results: list[DnsResult] | None = None,
    issues: list[Issue] | None = None,
    alerts: list[MetricAlert] | None = None,
) -> CheckResult:
    return CheckResult(
        timestamp=timestamp,
        gateway=gw if gw is not None else _gateway(),
        internet=inet if inet is not None else _internet(),
        dns=dns_results if dns_results is not None else _dns(),
        issues=issues or [],
        alerts=alerts or [],
    )


def _watch_state(*results: CheckResult, interval: int = 30) -> WatchState:
    state = WatchState(interval=interval)
    for r in results:
        state.append(r)
    return state


def _capture_watch(state: WatchState, buf: io.StringIO) -> str:
    """Call render() and return captured output."""
    buf.truncate(0)
    buf.seek(0)
    render(state)
    return buf.getvalue()


# -- Scenarios ----------------------------------------------------------------


def _all_good() -> WatchState:
    r = _result()
    return _watch_state(r)


def _one_issue() -> WatchState:
    gw = _gateway(rtts=[42.0, 48.0, 45.0, 44.0, 46.0])
    issues = [
        Issue(
            Severity.WARNING,
            "gateway-quality",
            source="LAN",
            name="elevated latency",
            cause="possible wifi congestion",
        ),
    ]
    alerts = [
        MetricAlert(gw.host, "rtt", Severity.WARNING),
    ]
    r = _result(
        gw=gw,
        issues=issues,
        alerts=alerts,
    )
    return _watch_state(r)


def _everything_failed() -> WatchState:
    issues = [
        Issue(
            Severity.ERROR,
            "no-connectivity",
            source="network",
            name="no targets reachable",
            cause="internet may be down",
        ),
        Issue(
            Severity.ERROR,
            "gateway-unreachable",
            source="LAN",
            name="gateway unreachable",
            cause="unusual routing setup",
        ),
    ]
    r = _result(
        gw=_gateway(rtts=[], sent=5, lost=5),
        inet=_internet(rtts=[[], [], []], sent=5, lost=[5, 5, 5]),
        issues=issues,
    )
    return _watch_state(r)


SCENARIOS = [
    ("all_good", _all_good),
    ("one_issue", _one_issue),
    ("everything_failed", _everything_failed),
]


def _snapshot_path(scenario: str) -> Path:
    return SNAPSHOT_DIR / f"{scenario}_watch.txt"


@pytest.mark.usefixtures("_plain_console")
@freeze_time(T0)
class TestUISnapshots:
    @pytest.mark.parametrize("name,builder", SCENARIOS, ids=[s[0] for s in SCENARIOS])
    def test_watch_snapshot(self, name, builder, request, _plain_console):
        state = builder()
        actual = _capture_watch(state, _plain_console)
        path = _snapshot_path(name)
        if request.config.getoption("--snapshot-update", default=False):
            SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True)
            path.write_text(actual)
            return
        assert path.exists(), (
            f"Snapshot not found: {path}\n"
            f"Run: pytest tests/test_snapshots.py --snapshot-update"
        )
        assert actual == path.read_text(), (
            f"Snapshot mismatch: {path.name}\n"
            f"Run: pytest tests/test_snapshots.py --snapshot-update"
        )
