"""Tests for the CLI auto-update check (cli/update_check.py)."""

import json
from unittest.mock import patch

import pytest
from typer.testing import CliRunner

from cli.main import app

runner = CliRunner()


@pytest.fixture(autouse=True)
def tmp_config(tmp_path, monkeypatch):
    monkeypatch.setenv("AGNES_CONFIG_DIR", str(tmp_path))
    # Point CLI at a fake server so get_server_url() returns something stable.
    monkeypatch.setenv("AGNES_SERVER", "http://server.test:8000")
    yield tmp_path


def test_check_returns_none_when_disabled(tmp_config):
    import os
    os.environ["AGNES_NO_UPDATE_CHECK"] = "1"
    try:
        from cli import update_check
        assert update_check.check("http://server.test:8000") is None
    finally:
        del os.environ["AGNES_NO_UPDATE_CHECK"]


def test_check_returns_none_when_server_url_missing(tmp_config):
    from cli import update_check
    assert update_check.check("") is None
    assert update_check.check(None) is None  # type: ignore[arg-type]


def test_check_bypass_disabled_overrides_env(monkeypatch, tmp_config):
    """`AGNES_NO_UPDATE_CHECK=1` silences the implicit warning loop, but
    explicit callers (e.g. `agnes self-upgrade`) pass `bypass_disabled=True`
    and must NOT become a silent no-op."""
    from cli import update_check

    monkeypatch.setenv("AGNES_NO_UPDATE_CHECK", "1")
    payload = {
        "version": "9.9.9",
        "wheel_filename": "x.whl",
        "download_url_path": "/cli/wheel/x.whl",
    }
    with patch("cli.update_check._installed_version", return_value="2.0.0"):
        with patch("cli.update_check._fetch_latest", return_value=payload):
            # Default: env var wins, returns None.
            assert update_check.check("http://server.test") is None
            # Bypass: env var ignored.
            info = update_check.check("http://server.test", bypass_disabled=True)
            assert info is not None and info.latest == "9.9.9"


def test_check_returns_none_when_installed_version_unknown(tmp_config):
    from cli import update_check
    with patch("cli.update_check._installed_version", return_value="unknown"):
        assert update_check.check("http://server.test:8000") is None


def test_check_fresh_fetch_and_cache_write(tmp_config):
    from cli import update_check

    payload = {
        "version": "2.1.0",
        "wheel_filename": "agnes_the_ai_analyst-2.1.0-py3-none-any.whl",
        "download_url_path": "/cli/wheel/agnes_the_ai_analyst-2.1.0-py3-none-any.whl",
    }
    with patch("cli.update_check._installed_version", return_value="2.0.0"):
        with patch("cli.update_check._fetch_latest", return_value=payload):
            info = update_check.check("http://server.test:8000")

    assert info is not None
    assert info.installed == "2.0.0"
    assert info.latest == "2.1.0"
    assert info.download_url == (
        "http://server.test:8000/cli/wheel/agnes_the_ai_analyst-2.1.0-py3-none-any.whl"
    )
    assert info.is_outdated() is True

    # Cache file was written and re-reading it returns the same latest.
    cache = json.loads((tmp_config / "update_check.json").read_text())
    assert cache["installed"] == "2.0.0"
    assert cache["latest"] == "2.1.0"


def test_check_uses_cache_within_ttl(tmp_config):
    """Cached entry within 24h skips the network fetch."""
    from cli import update_check

    # Seed a fresh cache entry.
    (tmp_config / "update_check.json").write_text(json.dumps({
        "installed": "2.0.0",
        "server_url": "http://server.test:8000",
        "latest": "2.0.5",
        "download_url": "http://server.test:8000/cli/wheel/agnes_the_ai_analyst-2.0.5-py3-none-any.whl",
        "checked_at": __import__("time").time(),  # now
    }))

    with patch("cli.update_check._installed_version", return_value="2.0.0"):
        with patch("cli.update_check._fetch_latest") as mock_fetch:
            info = update_check.check("http://server.test:8000")

    assert mock_fetch.call_count == 0  # cache hit
    assert info.latest == "2.0.5"
    assert info.is_outdated() is True


def test_check_invalidates_cache_when_installed_version_changed(tmp_config):
    """User ran a fresh install after the cache was written — re-probe."""
    from cli import update_check

    # Seed cache claiming the installed version was 1.9.0.
    (tmp_config / "update_check.json").write_text(json.dumps({
        "installed": "1.9.0",
        "server_url": "http://server.test:8000",
        "latest": "2.0.0",
        "download_url": "http://server.test:8000/cli/wheel/x.whl",
        "checked_at": __import__("time").time(),
    }))

    payload = {"version": "2.1.0", "download_url_path": "/cli/wheel/y.whl"}
    with patch("cli.update_check._installed_version", return_value="2.0.0"):
        with patch("cli.update_check._fetch_latest", return_value=payload) as mock_fetch:
            info = update_check.check("http://server.test:8000")

    assert mock_fetch.call_count == 1  # cache was invalidated
    assert info.latest == "2.1.0"


def test_check_handles_network_failure_silently(tmp_config):
    """A probe that errors out returns None; no exception leaks."""
    from cli import update_check
    with patch("cli.update_check._installed_version", return_value="2.0.0"):
        with patch("cli.update_check._fetch_latest", return_value=None):
            assert update_check.check("http://server.test:8000") is None


