"""Fetching, caching and the boot-loop guard."""

from pathlib import Path

import pytest

from navi.overlay import fetch as fetch_mod
from navi.overlay.fetch import (
    CachedState,
    Fetched,
    confirm_startup,
    load_cached,
    resolve_startup,
    save_cached,
    store_applied,
)
from navi.overlay.util import content_hash
from navi.settings_model import Settings


@pytest.fixture()
def base(tmp_path) -> Settings:
    return Settings(
        _env_file=None,
        hive_url="http://hive.test:8087",
        hive_config_cache_file=str(tmp_path / "cache.json"),
        hive_config_timeout_sec=0.1,
    )


def _doc(**settings) -> list[dict]:
    return [{"name": "global", "config": {"settings": settings}}]


def _path(base) -> Path:
    return Path(base.hive_config_cache_file)


def _patch_fetch(monkeypatch, result):
    def fake(base_settings):
        if isinstance(result, Exception):
            raise result
        return result

    monkeypatch.setattr(fetch_mod, "fetch_layers", fake)


# ── cache file ──────────────────────────────────────────────────────────────


def test_cache_roundtrip(base):
    save_cached(
        _path(base),
        CachedState(
            good={"version": 1, "hash": "sha256:x", "layers": _doc()},
            pending=None,
            blocked_hash="",
        ),
    )
    loaded = load_cached(_path(base))
    assert loaded.good["version"] == 1
    assert loaded.pending is None


def test_cache_is_private(base):
    save_cached(_path(base), CachedState(good={"version": 1, "hash": "", "layers": []}))
    assert (_path(base).stat().st_mode & 0o777) == 0o600


def test_broken_cache_is_treated_as_empty(base):
    _path(base).write_text("{not json")
    loaded = load_cached(_path(base))
    assert loaded.good is None


# ── startup resolution ──────────────────────────────────────────────────────


def test_no_hive_and_no_cache_runs_local(base, monkeypatch):
    _patch_fetch(monkeypatch, RuntimeError("connection refused"))
    result = resolve_startup(base)
    assert result.layers == []
    assert result.source == "local"
    assert "connection refused" in (result.error or "")


def test_hive_url_without_a_key_is_reported_as_unreachable(tmp_path):
    # No monkeypatching: the real fetch runs and stops at the missing key.
    settings = Settings(
        _env_file=None,
        hive_url="http://hive.test:8087",
        swarm_key_file=str(tmp_path / "absent-key"),
        hive_config_cache_file=str(tmp_path / "cache.json"),
        hive_config_timeout_sec=0.1,
    )
    result = resolve_startup(settings)
    assert result.layers == []
    assert result.source == "local"
    assert "swarm key" in (result.error or "")


def test_fetched_document_is_used_and_marked_pending(base, monkeypatch):
    _patch_fetch(
        monkeypatch,
        Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:a"),
    )
    result = resolve_startup(base)
    assert result.source == "hive"
    assert result.merged["settings"] == {"llm_complete_timeout": 30}

    cached = load_cached(_path(base))
    assert cached.pending is not None
    assert cached.pending["attempts"] == 1
    assert cached.good is None


def test_confirm_startup_promotes_pending(base, monkeypatch):
    _patch_fetch(
        monkeypatch,
        Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:a"),
    )
    resolve_startup(base)
    confirm_startup(base)
    cached = load_cached(_path(base))
    assert cached.good is not None
    assert cached.pending is None


def test_confirm_startup_re_reports_the_config_it_started_with(base, monkeypatch):
    from navi.overlay.state import get_state, reset_state

    reset_state()
    state = get_state()
    state.version = 5
    state.hash = "sha256:h"
    state.status = "applied"
    sent = []
    monkeypatch.setattr(fetch_mod, "send_ack", lambda *a: sent.append(a[2:]))
    try:
        confirm_startup(base)
        assert sent == [("applied", [], [])]
    finally:
        reset_state()


