"""Tests for ievo.core.startup — first-run checks and version notification."""

from __future__ import annotations

import json
from datetime import UTC, datetime, timedelta
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

from ievo.core.global_config import load_global_config, set_config
from ievo.core.startup import _VERSION_CHECK_INTERVAL, check_version_notification, ensure_startup
from ievo.core.telemetry import reset_sentry_state


def _setup_home(tmp_path: Path, **config: object) -> None:
    """Create config.json with given values."""
    load_global_config(tmp_path)
    for key, value in config.items():
        set_config(tmp_path, key, value)


def _fresh_cache_ts() -> str:
    """Return an ISO timestamp 1 hour ago (fresh cache)."""
    return (datetime.now(tz=UTC) - timedelta(hours=1)).isoformat()


def _stale_cache_ts() -> str:
    """Return an ISO timestamp 25 hours ago (stale cache)."""
    return (datetime.now(tz=UTC) - timedelta(hours=25)).isoformat()


# ── ensure_startup (existing behavior) ────────────────────────


def test_ensure_startup_first_run(tmp_path: Path) -> None:
    """First run: prompts both claude auth and telemetry."""
    reset_sentry_state()
    _setup_home(tmp_path)

    mock_prompt_auth = MagicMock(return_value="env")
    mock_prompt_telemetry = MagicMock(return_value=False)

    with (
        patch("ievo.core.startup.get_config", side_effect=[None, None]),
        patch("ievo.core.startup.init_sentry"),
        patch("ievo.core.startup.check_version_notification"),
        patch("ievo.core.claude_auth.prompt_claude_auth", mock_prompt_auth),
        patch("ievo.core.telemetry.prompt_telemetry", mock_prompt_telemetry),
    ):
        ensure_startup(tmp_path)

    mock_prompt_auth.assert_called_once_with(tmp_path)
    mock_prompt_telemetry.assert_called_once_with(tmp_path)


def test_ensure_startup_already_configured(tmp_path: Path) -> None:
    """Already configured: skips prompts, still inits sentry and version check."""
    reset_sentry_state()
    _setup_home(
        tmp_path,
        claude_auth="env",
        telemetry=False,
        version_check_last_run=_fresh_cache_ts(),
        version_check_result="0.1.0",
    )

    mock_init_sentry = MagicMock()

    with (
        patch("ievo.core.startup.init_sentry", mock_init_sentry),
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
    ):
        ensure_startup(tmp_path)

    mock_init_sentry.assert_called_once_with(tmp_path)


def test_ensure_startup_claude_configured_telemetry_not(tmp_path: Path) -> None:
    """Claude configured but telemetry not asked yet."""
    reset_sentry_state()
    _setup_home(tmp_path, claude_auth="cli")

    mock_prompt_telemetry = MagicMock(return_value=True)

    with (
        patch("ievo.core.startup.init_sentry"),
        patch("ievo.core.startup.check_version_notification"),
        patch("ievo.core.telemetry.prompt_telemetry", mock_prompt_telemetry),
    ):
        ensure_startup(tmp_path)

    mock_prompt_telemetry.assert_called_once_with(tmp_path)


def test_ensure_startup_telemetry_configured_claude_not(tmp_path: Path) -> None:
    """Telemetry configured but claude auth not."""
    reset_sentry_state()
    _setup_home(tmp_path, telemetry=True)

    mock_prompt_auth = MagicMock(return_value=None)

    with (
        patch("ievo.core.startup.init_sentry"),
        patch("ievo.core.startup.check_version_notification"),
        patch("ievo.core.claude_auth.prompt_claude_auth", mock_prompt_auth),
    ):
        ensure_startup(tmp_path)

    mock_prompt_auth.assert_called_once_with(tmp_path)


# ── check_version_notification ────────────────────────────────


def test_version_check_new_version_available(tmp_path: Path) -> None:
    """AC-1, AC-3: stale cache, newer version -> notification + cache updated."""
    _setup_home(tmp_path, version_check_last_run=_stale_cache_ts(), version_check_result="0.1.0")

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
            return_value="0.2.0",
        ),
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info") as mock_info,
    ):
        check_version_notification(tmp_path)

    mock_info.assert_called_once()
    msg = mock_info.call_args[0][0]
    assert "v0.2.0" in msg
    assert "ievo update" in msg

    # Cache updated
    config = json.loads((tmp_path / "config.json").read_text())
    assert config["version_check_result"] == "0.2.0"
    assert config["version_check_last_run"] is not None


def test_version_check_already_up_to_date(tmp_path: Path) -> None:
    """AC-4: same version -> no notification."""
    _setup_home(tmp_path, version_check_last_run=_stale_cache_ts(), version_check_result="0.1.0")

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
            return_value="0.1.0",
        ),
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info") as mock_info,
    ):
        check_version_notification(tmp_path)

    mock_info.assert_not_called()


def test_version_check_cache_fresh_with_newer_version(tmp_path: Path) -> None:
    """AC-2: fresh cache with newer version -> no network, shows notification."""
    _setup_home(tmp_path, version_check_last_run=_fresh_cache_ts(), version_check_result="0.2.0")

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
        ) as mock_fetch,
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info") as mock_info,
    ):
        check_version_notification(tmp_path)

    mock_fetch.assert_not_called()
    mock_info.assert_called_once()
    assert "v0.2.0" in mock_info.call_args[0][0]


