"""Tests for the update-check command."""

from __future__ import annotations

import importlib
import json
from unittest.mock import MagicMock

import httpx
import pytest
from typer.testing import CliRunner

from cli.commands.update import (
    CACHE_FILENAME,
    _compare_versions,
    _fetch_latest_version,
    _get_cache_dir,
    _get_installed_version,
    _write_cache,
)
from cli.config import CACHE_DIR_ENV, CONFIG_DIR_ENV


def _reload_cli_module():
    import cli.main as cli_main

    return importlib.reload(cli_main)


class TestGetInstalledVersion:
    def test_returns_package_version(self, monkeypatch):
        monkeypatch.setattr("cli.commands.update.metadata.version", lambda _: "1.2.3")
        assert _get_installed_version() == "1.2.3"

    def test_returns_dev_when_not_installed(self, monkeypatch):
        from importlib import metadata

        def raise_not_found(_):
            raise metadata.PackageNotFoundError("ctxme")

        monkeypatch.setattr("cli.commands.update.metadata.version", raise_not_found)
        assert _get_installed_version() == "dev"


class TestFetchLatestVersion:
    def test_returns_version_from_pypi(self):
        mock_client = MagicMock(spec=httpx.Client)
        mock_response = MagicMock()
        mock_response.json.return_value = {"info": {"version": "2.0.0"}}
        mock_client.get.return_value = mock_response

        result = _fetch_latest_version(client=mock_client)
        assert result == "2.0.0"
        mock_client.get.assert_called_once()

    def test_returns_none_on_http_error(self):
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.get.side_effect = httpx.ConnectError("Network error")

        result = _fetch_latest_version(client=mock_client)
        assert result is None

    def test_returns_none_on_timeout(self):
        mock_client = MagicMock(spec=httpx.Client)
        mock_client.get.side_effect = httpx.TimeoutException("Timed out")

        result = _fetch_latest_version(client=mock_client)
        assert result is None

    def test_returns_none_on_invalid_json(self):
        mock_client = MagicMock(spec=httpx.Client)
        mock_response = MagicMock()
        mock_response.json.return_value = {}  # Missing 'info' key
        mock_client.get.return_value = mock_response

        result = _fetch_latest_version(client=mock_client)
        assert result is None


class TestCompareVersions:
    def test_update_available(self):
        assert _compare_versions("1.0.0", "2.0.0") == -1

    def test_up_to_date(self):
        assert _compare_versions("1.0.0", "1.0.0") == 0

    def test_ahead_of_release(self):
        assert _compare_versions("2.0.0", "1.0.0") == 1

    def test_prerelease_comparison(self):
        assert _compare_versions("1.0.0a1", "1.0.0") == -1
        assert _compare_versions("1.0.0", "1.0.0a1") == 1

    def test_invalid_version_raises(self):
        from packaging.version import InvalidVersion

        with pytest.raises(InvalidVersion):
            _compare_versions("invalid", "1.0.0")


class TestCacheHelpers:
    def test_get_cache_dir_uses_env_override(self, tmp_path, monkeypatch):
        custom_dir = tmp_path / "custom_cache"
        monkeypatch.setenv(CACHE_DIR_ENV, str(custom_dir))
        assert _get_cache_dir() == custom_dir

    def test_get_cache_dir_uses_platformdirs_default(self, monkeypatch):
        monkeypatch.delenv(CACHE_DIR_ENV, raising=False)
        cache_dir = _get_cache_dir()
        # Should be a path containing 'ctxme' (platform-specific)
        assert "ctxme" in str(cache_dir)

    def test_write_cache_creates_file(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CACHE_DIR_ENV, str(tmp_path))
        _write_cache("2.0.0")

        cache_file = tmp_path / CACHE_FILENAME
        assert cache_file.exists()

        data = json.loads(cache_file.read_text())
        assert data["latest_version"] == "2.0.0"
        assert "last_check" in data

    def test_write_cache_creates_directory(self, tmp_path, monkeypatch):
        nested_dir = tmp_path / "nested" / "cache"
        monkeypatch.setenv(CACHE_DIR_ENV, str(nested_dir))
        _write_cache("1.5.0")

        cache_file = nested_dir / CACHE_FILENAME
        assert cache_file.exists()

    def test_write_cache_ignores_os_errors(self, tmp_path, monkeypatch):
        # Make directory read-only to trigger OSError
        read_only_dir = tmp_path / "readonly"
        read_only_dir.mkdir()
        read_only_dir.chmod(0o444)
        monkeypatch.setenv(CACHE_DIR_ENV, str(read_only_dir / "subdir"))

        # Should not raise
        _write_cache("1.0.0")

        # Cleanup: restore permissions for test cleanup
        read_only_dir.chmod(0o755)


class TestUpdateCheckCommand:
    def test_dev_version_skips_check(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "dev")
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 0
        assert "development version" in result.stdout
        assert "skipping" in result.stdout.lower()

    def test_unknown_version_skips_check(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "unknown")
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 0
        assert "development version" in result.stdout

    def test_network_error_shows_warning(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "1.0.0")
        monkeypatch.setattr("cli.commands.update._fetch_latest_version", lambda client=None: None)
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 1
        assert "Unable to check for updates" in result.stdout

    def test_update_available_message(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "1.0.0")
        monkeypatch.setattr(
            "cli.commands.update._fetch_latest_version", lambda client=None: "2.0.0"
        )
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 0
        # Notification format per issue #184/#198
        assert "ctxme v2.0.0 is available" in result.stdout
        assert "you have v1.0.0" in result.stdout
        assert "to update" in result.stdout

    def test_up_to_date_message(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "1.0.0")
        monkeypatch.setattr(
            "cli.commands.update._fetch_latest_version", lambda client=None: "1.0.0"
        )
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 0
        assert "up to date" in result.stdout.lower()

    def test_ahead_of_release_message(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "2.0.0")
        monkeypatch.setattr(
            "cli.commands.update._fetch_latest_version", lambda client=None: "1.0.0"
        )
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 0
        assert "ahead" in result.stdout.lower()

    def test_invalid_installed_version_skips_check(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "not-a-version!")
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 0
        assert "Unable to parse" in result.stdout

    def test_invalid_latest_version_shows_error(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "1.0.0")
        monkeypatch.setattr(
            "cli.commands.update._fetch_latest_version",
            lambda client=None: "not-valid!",
        )
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 1
        assert "Unable to parse" in result.stdout

    def test_successful_check_writes_cache(self, tmp_path, monkeypatch):
        monkeypatch.setenv(CONFIG_DIR_ENV, str(tmp_path))
        cache_dir = tmp_path / "cache"
        monkeypatch.setenv(CACHE_DIR_ENV, str(cache_dir))
        monkeypatch.setattr("cli.commands.update._get_installed_version", lambda: "1.0.0")
        monkeypatch.setattr(
            "cli.commands.update._fetch_latest_version", lambda client=None: "2.0.0"
        )
        cli_main = _reload_cli_module()

        runner = CliRunner()
        result = runner.invoke(cli_main.app, ["update-check"])

        assert result.exit_code == 0
        cache_file = cache_dir / CACHE_FILENAME
        assert cache_file.exists()
        data = json.loads(cache_file.read_text())
        assert data["latest_version"] == "2.0.0"