def test_confirm_startup_sends_no_ack_without_a_hive_config(base, monkeypatch):
    from navi.overlay.state import reset_state

    reset_state()
    sent = []
    monkeypatch.setattr(fetch_mod, "send_ack", lambda *a: sent.append(a))
    try:
        confirm_startup(base)  # state.version is 0 — nothing was applied
        assert sent == []
    finally:
        reset_state()


def test_same_config_retried_once_then_blocked(base, monkeypatch):
    """A config that was applied but never confirmed is retried once."""
    _patch_fetch(
        monkeypatch,
        Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:a"),
    )

    resolve_startup(base)  # attempt 1
    resolve_startup(base)  # attempt 2
    result = resolve_startup(base)  # guard trips, nothing is applied

    assert result.layers == []
    assert result.boot_guard is True
    cached = load_cached(_path(base))
    assert cached.blocked_hash == "sha256:a"
    assert cached.pending is None


def test_a_new_document_clears_the_guard(base, monkeypatch):
    _patch_fetch(
        monkeypatch,
        Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:a"),
    )
    for _ in range(3):
        resolve_startup(base)

    _patch_fetch(
        monkeypatch,
        Fetched(version=4, layers=_doc(llm_complete_timeout=45), hash="sha256:b"),
    )
    result = resolve_startup(base)
    assert result.source == "hive"
    assert result.merged["settings"] == {"llm_complete_timeout": 45}


def test_fetch_failure_falls_back_to_the_last_good_cache(base, monkeypatch):
    good = _doc(llm_complete_timeout=30)
    save_cached(
        _path(base),
        CachedState(good={"version": 2, "hash": "sha256:g", "layers": good}),
    )
    _patch_fetch(monkeypatch, RuntimeError("hive down"))

    result = resolve_startup(base)
    assert result.source == "cache"
    assert result.merged["settings"] == {"llm_complete_timeout": 30}
    assert result.error and "hive down" in result.error


def test_invalid_fetched_document_falls_back_to_good_cache(base, monkeypatch):
    good = _doc(llm_complete_timeout=30)
    save_cached(
        _path(base),
        CachedState(good={"version": 2, "hash": "sha256:g", "layers": good}),
    )
    _patch_fetch(
        monkeypatch,
        Fetched(version=3, layers=_doc(llm_complete_timeout="soon"), hash="sha256:b"),
    )

    result = resolve_startup(base)
    assert result.source == "cache"
    assert result.merged["settings"] == {"llm_complete_timeout": 30}


def test_invalid_document_with_no_cache_runs_local(base, monkeypatch):
    _patch_fetch(
        monkeypatch,
        Fetched(version=3, layers=_doc(llm_complete_timeout="soon"), hash="sha256:b"),
    )
    result = resolve_startup(base)
    assert result.layers == []
    assert result.source == "local"
    assert result.validation is not None and not result.validation.ok


def test_store_applied_refreshes_good_and_drops_pending(base):
    save_cached(
        _path(base),
        CachedState(
            good={"version": 1, "hash": "sha256:g", "layers": []},
            pending={"version": 2, "hash": "sha256:p", "layers": [], "attempts": 1},
        ),
    )
    store_applied(base, Fetched(version=3, layers=_doc(llm_complete_timeout=30), hash="sha256:n"))
    cached = load_cached(_path(base))
    assert cached.good["version"] == 3
    assert cached.pending is None


def test_empty_layer_answer_is_a_real_answer(base, monkeypatch):
    """The hive saying "no layers" must revert to local, not to the cache."""
    save_cached(
        _path(base),
        CachedState(
            good={
                "version": 2,
                "hash": "sha256:g",
                "layers": _doc(llm_complete_timeout=30),
            }
        ),
    )
    _patch_fetch(
        monkeypatch, Fetched(version=5, layers=[], hash=content_hash([]))
    )

    result = resolve_startup(base)
    assert result.layers == []
    assert result.source == "hive"
    assert result.merged == {}