def test_version_check_cache_fresh_up_to_date(tmp_path: Path) -> None:
    """AC-2, AC-4: fresh cache, same version -> no network, no notification."""
    _setup_home(tmp_path, version_check_last_run=_fresh_cache_ts(), version_check_result="0.1.0")

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
        ) as mock_fetch,
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info") as mock_info,
    ):
        check_version_notification(tmp_path)

    mock_fetch.assert_not_called()
    mock_info.assert_not_called()


def test_version_check_network_failure(tmp_path: Path) -> None:
    """AC-5: network error -> no notification, no cache update."""
    _setup_home(tmp_path)

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
            side_effect=Exception("Connection failed"),
        ),
        patch("ievo.utils.console.info") as mock_info,
    ):
        check_version_notification(tmp_path)

    mock_info.assert_not_called()
    config = json.loads((tmp_path / "config.json").read_text())
    assert config.get("version_check_last_run") is None


def test_version_check_no_cache_first_run(tmp_path: Path) -> None:
    """AC-1: no cache at all -> fetches, shows notification, populates cache."""
    _setup_home(tmp_path)

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
            return_value="0.2.0",
        ) as mock_fetch,
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info") as mock_info,
    ):
        check_version_notification(tmp_path)

    mock_fetch.assert_called_once()
    mock_info.assert_called_once()
    config = json.loads((tmp_path / "config.json").read_text())
    assert config["version_check_result"] == "0.2.0"


def test_version_check_malformed_cache_timestamp(tmp_path: Path) -> None:
    """Edge case: bad timestamp -> treated as stale, fetches."""
    _setup_home(tmp_path, version_check_last_run="not-a-date", version_check_result="0.1.0")

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
            return_value="0.2.0",
        ) as mock_fetch,
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info"),
    ):
        check_version_notification(tmp_path)

    mock_fetch.assert_called_once()


def test_version_check_malformed_cached_version(tmp_path: Path) -> None:
    """Edge case: bad cached version string -> no crash, no notification."""
    _setup_home(
        tmp_path,
        version_check_last_run=_fresh_cache_ts(),
        version_check_result="not-a-version",
    )

    with (
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info") as mock_info,
    ):
        check_version_notification(tmp_path)

    mock_info.assert_not_called()


# ── ensure_startup integration with version check ─────────────


def test_ensure_startup_calls_version_check_last(tmp_path: Path) -> None:
    """AC-6: version check runs after auth + telemetry + sentry."""
    reset_sentry_state()
    _setup_home(tmp_path, claude_auth="env", telemetry=False)

    call_order: list[str] = []

    def track_sentry(_home: Path) -> None:
        call_order.append("sentry")

    def track_version(_home: Path) -> None:
        call_order.append("version")

    with (
        patch("ievo.core.startup.init_sentry", side_effect=track_sentry),
        patch("ievo.core.startup.check_version_notification", side_effect=track_version),
    ):
        ensure_startup(tmp_path)

    assert call_order == ["sentry", "version"]


def test_ensure_startup_version_check_exception_swallowed(tmp_path: Path) -> None:
    """Negative: version check exception does not block startup."""
    reset_sentry_state()
    _setup_home(tmp_path, claude_auth="env", telemetry=False)

    with (
        patch("ievo.core.startup.init_sentry"),
        patch(
            "ievo.core.startup.check_version_notification",
            side_effect=RuntimeError("boom"),
        ),
    ):
        # Should not raise
        ensure_startup(tmp_path)


def test_version_check_message_format(tmp_path: Path) -> None:
    """Exact message format verification."""
    _setup_home(tmp_path)

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
            return_value="1.2.3",
        ),
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info") as mock_info,
    ):
        check_version_notification(tmp_path)

    msg = mock_info.call_args[0][0]
    assert msg.startswith("A new version of ievo is available: v1.2.3.")
    assert "'ievo update'" in msg


def test_version_check_cache_exactly_24h(tmp_path: Path) -> None:
    """Boundary: exactly 24h old -> stale, fetches."""
    ts = (datetime.now(tz=UTC) - _VERSION_CHECK_INTERVAL).isoformat()
    _setup_home(tmp_path, version_check_last_run=ts, version_check_result="0.1.0")

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
            return_value="0.1.0",
        ) as mock_fetch,
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info"),
    ):
        check_version_notification(tmp_path)

    mock_fetch.assert_called_once()


def test_version_check_cache_23h59m(tmp_path: Path) -> None:
    """Boundary: 23h59m old -> still fresh, no fetch."""
    ts = (datetime.now(tz=UTC) - timedelta(hours=23, minutes=59)).isoformat()
    _setup_home(tmp_path, version_check_last_run=ts, version_check_result="0.1.0")

    with (
        patch(
            "ievo.core.updater.fetch_latest_version",
            new_callable=AsyncMock,
        ) as mock_fetch,
        patch("ievo.core.updater.get_current_version", return_value="0.1.0"),
        patch("ievo.utils.console.info"),
    ):
        check_version_notification(tmp_path)

    mock_fetch.assert_not_called()
