"""The credential CONTROL PLANE at the HTTP boundary — /admin/reload-config and
/admin/creds-status, exercised through DRF's real authentication + permission stack.

These are the tests that would have caught rm#334: `POST /admin/reload-config -> 200 ANONYMOUS` on
every service without a provisioned key. A unit test of the permission class alone would not have —
the defect only shows when the view, the auth class and the permission are wired together.

⚑ No test here asserts on a credential value. Auth is exercised with obviously-fake sentinels, and
what is asserted is the STATUS CODE and the posture.
"""
import json

import pytest
from django.test import override_settings
from rest_framework.test import APIRequestFactory

from andrea_svc_web import creds as credsmod
from andrea_svc_web.creds import ServiceSecrets
from andrea_svc_web.health import HealthView
from andrea_svc_web.reload import CredsStatusView, ReloadConfigView

rf = APIRequestFactory()

KEY = "endpoint-test-sentinel-key"


def _secrets(tmp_path, body=f"ANDREA_SVC_API_KEY={KEY}\n", **kw):
    f = tmp_path / "svc.env"
    f.write_text(body)
    kw.setdefault("env", {})
    s = ServiceSecrets("endpoint-svc", env_files=[str(f)],
                       state_path=str(tmp_path / "stamp.json"), **kw)
    s.load()
    return s, f


def _post(secrets, key=None):
    headers = {"X-Agent-Key": key} if key else {}
    req = rf.post("/admin/reload-config", **({"headers": headers} if headers else {}))
    return ReloadConfigView.as_view()(req)


def _get_status(secrets, key=None):
    headers = {"X-Agent-Key": key} if key else {}
    req = rf.get("/admin/creds-status", **({"headers": headers} if headers else {}))
    return CredsStatusView.as_view()(req)