def test_negative_cache_avoids_reprobe_on_repeated_failure(tmp_config):
    """Two consecutive check() calls after a failed probe must fire the
    network once — the second call hits the 5-minute negative cache."""
    from cli import update_check

    with patch("cli.update_check._installed_version", return_value="2.0.0"):
        with patch("cli.update_check._fetch_latest", return_value=None) as mock_fetch:
            assert update_check.check("http://server.test:8000") is None
            # Second call within the negative-cache window.
            assert update_check.check("http://server.test:8000") is None

    assert mock_fetch.call_count == 1  # no re-probe


def test_negative_cache_expires_after_ttl(tmp_config):
    """After the negative TTL elapses, the probe fires again."""
    import time
    import json as _json

    from cli import update_check

    # Seed a stale negative-cache entry (older than 5min).
    stale_ts = time.time() - (update_check._NEGATIVE_CACHE_TTL_SECONDS + 60)
    (tmp_config / "update_check.json").write_text(_json.dumps({
        "installed": "2.0.0",
        "server_url": "http://server.test:8000",
        "latest": None,
        "download_url": None,
        "checked_at": stale_ts,
    }))

    payload = {"version": "2.1.0", "download_url_path": "/cli/wheel/x.whl"}
    with patch("cli.update_check._installed_version", return_value="2.0.0"):
        with patch("cli.update_check._fetch_latest", return_value=payload) as mock_fetch:
            info = update_check.check("http://server.test:8000")

    assert mock_fetch.call_count == 1  # cache expired, refetch
    assert info is not None
    assert info.latest == "2.1.0"


def test_is_outdated_false_when_same_version(tmp_config):
    from cli.update_check import UpdateInfo
    info = UpdateInfo(installed="2.0.0", latest="2.0.0", download_url="…")
    assert info.is_outdated() is False


def test_is_outdated_false_when_latest_unknown(tmp_config):
    from cli.update_check import UpdateInfo
    info = UpdateInfo(installed="2.0.0", latest=None, download_url=None)
    assert info.is_outdated() is False


def test_is_outdated_true_when_installed_older(tmp_config):
    from cli.update_check import UpdateInfo
    info = UpdateInfo(installed="2.0.0", latest="2.1.0", download_url="…")
    assert info.is_outdated() is True


def test_is_outdated_false_when_installed_newer_than_server(tmp_config):
    """After a server rollback the CLI may be ahead — don't prompt a downgrade."""
    from cli.update_check import UpdateInfo
    info = UpdateInfo(installed="2.1.0", latest="2.0.0", download_url="…")
    assert info.is_outdated() is False


def test_is_outdated_uses_pep440_comparison(tmp_config):
    """`10.0.0 > 2.1.0` — must not be tripped by lexicographic string compare."""
    from cli.update_check import UpdateInfo
    newer_on_server = UpdateInfo(installed="2.1.0", latest="10.0.0", download_url="…")
    older_on_server = UpdateInfo(installed="10.0.0", latest="2.1.0", download_url="…")
    assert newer_on_server.is_outdated() is True
    assert older_on_server.is_outdated() is False


def test_is_outdated_false_for_unparseable_strings(tmp_config):
    """Unparseable versions default to False — we'd rather miss an upgrade
    hint than suggest a bogus downgrade."""
    from cli.update_check import UpdateInfo
    info = UpdateInfo(installed="nightly-abc", latest="nightly-def", download_url="…")
    assert info.is_outdated() is False


def test_format_outdated_notice_drops_upgrade_line_when_no_download_url(tmp_config):
    """`download_url=None` must NOT produce literal "None" in the copy-pasteable command."""
    from cli.update_check import UpdateInfo, format_outdated_notice
    info = UpdateInfo(installed="2.0.0", latest="2.1.0", download_url=None)
    msg = format_outdated_notice(info)
    assert "None" not in msg
    assert "uv tool install" not in msg
    assert "2.0.0" in msg and "2.1.0" in msg


def test_format_outdated_notice_includes_upgrade_command_when_url_present(tmp_config):
    from cli.update_check import UpdateInfo, format_outdated_notice
    info = UpdateInfo(
        installed="2.0.0",
        latest="2.1.0",
        download_url="http://s/cli/wheel/a-2.1.0-py3-none-any.whl",
    )
    msg = format_outdated_notice(info)
    assert "uv tool install --force http://s/cli/wheel/a-2.1.0-py3-none-any.whl" in msg


class TestRootCallbackIntegration:
    """The root callback must not crash a command when the probe fails, and
    must emit a stderr warning when the server advertises a newer version."""

    def test_probe_failure_does_not_break_command(self, tmp_config):
        with patch("cli.update_check.check", side_effect=RuntimeError("boom")):
            result = runner.invoke(app, ["--help"])
        assert result.exit_code == 0

    def test_outdated_warning_is_emitted(self, tmp_config, capsys):
        """Unit-test the warning hook directly: `--help` is eager and bypasses
        the callback body, so we test `_maybe_warn_outdated` itself, which
        is what every real subcommand dispatch triggers."""
        from cli.main import _maybe_warn_outdated
        from cli.update_check import UpdateInfo
        info = UpdateInfo(
            installed="2.0.0",
            latest="2.1.0",
            download_url="http://server.test:8000/cli/wheel/x.whl",
        )
        with patch("cli.update_check.check", return_value=info):
            _maybe_warn_outdated()
        captured = capsys.readouterr()
        assert "[update]" in captured.err
        assert "2.1.0" in captured.err
