"""Tests for aablocks.version_check module."""

from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest
import requests

import aablocks.version_check as version_check
from aablocks.version_check import _warnings_suppressed, warn_if_outdated_once


@pytest.fixture(autouse=True)
def reset_check_cache():
    """Reset the once-per-process cache so each test starts fresh."""
    warn_if_outdated_once.cache_clear()
    yield
    warn_if_outdated_once.cache_clear()


@pytest.fixture(autouse=True)
def mock_log_request():
    """Stub activity logging so tests don't write to ~/.aab/logs."""
    with patch("aablocks.version_check.log_request") as mock_log:
        yield mock_log


@pytest.fixture(autouse=True)
def warning_display_env(monkeypatch: pytest.MonkeyPatch):
    """Bypass suppression (pytest stderr is not a tty). Suppression itself is
    tested via `_warnings_suppressed` directly in TestWarningsSuppressed."""
    monkeypatch.setattr(version_check, "_warnings_suppressed", lambda: False)


@pytest.fixture(autouse=True)
def isolate_state_file(tmp_path, monkeypatch: pytest.MonkeyPatch):
    """Redirect the once-per-day state file to a temp path so tests neither
    touch ~/.aab nor leak state between each other."""
    monkeypatch.setattr(
        version_check, "STATE_FILE_PATH", tmp_path / "version_check.json"
    )


@pytest.fixture(autouse=True)
def installed_version(monkeypatch: pytest.MonkeyPatch):
    """Pin the installed version so comparisons are deterministic."""
    monkeypatch.setattr(version_check, "_installed_version", lambda: "0.1.22")


def _pypi_response(version, status_code=200):
    resp = MagicMock(spec=requests.Response)
    resp.status_code = status_code
    resp.ok = status_code < 400
    resp.json.return_value = {"info": {"version": version}}
    resp.raise_for_status.return_value = None
    return resp


class TestWarnIfOutdated:
    @patch("aablocks.version_check.requests.get")
    def test_warns_when_newer_version_exists(self, mock_get, capsys):
        """An out-of-date install prints an upgrade hint to stderr."""
        mock_get.return_value = _pypi_response("0.1.23")
        warn_if_outdated_once()
        captured = capsys.readouterr()
        assert captured.out == ""
        assert "newer version is available (0.1.23 > 0.1.22)" in captured.err
        assert "pip install --upgrade aablocks" in captured.err

    @patch("aablocks.version_check.requests.get")
    def test_quiet_when_up_to_date(self, mock_get, capsys):
        """Matching versions produce no output."""
        mock_get.return_value = _pypi_response("0.1.22")
        warn_if_outdated_once()
        assert capsys.readouterr().err == ""

    @patch("aablocks.version_check.requests.get")
    def test_quiet_when_installed_is_ahead(self, mock_get, capsys):
        """A local build ahead of PyPI produces no output."""
        mock_get.return_value = _pypi_response("0.1.21")
        warn_if_outdated_once()
        assert capsys.readouterr().err == ""

    @patch("aablocks.version_check.requests.get")
    def test_shown_only_once_per_process(self, mock_get, capsys):
        """The second call neither re-fetches nor re-prints."""
        mock_get.return_value = _pypi_response("0.1.23")
        warn_if_outdated_once()
        capsys.readouterr()
        warn_if_outdated_once()
        assert capsys.readouterr().err == ""
        assert mock_get.call_count == 1

    @patch("aablocks.version_check.requests.get")
    def test_fetch_failure_is_swallowed(self, mock_get, capsys):
        """A network error neither raises nor prints."""
        mock_get.side_effect = requests.ConnectionError("offline")
        warn_if_outdated_once()
        assert capsys.readouterr().err == ""

    @patch("aablocks.version_check.requests.get")
    def test_http_error_is_swallowed(self, mock_get, capsys):
        """A 500 from PyPI neither raises nor prints."""
        resp = _pypi_response("0.1.23", status_code=500)
        resp.raise_for_status.side_effect = requests.HTTPError(response=resp)
        mock_get.return_value = resp
        warn_if_outdated_once()
        assert capsys.readouterr().err == ""

    @patch("aablocks.version_check.requests.get")
    def test_malformed_payload_is_ignored(self, mock_get, capsys):
        """A payload missing info.version is swallowed."""
        resp = MagicMock(spec=requests.Response)
        resp.status_code = 200
        resp.json.return_value = {"unexpected": "shape"}
        resp.raise_for_status.return_value = None
        mock_get.return_value = resp
        warn_if_outdated_once()
        assert capsys.readouterr().err == ""

    @patch("aablocks.version_check.requests.get")
    def test_queries_pypi_with_short_timeout(self, mock_get):
        """The fetch targets the PyPI JSON endpoint with a short timeout."""
        mock_get.return_value = _pypi_response("0.1.22")
        warn_if_outdated_once()
        mock_get.assert_called_once_with(version_check.PYPI_URL, timeout=5)

    @patch("aablocks.version_check.requests.get")
    def test_successful_fetch_is_logged(self, mock_get, mock_log_request):
        """A successful fetch logs the request with its status code."""
        mock_get.return_value = _pypi_response("0.1.22")
        warn_if_outdated_once()
        mock_log_request.assert_called_once()
        args = mock_log_request.call_args.args
        assert args[0] == "GET"
        assert args[1] == version_check.PYPI_URL
        assert args[3] == 200  # status_code
        assert args[4] is None  # error

    @patch("aablocks.version_check.requests.get")
    def test_fetch_failure_is_logged(self, mock_get, mock_log_request):
        """A failed fetch is swallowed but still logged with the error."""
        mock_get.side_effect = requests.ConnectionError("offline")
        warn_if_outdated_once()
        mock_log_request.assert_called_once()
        args = mock_log_request.call_args.args
        assert args[3] is None  # no status_code
        assert "offline" in args[4]  # error message