# ══════════════════════════════════════════════════════════════════════════════════════════════
# rm#334 — the control plane is never anonymous-200 again
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_reload_endpoint_is_503_not_200_when_the_service_has_no_key(tmp_path):
    """THE regression test for the known defect. word-detect answered 200 ANONYMOUS here."""
    s, _ = _secrets(tmp_path, body="NOTHING=x\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=None, OIDC_ISSUER="", OIDC_JWKS_URL=""):
        resp = _post(s)
    assert resp.status_code == 503
    assert resp.data["posture"] == "unconfigured"
    assert resp.data["detail"] == "service credential not provisioned"


def test_creds_status_endpoint_is_503_when_the_service_has_no_key(tmp_path):
    s, _ = _secrets(tmp_path, body="NOTHING=x\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=None, OIDC_ISSUER="", OIDC_JWKS_URL=""):
        resp = _get_status(s)
    assert resp.status_code == 503


def test_reload_endpoint_is_401_when_configured_and_uncredentialed(tmp_path):
    s, _ = _secrets(tmp_path)
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        resp = _post(s)
    assert resp.status_code == 401                    # a credential EXISTS that would work -> 401


def test_reload_endpoint_is_401_with_the_wrong_key(tmp_path):
    s, _ = _secrets(tmp_path)
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        resp = _post(s, key="not-the-key")
    assert resp.status_code == 401


def test_reload_endpoint_applies_with_a_valid_key(tmp_path):
    s, f = _secrets(tmp_path)
    f.write_text(f"ANDREA_SVC_API_KEY={KEY}\nPOLL_INTERVAL=9\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        resp = _post(s, key=KEY)
    assert resp.status_code == 200
    assert resp.data["applied"] is True
    assert resp.data["changed"]["other"] == ["POLL_INTERVAL"]


# ══════════════════════════════════════════════════════════════════════════════════════════════
# 409 — a rejected reload is not a 200 with applied:false, and not a 500
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_a_rejected_reload_answers_409_and_changes_nothing(tmp_path):
    s, f = _secrets(tmp_path, validators={"POLL_INTERVAL": lambda v: v.isdigit()})
    f.write_text(f"ANDREA_SVC_API_KEY={KEY}\nPOLL_INTERVAL=9\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        assert _post(s, key=KEY).status_code == 200
        gen = s.generation
        f.write_text(f"ANDREA_SVC_API_KEY={KEY}\nPOLL_INTERVAL=not-a-number\n")
        resp = _post(s, key=KEY)
    assert resp.status_code == 409
    assert resp.data["applied"] is False
    assert resp.data["reason"] == "rejected"
    assert "POLL_INTERVAL" in resp.data["rejected"]
    assert resp.data["generation"] == gen             # generation UNCHANGED


def test_a_rejected_candidate_is_quarantined_from_the_read_path(tmp_path):
    """⚑ Without the quarantine the very next read would silently re-apply the values the reload
    just refused — the rejection would be cosmetic."""
    s, f = _secrets(tmp_path, validators={"POLL_INTERVAL": lambda v: v.isdigit()})
    f.write_text(f"ANDREA_SVC_API_KEY={KEY}\nPOLL_INTERVAL=9\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        _post(s, key=KEY)
    good = s.digest("POLL_INTERVAL")
    f.write_text(f"ANDREA_SVC_API_KEY={KEY}\nPOLL_INTERVAL=not-a-number\n")
    assert s.reload().applied is False
    for _ in range(3):                                # every subsequent read
        assert s.digest("POLL_INTERVAL") == good      # keeps the GOOD value
    # ...and a corrected value lifts the quarantine
    f.write_text(f"ANDREA_SVC_API_KEY={KEY}\nPOLL_INTERVAL=11\n")
    assert s.digest("POLL_INTERVAL") != good


# ══════════════════════════════════════════════════════════════════════════════════════════════
# The response body stays a SUPERSET (config-reload-fanout.py must keep working) and value-free
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_the_reload_body_keeps_the_pre_0_3_field_names(tmp_path):
    """scripts/config-reload-fanout.py reads `reloaded`, `changed`, `db_aliases_reconnected` and
    `reason`. Renaming any of them would silently break every rotation fan-out."""
    s, f = _secrets(tmp_path)
    f.write_text(f"ANDREA_SVC_API_KEY={KEY}\nPOLL_INTERVAL=9\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        body = _post(s, key=KEY).data
    for legacy in ("reloaded", "changed", "changed_count", "db_aliases_reconnected"):
        assert legacy in body, legacy
    assert body["reloaded"] is body["applied"]
    assert body["db_aliases_reconnected"] == body["reconnected"]


def test_no_endpoint_body_ever_carries_a_credential_value(tmp_path):
    secret = "ENDPOINT-SECRET-VALUE"
    s, f = _secrets(tmp_path, body=f"ANDREA_SVC_API_KEY={KEY}\nDOCSTORE_TOKEN={secret}\n")
    f.write_text(f"ANDREA_SVC_API_KEY={KEY}\nDOCSTORE_TOKEN={secret}-rotated\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        reload_body = _post(s, key=KEY).data
        status_body = _get_status(s, key=KEY).data
    for blob in (json.dumps(reload_body, default=str), json.dumps(status_body, default=str)):
        assert secret not in blob
        assert KEY not in blob
    assert "DOCSTORE_TOKEN" in json.dumps(reload_body)          # the NAME is there
    assert len(status_body["creds"]["DOCSTORE_TOKEN"]["digest"]) == 16


def test_creds_status_reports_provenance_and_generation(tmp_path):
    s, f = _secrets(tmp_path)
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        body = _get_status(s, key=KEY).data
    assert body["service"] == "endpoint-svc"
    assert body["posture"] == "enforcing"
    assert body["rotatable"] is True
    assert body["creds"]["ANDREA_SVC_API_KEY"]["source"] == str(f)
    assert body["counters"]["creds_unconfigured"] == 0


# ══════════════════════════════════════════════════════════════════════════════════════════════
# Backward compatibility: a service WITHOUT the SECRETS singleton still works
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_a_service_without_secrets_falls_back_to_the_legacy_reload(monkeypatch):
    """uuid_service and pod-gem have their own settings modules and no SECRETS. They must not break:
    the view falls back to the pre-0.3 reload_config()."""
    from andrea_svc_web import config as cfgmod
    from andrea_svc_web import reload as reloadmod

    cfgmod.record_applied({"POLL_INTERVAL": "5"})
    monkeypatch.setattr(reloadmod, "fetch_config", lambda *a, **k: {"POLL_INTERVAL": "7"})
    with override_settings(ANDREA_SVC_KEYS=KEY):            # no SECRETS in settings
        assert credsmod.current() is None
        resp = ReloadConfigView.as_view()(
            rf.post("/admin/reload-config", headers={"X-Agent-Key": KEY}))
    assert resp.status_code == 200
    assert resp.data["reloaded"] is True
    assert resp.data["changed"]["other"] == ["POLL_INTERVAL"]


def test_creds_status_degrades_gracefully_without_secrets():
    with override_settings(ANDREA_SVC_KEYS=KEY):
        resp = CredsStatusView.as_view()(
            rf.get("/admin/creds-status", headers={"X-Agent-Key": KEY}))
    assert resp.status_code == 200
    assert resp.data["posture"] == "unknown"


# ══════════════════════════════════════════════════════════════════════════════════════════════
# /health — additive, and the ONLY new 503 is a declared-required credential
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_health_reports_posture_without_changing_its_status(tmp_path):
    s, _ = _secrets(tmp_path, body="NOTHING=x\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=None, OIDC_ISSUER="", OIDC_JWKS_URL=""):
        resp = HealthView.as_view()(rf.get("/health"))
    assert resp.status_code == 200                     # an unconfigured service is still HEALTHY
    assert resp.data["posture"] == "unconfigured"
    assert "creds_missing" not in resp.data


def test_health_503s_only_for_a_declared_required_credential(tmp_path):
    s, _ = _secrets(tmp_path, required={"DOCSTORE_TOKEN": "docstore auth"})
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=None):
        resp = HealthView.as_view()(rf.get("/health"))
    assert resp.status_code == 503
    assert resp.data["status"] == "degraded"
    assert resp.data["creds_missing"] == ["DOCSTORE_TOKEN"]   # NAMES only


def test_a_config_plane_outage_does_not_evict_a_healthy_service(tmp_path, monkeypatch):
    """Fail-soft is the existing contract and stays the contract: a config-service outage must not
    take a healthy service out of rotation."""
    from andrea_svc_web.config import ConfigFetch
    monkeypatch.setattr(credsmod, "fetch_config_result",
                        lambda *a, **k: ConfigFetch(status="unreachable", detail="URLError"))
    s, _ = _secrets(tmp_path, config_service=True)
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY):
        resp = HealthView.as_view()(rf.get("/health"))
    assert resp.status_code == 200
    assert resp.data["posture"] == "degraded"          # visible, but NOT evicted


def test_health_survives_a_broken_credential_layer(tmp_path):
    class _Broken:
        @property
        def posture(self):
            raise RuntimeError("credential layer exploded")

    with override_settings(SECRETS=_Broken()):
        resp = HealthView.as_view()(rf.get("/health"))
    assert resp.status_code == 200                    # not a ServiceSecrets -> current() is None


# ══════════════════════════════════════════════════════════════════════════════════════════════
# The bootstrap (§4c): a key dropped into a declared file goes live per request
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_a_key_dropped_into_a_declared_file_goes_live_via_the_middleware(tmp_path):
    """Provisioning must not require the endpoint the provisioning would authorize. The middleware
    pulls the key through the mtime-gated read path BEFORE DRF authenticates."""
    from andrea_svc_web.middleware import CredsRefreshMiddleware

    s, f = _secrets(tmp_path, body="NOTHING=x\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=None, OIDC_ISSUER="", OIDC_JWKS_URL=""):
        from django.conf import settings as dj
        assert _post(s).status_code == 503            # closed: no key anywhere

        f.write_text(f"ANDREA_SVC_API_KEY={KEY}\n")   # the operator drops the key in
        CredsRefreshMiddleware(lambda r: None)(rf.post("/x"))
        assert bool(dj.ANDREA_SVC_KEYS) is True       # presence, never the value
        assert _post(s).status_code == 401            # now closed-but-serviceable
        assert _post(s, key=KEY).status_code == 200   # and the new key works


def test_the_middleware_is_a_no_op_without_secrets():
    from andrea_svc_web.middleware import CredsRefreshMiddleware
    sentinel = object()
    assert CredsRefreshMiddleware(lambda r: sentinel)(rf.get("/x")) is sentinel


def test_the_middleware_never_fails_a_request(monkeypatch):
    from andrea_svc_web import middleware as mw

    def boom():
        raise RuntimeError("refresh exploded")

    monkeypatch.setattr(mw, "refresh_auth_settings", boom)
    sentinel = object()
    assert mw.CredsRefreshMiddleware(lambda r: sentinel)(rf.get("/x")) is sentinel


@pytest.mark.parametrize("mode", ["auto", "enforce", "dev-open"])
def test_the_control_plane_is_closed_in_every_auth_mode(tmp_path, mode):
    s, _ = _secrets(tmp_path, body="NOTHING=x\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=None, OIDC_ISSUER="", OIDC_JWKS_URL="",
                           ANDREA_AUTH_MODE=mode):
        assert _post(s).status_code == 503, mode
