"""Unit tests for runner/harness/deps.py (Phase 2 §4.2 / Q2.3).

Pure-Python: no real deploys. Tests the declarative parts of the dep resolver — DEPS declaration
(read through the single meta loader since rcust P1), the per-dep domain derivation, and write/load
of the run state file. The deploy_deps + teardown_deps integration is exercised by real e2e against
cc-ci (Q2.4 acceptance).
"""

from __future__ import annotations

import os
import sys

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
from harness import deps  # noqa: E402
from harness import meta as meta_mod  # noqa: E402


def test_declared_deps_empty_for_no_meta(monkeypatch, tmp_path):
    """A recipe with no recipe_meta.py declares no deps (rcust P1: DEPS via meta.load)."""
    monkeypatch.setattr(meta_mod, "TESTS_DIR", str(tmp_path / "tests"))
    assert meta_mod.load("ccci-no-meta").DEPS == []


def test_declared_deps_reads_DEPS_list(tmp_path, monkeypatch):
    """A recipe_meta.py with `DEPS = [...]` surfaces the list on the loaded meta (the orchestrator
    reads meta.DEPS — the successor of the deleted deps.declared_deps loader)."""
    recipe_dir = tmp_path / "tests" / "ccci-with-deps"
    recipe_dir.mkdir(parents=True)
    (recipe_dir / "recipe_meta.py").write_text('HEALTH_PATH = "/"\nDEPS = ["keycloak", "redis"]\n')
    monkeypatch.setattr(meta_mod, "TESTS_DIR", str(tmp_path / "tests"))
    assert meta_mod.load("ccci-with-deps").DEPS == ["keycloak", "redis"]


def test_dep_domain_distinct_per_dep():
    """Two deps of different kinds (same parent/pr/ref) get distinct per-run domains."""
    parent = "lasuite-docs"
    pr = "42"
    ref = "abc123"
    d1 = deps.dep_domain(parent, pr, ref, "keycloak")
    d2 = deps.dep_domain(parent, pr, ref, "redis")
    assert d1 != d2
    # Both must look like the standard run-app pattern: <recipe[:4]>-<6hex>.ci.commoninternet.net
    assert d1.endswith(".ci.commoninternet.net")
    assert d2.endswith(".ci.commoninternet.net")
    # The hash is determined by (parent, pr, ref, dep) — same inputs = same domain (idempotent)
    assert deps.dep_domain(parent, pr, ref, "keycloak") == d1


def test_dep_domain_distinct_per_parent():
    """The same dep deployed by two different parent recipes (same dep, pr, ref) gets distinct
    domains — proves the dep is parent-scoped not just dep-name-scoped."""
    d1 = deps.dep_domain("lasuite-docs", "42", "abc", "keycloak")
    d2 = deps.dep_domain("cryptpad", "42", "abc", "keycloak")
    assert d1 != d2


def test_write_and_load_run_state(tmp_path, monkeypatch):
    """write_run_state writes JSON to $CCCI_DEPS_FILE; load_run_state reads it back."""
    state_path = tmp_path / "deps.json"
    monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path))
    state = [
        {"recipe": "keycloak", "domain": "kc-deadbe.ci.commoninternet.net"},
        {"recipe": "redis", "domain": "redi-abc123.ci.commoninternet.net"},
    ]
    deps.write_run_state(state)
    loaded = deps.load_run_state()
    assert loaded == state


def test_load_run_state_missing_env_returns_empty(monkeypatch):
    """No $CCCI_DEPS_FILE -> empty list."""
    monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
    assert deps.load_run_state() == []


def test_write_run_state_no_env_is_noop(monkeypatch):
    """write_run_state silently no-ops without $CCCI_DEPS_FILE (so standalone helper use doesn't
    require setting up the env)."""
    monkeypatch.delenv("CCCI_DEPS_FILE", raising=False)
    deps.write_run_state([{"recipe": "x", "domain": "y"}])  # must not raise


# ---------------------------------------------------------------------------
# ADV-drone-02 fallback: load_run_state provides teardown data when deps_state={}
# ---------------------------------------------------------------------------


def test_load_run_state_provides_fallback_for_enrichment_failure(tmp_path, monkeypatch):
    """ADV-drone-02: deploy_deps writes legacy-list to $CCCI_DEPS_FILE; if _enrich_deps_with_sso
    raises, deps_state = {} in main(). The fallback in the finally block reads load_run_state()
    to recover the deployed-but-unenriched entries for teardown.

    This test verifies that load_run_state() returns the entries written by deploy_deps (legacy
    list shape) so the fallback teardown receives the correct cold entries."""
    state_path = tmp_path / "deps.json"
    monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path))
    # deploy_deps writes the legacy list shape (before enrichment converts to dict)
    deployed_by_deploy_deps = [
        {"recipe": "gitea", "domain": "gite-aabbcc.ci.commoninternet.net"},
    ]
    deps.write_run_state(deployed_by_deploy_deps)

    raw = deps.load_run_state()
    assert isinstance(raw, list), "load_run_state must return the legacy list shape"
    assert len(raw) == 1
    cold_raw = [e for e in raw if isinstance(e, dict) and not e.get("warm")]
    assert len(cold_raw) == 1
    assert cold_raw[0]["recipe"] == "gitea"
    assert cold_raw[0]["domain"] == "gite-aabbcc.ci.commoninternet.net"


def test_fallback_skips_warm_entries(tmp_path, monkeypatch):
    """ADV-drone-02 fallback filtering: warm entries (keycloak live-warm provider) must NOT be
    undeployed — only cold entries should be in the teardown list."""
    state_path = tmp_path / "deps.json"
    monkeypatch.setenv("CCCI_DEPS_FILE", str(state_path))
    mixed = [
        {"recipe": "keycloak", "domain": "keyc-live.ci.commoninternet.net", "warm": True},
        {"recipe": "gitea", "domain": "gite-aabbcc.ci.commoninternet.net"},
    ]
    deps.write_run_state(mixed)

    raw = deps.load_run_state()
    cold_raw = [
        e
        for e in (raw if isinstance(raw, list) else list(raw.values()))
        if isinstance(e, dict) and not e.get("warm")
    ]
    assert len(cold_raw) == 1
    assert cold_raw[0]["recipe"] == "gitea"
