"""Unit tests for the WC1.2 safety-gate + version helpers in runner/warm_reconcile.py.

Pure logic only (no abra/docker). The reconcile flow itself is proven live on cc-ci against the warm
keycloak (W0.6). These lock the gate's correctness: which bumps auto-apply vs hold, and the
manual-migration marker scan.
"""

from __future__ import annotations

import os
import sys
from types import SimpleNamespace

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
import warm_reconcile as wr  # noqa: E402


def test_is_version_tag():
    assert wr.is_version_tag("10.7.1+26.6.2")
    assert wr.is_version_tag("3.2.0")
    assert not wr.is_version_tag("main")
    assert not wr.is_version_tag("latest")
    assert not wr.is_version_tag("")


def test_sort_and_latest():
    tags = ["10.6.0+26.5.4", "10.7.1+26.6.2", "10.5.1+26.4.5", "main", "10.7.0+26.6.1"]
    assert wr.latest_version(tags) == "10.7.1+26.6.2"
    assert wr.sort_versions(tags)[0] == "10.5.1+26.4.5"


def test_latest_none_when_no_tags():
    assert wr.latest_version(["main", "feature-x"]) is None


def test_is_released_version(monkeypatch):
    # canon §2.A tagged-promote gate. is_released_version(recipe, version) → True iff version is a
    # published release tag (exact or by version_key). recipe_tags is the only I/O — monkeypatch it.
    tags = ["1.13.0+1.31.1", "1.12.0+1.30.0", "main", "feature-x"]
    monkeypatch.setattr(wr, "recipe_tags", lambda r: tags)
    assert wr.is_released_version("custom-html", "1.13.0+1.31.1") is True  # exact tag
    assert wr.is_released_version("custom-html", "1.12.0+1.30.0") is True
    assert wr.is_released_version("custom-html", "9.9.9+9.9.9") is False  # not a tag → untagged
    assert wr.is_released_version("custom-html", None) is False
    assert wr.is_released_version("custom-html", "") is False


def test_sweep_decision():
    # canon §2.D new-release-tag trigger (pure), keyed on release tag vs canonical version.
    # new release tag > canonical → run
    assert wr.sweep_decision("1.13.0+1.31.1", "1.12.0+1.30.0")[0] == "run"
    # latest tag == canonical → skip (no-new-version) — the run-twice determinism no-op
    assert wr.sweep_decision("1.13.0+1.31.1", "1.13.0+1.31.1")[0] == "skip"
    assert "no-new-version" in wr.sweep_decision("1.13.0+1.31.1", "1.13.0+1.31.1")[1]
    # latest tag OLDER than canonical (shouldn't happen, but never downgrade) → skip
    assert wr.sweep_decision("1.12.0+1.30.0", "1.13.0+1.31.1")[0] == "skip"
    # no canonical yet → run (seed)
    assert wr.sweep_decision("1.0.0+1.0.0", None)[0] == "run"
    # no release tag at all → skip (never-released)
    assert wr.sweep_decision(None, "1.0.0+1.0.0")[0] == "skip"
    assert wr.sweep_decision(None, None)[0] == "skip"
    assert "never-released" in wr.sweep_decision(None, None)[1]


def test_sweep_decision_skips_untagged_ahead():
    # The trigger compares the latest RELEASE TAG, not main HEAD: if main has new untagged commits
    # but the latest tag still equals the canonical, the recipe is SKIPPED (no untagged promote).
    # (Modelled here as latest_tag unchanged vs canonical — commits never enter the decision.)
    assert wr.sweep_decision("2.0.0+9.9.9", "2.0.0+9.9.9") == (
        "skip",
        "no-new-version (latest release 2.0.0+9.9.9 <= canonical 2.0.0+9.9.9)",
    )


def test_is_released_version_no_tags(monkeypatch):
    # a recipe that has never cut a release → no version is a release
    monkeypatch.setattr(wr, "recipe_tags", lambda r: ["main"])
    assert wr.is_released_version("never-released", "0.1.0") is False


def test_minor_patch_bump_not_major():
    # recipe-semver 10.7.0 -> 10.7.1 (patch); app 26.6.1 -> 26.6.2 (patch). Auto-apply.
    assert wr.is_major_bump("10.7.0+26.6.1", "10.7.1+26.6.2") is False
    # minor recipe bump 10.7.1 -> 10.8.0. Auto-apply.
    assert wr.is_major_bump("10.7.1+26.6.2", "10.8.0+26.6.2") is False


def test_recipe_major_bump_held():
    # recipe-semver 10.x -> 11.0 (major). HELD.
    assert wr.is_major_bump("10.7.1+26.6.2", "11.0.0+26.6.2") is True


def test_app_major_bump_held():
    # app version 26.x -> 27.0 (major, e.g. keycloak DB migration era). HELD (conservative).
    assert wr.is_major_bump("10.7.1+26.6.2", "10.8.0+27.0.0") is True


def test_app_major_bump_held_even_if_no_plus_on_current():
    assert wr.is_major_bump("0", "11.0.0+1.0.0") is True


def test_traefik_spec_is_stateless_with_setup():
    # WC1.1 traefik = stateless (version-rollback-only, NO snapshot) + its own cert/file-provider
    # setup. Health is probed on traefik's OWN /api/version endpoint (phase pxgate A1: probing the
    # dashboard host caused a cold-boot deadlock since deploy-dashboard is After=deploy-proxy, so the
    # spec carries no `health_domain` override and health_code() falls back to the app domain).
    t = wr.SPECS["traefik"]
    assert t["stateful"] is False
    assert callable(t.get("setup"))
    assert "health_domain" not in t  # probes traefik's own domain, not a routed dashboard host
    assert t["health_path"] == "/api/version"
    assert t["domain"] == "traefik.ci.commoninternet.net"
    # keycloak stays stateful with no custom setup (default path)
    assert wr.SPECS["keycloak"]["stateful"] is True
    assert "setup" not in wr.SPECS["keycloak"]


def test_traefik_certificate_rollout_detects_active_secret_version(monkeypatch):
    stack = "traefik_ci_commoninternet_net"
    monkeypatch.setattr(wr.lifecycle, "_stack_name", lambda _domain: stack)
    monkeypatch.setattr(
        wr,
        "_run",
        lambda *_args, **_kwargs: SimpleNamespace(
            stdout=f"{stack}_ssl_cert_vnew {stack}_ssl_key_vnew"
        ),
    )
    assert not wr._traefik_requires_certificate_rollout("traefik.ci.commoninternet.net", "vnew")
    assert wr._traefik_requires_certificate_rollout("traefik.ci.commoninternet.net", "vold")


def test_manual_migration_markers():
    assert wr.notes_flag_manual_migration("This release requires a MANUAL MIGRATION of the DB.")
    assert wr.notes_flag_manual_migration("Breaking change: action required before upgrade.")
    assert wr.notes_flag_manual_migration("You must run the migration by hand.")
    assert not wr.notes_flag_manual_migration("Routine patch. Automatic, no action needed.")
    assert not wr.notes_flag_manual_migration("")
