"""Unit tests for the console launcher's pure logic — no browser, no subprocess.

``live_server``'s real subprocess lifecycle (early exit, never-serves) is covered
by ``tests/integration/test_live_server.py``; here we pin the pieces that need
nothing but the interpreter.
"""

from __future__ import annotations

import socket
import urllib.error
from unittest import mock

from mgf.uitest_web import LiveServer, ServerStartError, UITestWebError, free_port
from mgf.uitest_web.server import _probe


def test_free_port_returns_a_bindable_port() -> None:
    port = free_port()
    assert 1 <= port <= 65535
    # It was released before returning, so we can bind it ourselves.
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.bind(("127.0.0.1", port))


def test_liveserver_url_joins_paths() -> None:
    srv = LiveServer("http://127.0.0.1:9999", 9999)
    assert srv.url() == "http://127.0.0.1:9999/"
    assert srv.url("/a/b") == "http://127.0.0.1:9999/a/b"
    assert srv.url("a/b") == "http://127.0.0.1:9999/a/b"  # leading slash added


def test_probe_returns_status_on_success() -> None:
    resp = mock.MagicMock()
    resp.status = 200
    resp.__enter__.return_value = resp
    resp.__exit__.return_value = False
    with mock.patch("urllib.request.urlopen", return_value=resp):
        assert _probe("http://127.0.0.1:1/") == 200


def test_probe_returns_status_on_http_error() -> None:
    # Any HTTP response means the server is up — even a 503.
    err = urllib.error.HTTPError("http://x/", 503, "Service Unavailable", {}, None)
    with mock.patch("urllib.request.urlopen", side_effect=err):
        assert _probe("http://127.0.0.1:1/") == 503


def test_probe_returns_none_when_unreachable() -> None:
    with mock.patch("urllib.request.urlopen", side_effect=urllib.error.URLError("down")):
        assert _probe("http://127.0.0.1:1/") is None


def test_server_start_error_is_uitestweberror_and_carries_fields() -> None:
    err = ServerStartError("boom", returncode=3, output="console log")
    assert isinstance(err, UITestWebError)
    # Back-compat: an existing `except RuntimeError` still catches it.
    assert isinstance(err, RuntimeError)
    assert err.returncode == 3
    assert err.output == "console log"
    # Defaults when a caller omits the machine fields.
    assert ServerStartError("bare").returncode is None
    assert ServerStartError("bare").output == ""
