from __future__ import annotations

import json
from datetime import datetime, timezone, timedelta
from unittest.mock import patch, MagicMock

import pytest

from autosana_cli import update_check
from autosana_cli.models import update_check_path


def _make_pypi_response(version: str) -> bytes:
    return json.dumps({"info": {"version": version}}).encode("utf-8")


def _write_cache(version: str, age_seconds: int = 0) -> None:
    cache_file = update_check_path()
    cache_file.parent.mkdir(parents=True, exist_ok=True)
    checked_at = datetime.now(timezone.utc) - timedelta(seconds=age_seconds)
    payload = {"latest_version": version, "checked_at": checked_at.isoformat()}
    cache_file.write_text(json.dumps(payload), encoding="utf-8")


class TestCheckForUpdate:
    def test_warns_when_outdated(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "0.2.0")
        mock_resp = MagicMock()
        mock_resp.read.return_value = _make_pypi_response("0.3.0")
        mock_resp.__enter__ = lambda s: s
        mock_resp.__exit__ = MagicMock(return_value=False)
        with patch.object(update_check, "urlopen", return_value=mock_resp):
            update_check.check_for_update()
        captured = capsys.readouterr()
        assert "0.3.0 is available" in captured.err
        assert "pipx upgrade autosana" in captured.err

    def test_no_warn_when_current(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "0.3.0")
        mock_resp = MagicMock()
        mock_resp.read.return_value = _make_pypi_response("0.3.0")
        mock_resp.__enter__ = lambda s: s
        mock_resp.__exit__ = MagicMock(return_value=False)
        with patch.object(update_check, "urlopen", return_value=mock_resp):
            update_check.check_for_update()
        captured = capsys.readouterr()
        assert captured.err == ""

    def test_no_warn_when_ahead(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "0.4.0")
        mock_resp = MagicMock()
        mock_resp.read.return_value = _make_pypi_response("0.3.0")
        mock_resp.__enter__ = lambda s: s
        mock_resp.__exit__ = MagicMock(return_value=False)
        with patch.object(update_check, "urlopen", return_value=mock_resp):
            update_check.check_for_update()
        captured = capsys.readouterr()
        assert captured.err == ""

    def test_cache_hit_skips_http(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "0.2.0")
        _write_cache("99.0.0", age_seconds=100)
        with patch.object(update_check, "urlopen") as mock_urlopen:
            update_check.check_for_update()
        mock_urlopen.assert_not_called()
        captured = capsys.readouterr()
        assert "99.0.0 is available" in captured.err

    def test_expired_cache_refetches(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "0.2.0")
        _write_cache("0.2.0", age_seconds=90000)  # > 24h
        mock_resp = MagicMock()
        mock_resp.read.return_value = _make_pypi_response("0.5.0")
        mock_resp.__enter__ = lambda s: s
        mock_resp.__exit__ = MagicMock(return_value=False)
        with patch.object(update_check, "urlopen", return_value=mock_resp) as mock_urlopen:
            update_check.check_for_update()
        mock_urlopen.assert_called_once()
        captured = capsys.readouterr()
        assert "0.5.0 is available" in captured.err

    def test_pypi_unreachable_is_silent(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "0.2.0")
        from urllib.error import URLError

        with patch.object(update_check, "urlopen", side_effect=URLError("network down")):
            update_check.check_for_update()
        captured = capsys.readouterr()
        assert captured.err == ""

    def test_corrupt_cache_is_silent(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "0.2.0")
        cache_file = update_check_path()
        cache_file.parent.mkdir(parents=True, exist_ok=True)
        cache_file.write_text("not valid json!!!", encoding="utf-8")
        with patch.object(update_check, "urlopen", side_effect=OSError("no network")):
            update_check.check_for_update()
        captured = capsys.readouterr()
        assert captured.err == ""

    def test_env_var_disables_check(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "0.2.0")
        monkeypatch.setenv("AUTOSANA_NO_UPDATE_CHECK", "1")
        with patch.object(update_check, "urlopen") as mock_urlopen:
            update_check.check_for_update()
        mock_urlopen.assert_not_called()
        captured = capsys.readouterr()
        assert captured.err == ""

    def test_dev_version_skips_check(self, capsys, monkeypatch):
        monkeypatch.setattr(update_check, "__version__", "dev")
        with patch.object(update_check, "urlopen") as mock_urlopen:
            update_check.check_for_update()
        mock_urlopen.assert_not_called()
        captured = capsys.readouterr()
        assert captured.err == ""


class TestParseVersion:
    def test_valid(self):
        assert update_check._parse_version("0.2.0") == (0, 2, 0)
        assert update_check._parse_version("1.10.3") == (1, 10, 3)

    def test_invalid(self):
        assert update_check._parse_version("dev") is None
        assert update_check._parse_version("") is None
        assert update_check._parse_version("abc.def.ghi") is None
