# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0

"""Coverage for the exposed-first-run auto-shutdown deadline.

Tests the env parsing, the pure arm/no-arm decision matrix, and the deadline
handler (shut down iff the seeded admin password is still unchanged). The
threading.Timer itself is not exercised; the handler is invoked directly.
"""

from types import SimpleNamespace

from auth.bootstrap_timeout import (
    DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS,
    _format_duration,
    bootstrap_timeout_seconds,
    enforce_bootstrap_password_deadline,
    should_arm_bootstrap_timeout,
)


# ── bootstrap_timeout_seconds ───────────────────────────────────────


def test_default_when_unset():
    assert bootstrap_timeout_seconds(env = {}) == DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS


def test_default_when_empty():
    assert bootstrap_timeout_seconds(env = {"UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT": "  "}) == (
        DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS
    )


def test_explicit_value_parsed():
    assert bootstrap_timeout_seconds(env = {"UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT": "1800"}) == 1800


def test_zero_disables():
    assert bootstrap_timeout_seconds(env = {"UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT": "0"}) == 0


def test_negative_disables():
    assert bootstrap_timeout_seconds(env = {"UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT": "-5"}) == 0


def test_invalid_falls_back_to_default():
    # A typo must keep the protection, not silently disable it.
    assert bootstrap_timeout_seconds(env = {"UNSLOTH_STUDIO_BOOTSTRAP_TIMEOUT": "abc"}) == (
        DEFAULT_BOOTSTRAP_TIMEOUT_SECONDS
    )


# ── should_arm_bootstrap_timeout matrix ─────────────────────────────


def _arm_kwargs(**overrides):
    kwargs = dict(
        host = "0.0.0.0",
        secure = False,
        api_only = False,
        frontend_served = True,
        is_colab = False,
        requires_change = True,
        timeout_seconds = 3600,
    )
    kwargs.update(overrides)
    return kwargs


def test_arm_exposed_wildcard_web_ui():
    assert should_arm_bootstrap_timeout(**_arm_kwargs()) is True


def test_arm_secure_loopback_bind():
    # --secure forces a loopback bind but exposes a public tunnel.
    assert should_arm_bootstrap_timeout(**_arm_kwargs(host = "127.0.0.1", secure = True)) is True


def test_no_arm_loopback_bind():
    assert should_arm_bootstrap_timeout(**_arm_kwargs(host = "127.0.0.1", secure = False)) is False


def test_no_arm_api_only():
    assert should_arm_bootstrap_timeout(**_arm_kwargs(api_only = True)) is False


def test_no_arm_no_frontend():
    assert should_arm_bootstrap_timeout(**_arm_kwargs(frontend_served = False)) is False


def test_no_arm_colab():
    assert should_arm_bootstrap_timeout(**_arm_kwargs(is_colab = True)) is False


def test_no_arm_password_already_changed():
    assert should_arm_bootstrap_timeout(**_arm_kwargs(requires_change = False)) is False


def test_no_arm_timeout_disabled():
    assert should_arm_bootstrap_timeout(**_arm_kwargs(timeout_seconds = 0)) is False


# ── enforce_bootstrap_password_deadline ─────────────────────────────


def _fake_storage(requires_change: bool):
    return SimpleNamespace(
        DEFAULT_ADMIN_USERNAME = "unsloth",
        requires_password_change = lambda _username: requires_change,
    )


def test_deadline_shuts_down_when_password_unchanged():
    calls = []
    result = enforce_bootstrap_password_deadline(
        _fake_storage(requires_change = True),
        lambda: calls.append("shutdown"),
        timeout_seconds = 3600,
    )
    assert result is True
    assert calls == ["shutdown"]


def test_deadline_keeps_running_when_password_changed():
    calls = []
    result = enforce_bootstrap_password_deadline(
        _fake_storage(requires_change = False),
        lambda: calls.append("shutdown"),
        timeout_seconds = 3600,
    )
    assert result is False
    assert calls == []


def test_deadline_swallows_shutdown_errors():
    def _boom():
        raise RuntimeError("shutdown failed")

    # A failing shutdown must not propagate out of the timer thread.
    result = enforce_bootstrap_password_deadline(
        _fake_storage(requires_change = True),
        _boom,
        timeout_seconds = 3600,
    )
    assert result is True


# ── _format_duration ────────────────────────────────────────────────


def test_format_duration_sub_minute_uses_seconds():
    assert _format_duration(30) == "30 seconds"


def test_format_duration_singular_second():
    assert _format_duration(1) == "1 second"


def test_format_duration_exact_minutes():
    assert _format_duration(60) == "1 minute"
    assert _format_duration(3600) == "60 minutes"


def test_format_duration_minutes_and_seconds():
    assert _format_duration(90) == "1 minute 30 seconds"


def test_shutdown_message_uses_formatted_duration():
    # The deadline message must reflect the real timeout, not a rounded
    # "minute(s)" placeholder. Capture the warning via a fake logger.
    logged = []

    class _Logger:
        def warning(self, msg, *args):
            logged.append(msg)

    enforce_bootstrap_password_deadline(
        _fake_storage(requires_change = True),
        lambda: None,
        timeout_seconds = 3600,
        logger = _Logger(),
    )
    assert any("60 minutes" in m for m in logged)
    assert not any("minute(s)" in m for m in logged)