class TestOncePerDay:
    """The state file throttles the PyPI check to once per calendar day."""

    @patch("aablocks.version_check.requests.get")
    def test_not_rechecked_same_day_new_process(self, mock_get, capsys):
        """A second invocation the same day makes no network call and is quiet."""
        mock_get.return_value = _pypi_response("0.1.23")
        warn_if_outdated_once()
        capsys.readouterr()
        warn_if_outdated_once.cache_clear()  # simulate a fresh CLI process
        warn_if_outdated_once()
        assert capsys.readouterr().err == ""
        assert mock_get.call_count == 1

    @patch("aablocks.version_check.requests.get")
    def test_rechecked_on_a_new_day(self, mock_get, capsys, monkeypatch):
        """A new day re-fetches and re-warns."""
        mock_get.return_value = _pypi_response("0.1.23")
        monkeypatch.setattr(version_check, "_today", lambda: "2026-06-05")
        warn_if_outdated_once()
        capsys.readouterr()
        warn_if_outdated_once.cache_clear()
        monkeypatch.setattr(version_check, "_today", lambda: "2026-06-06")
        warn_if_outdated_once()
        assert "newer version is available" in capsys.readouterr().err
        assert mock_get.call_count == 2

    @patch("aablocks.version_check.requests.get")
    def test_corrupt_state_file_is_tolerated(self, mock_get, capsys):
        """A malformed state file is treated as empty, so the check still runs."""
        version_check.STATE_FILE_PATH.write_text("not json{")
        mock_get.return_value = _pypi_response("0.1.23")
        warn_if_outdated_once()
        assert "newer version is available" in capsys.readouterr().err


def _boom(*_args, **_kwargs):
    raise RuntimeError("boom")


class TestNeverRaises:
    """The version check is best-effort: warn_if_outdated_once must never
    propagate an error, even from failures outside the fetch try/except."""

    def test_suppression_check_error_is_swallowed(self, monkeypatch):
        monkeypatch.setattr(version_check, "_warnings_suppressed", _boom)
        warn_if_outdated_once()  # must not raise

    @patch("aablocks.version_check.requests.get")
    def test_state_write_error_is_swallowed(self, mock_get, monkeypatch):
        mock_get.return_value = _pypi_response("0.1.23")
        monkeypatch.setattr(version_check, "write_secure_file", _boom)
        warn_if_outdated_once()  # must not raise

    @patch("aablocks.version_check.requests.get")
    def test_print_error_is_swallowed(self, mock_get, monkeypatch):
        mock_get.return_value = _pypi_response("0.1.23")
        monkeypatch.setattr("builtins.print", _boom)
        warn_if_outdated_once()  # must not raise


class TestVersionComparison:
    """`_is_outdated` parses dotted versions and compares them numerically."""

    @pytest.mark.parametrize(
        "installed,latest,expected",
        [
            ("0.1.22", "0.1.23", True),
            ("0.1.22", "0.2.0", True),
            ("0.1.22", "1.0.0", True),
            ("0.1.22", "0.1.22", False),
            ("0.1.22", "0.1.21", False),
            ("0.1.9", "0.1.10", True),  # numeric, not lexicographic
        ],
    )
    def test_is_outdated(self, installed, latest, expected):
        assert version_check._is_outdated(installed, latest) is expected


class TestWarningsSuppressed:
    """Tests for `_warnings_suppressed` (the autouse fixture patches the
    module attribute, so the directly imported original is used here)."""

    @pytest.fixture(autouse=True)
    def interactive_stderr(self, monkeypatch: pytest.MonkeyPatch):
        """Start from an interactive-terminal state."""
        monkeypatch.setattr(
            version_check, "sys", MagicMock(stderr=MagicMock(isatty=lambda: True))
        )

    @patch("aablocks.version_check.get_version_check_enabled", return_value=True)
    def test_interactive_default_not_suppressed(self, _mock_enabled):
        assert _warnings_suppressed() is False

    @patch("aablocks.version_check.get_version_check_enabled", return_value=True)
    def test_non_tty_stderr(self, _mock_enabled, monkeypatch):
        monkeypatch.setattr(
            version_check, "sys", MagicMock(stderr=MagicMock(isatty=lambda: False))
        )
        assert _warnings_suppressed() is True

    @patch("aablocks.version_check.get_version_check_enabled", return_value=False)
    def test_config_disabled(self, _mock_enabled):
        assert _warnings_suppressed() is True

    @patch("aablocks.version_check.requests.get")
    def test_suppressed_skips_fetch(self, mock_get, monkeypatch):
        """When suppressed, no request is made at all."""
        monkeypatch.setattr(version_check, "_warnings_suppressed", lambda: True)
        warn_if_outdated_once()
        mock_get.assert_not_called()
