"""FAIL-OPEN negative controls — the four cases the 0.3.0 suite passed BY OMITTING.

Every test in this file was written BEFORE the fix and observed FAILING against the shipped
0.3.0 tree. That is the entire point: the 108/108 green suite was green because it exercised
`dev-open` only with `ANDREA_SVC_KEYS=None` (test_creds.py:400, test_creds_endpoints.py:279) —
the CONFIGURED case, the one where the flag actually removes protection, was never asked.

  D1  ANDREA_AUTH_MODE=dev-open disabled the DATA PLANE of a fully-CONFIGURED service.
      permissions.py returned True before `configured` was ever consulted, so a service holding
      a real key served anonymous 200.
  D2  the dev-open switch was reachable FROM THE CONFIG PLANE. auth_mode() falls back to
      os.environ; settings.py reads ANDREA_AUTH_MODE from env; and apply_to_env() — on by
      default — wrote ARBITRARY config-service payload keys into os.environ BEFORE that read.
      One config-service row was an auth kill switch for the whole fleet. The same reachability
      existed through creds._mirror_to_settings(), which mirrored any changed name onto a
      same-named Django setting — including ANDREA_AUTH_MODE.
  D3  credential WITHDRAWAL blanked settings.ANDREA_SVC_KEYS -> anonymous 200, driven by the
      PASSIVE path (an unauthenticated GET), and a 0-byte read triggered it: parse_env_file
      reports present=True for an empty-but-readable file, so a non-atomic rewrite caught
      mid-flight (or a stray `> file`) opened the service.

⚑ NO TEST HERE ASSERTS ON A CREDENTIAL VALUE (rm#206). The assertions are status codes,
presence (`bool(...)`), digests and provenance. The sentinels are obviously-not-real strings so
that a leak in a failure message is unmistakable.
"""
import os

import pytest
from django.conf import settings as dj
from django.test import override_settings
from rest_framework.response import Response
from rest_framework.test import APIRequestFactory
from rest_framework.views import APIView

from andrea_svc_web import config as configmod
from andrea_svc_web import creds as credsmod
from andrea_svc_web import permissions as permmod
from andrea_svc_web.auth import ServiceKeyRequiredIfConfigured, ServiceOrOIDCAuthentication
from andrea_svc_web.creds import ServiceSecrets
from andrea_svc_web.middleware import CredsRefreshMiddleware

rf = APIRequestFactory()

KEY = "failopen-control-sentinel-key"          # obviously not a real credential
UNCONFIGURED = dict(ANDREA_SVC_KEYS=None, OIDC_ISSUER="", OIDC_JWKS_URL="")


class _DataPlaneView(APIView):
    """A service's ORDINARY endpoint: the shape every migrated service's business API has."""

    authentication_classes = [ServiceOrOIDCAuthentication]
    permission_classes = [ServiceKeyRequiredIfConfigured]

    def get(self, request):
        # A stand-in for whatever the service serves. Never a credential.
        return Response({"business_data": "served"})


def _data_get(key=None):
    """One request through the REAL stack: CredsRefreshMiddleware (which is what makes the
    passive read path run on an unauthenticated GET) and then the full DRF auth+permission cycle."""
    headers = {"X-Agent-Key": key} if key else {}
    req = rf.get("/data", **({"headers": headers} if headers else {}))
    CredsRefreshMiddleware(lambda r: None)(req)
    return _DataPlaneView.as_view()(req)


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


@pytest.fixture(autouse=True)
def _no_env_leak():
    """apply_to_env() writes the REAL os.environ. Snapshot/restore so a refusal test cannot leak an
    auth-control key into the rest of the session (which would itself be a fail-open)."""
    before = dict(os.environ)
    yield
    for k in set(os.environ) - set(before):
        os.environ.pop(k, None)
    os.environ.update(before)


# ══════════════════════════════════════════════════════════════════════════════════════════════
# D1 — dev-open must NEVER weaken a service that HAS credentials
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_dev_open_does_not_open_the_data_plane_of_a_configured_service():
    """THE omitted case. `dev-open` was consulted BEFORE `configured`, so a service holding a real
    key answered anonymous 200 on its whole data plane."""
    perm = ServiceKeyRequiredIfConfigured()
    req = rf.get("/data")
    req.user = None
    with override_settings(ANDREA_SVC_KEYS=KEY, OIDC_ISSUER="", OIDC_JWKS_URL="",
                           ANDREA_AUTH_MODE="dev-open"):
        assert perm.has_permission(req, None) is False        # -> DRF 401


def test_dev_open_configured_service_answers_401_not_200_through_the_full_drf_cycle(tmp_path):
    """The unit assertion above proves the decision; this proves the WIRING — a permission class
    is only as good as the view it is mounted on (that is why rm#334 needed both)."""
    s, _ = _secrets(tmp_path, f"ANDREA_SVC_API_KEY={KEY}\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY, OIDC_ISSUER="", OIDC_JWKS_URL="",
                           ANDREA_AUTH_MODE="dev-open"):
        anon = _data_get()
        assert anon.status_code == 401
        assert "business_data" not in (anon.data or {})       # and the payload never left
        assert _data_get(key=KEY).status_code == 200          # the real key still works


def test_dev_open_still_opens_a_genuinely_unconfigured_service():
    """POSITIVE control for the fix: the INTENTIONAL dev/local default must survive. dev-open is
    legitimate for a service that has no credential at all — that is the case it was built for."""
    perm = ServiceKeyRequiredIfConfigured()
    req = rf.get("/data")
    req.user = None
    with override_settings(ANDREA_AUTH_MODE="dev-open", **UNCONFIGURED):
        assert perm.has_permission(req, None) is True


# ══════════════════════════════════════════════════════════════════════════════════════════════
# D2 — the auth switch must NOT be reachable from the config plane
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_apply_to_env_refuses_auth_control_keys():
    """apply_to_env() wrote arbitrary payload keys into os.environ. settings.py then read
    ANDREA_AUTH_MODE out of that same os.environ, one screen further down — so a single
    config-service row disabled auth for every service that overlays."""
    for k in ("ANDREA_AUTH_MODE", "ANDREA_SVC_KEYS", "ANDREA_SVC_API_KEY", "OIDC_ISSUER",
              "OIDC_JWKS_URL", "ANDREA_CREDS_HONOR_WITHDRAWAL", "CFG_BENIGN"):
        os.environ.pop(k, None)
    applied = configmod.apply_to_env("svc", prefetched={
        "ANDREA_AUTH_MODE": "dev-open",              # the kill switch
        "ANDREA_SVC_KEYS": "attacker-chosen",        # would re-point the accepted credential
        "ANDREA_SVC_API_KEY": "attacker-chosen",
        "OIDC_ISSUER": "https://issuer.invalid",     # would let anyone mint an accepted token
        "OIDC_JWKS_URL": "https://issuer.invalid/jwks",
        "ANDREA_CREDS_HONOR_WITHDRAWAL": "1",        # would arm the withdrawal path remotely
        "CFG_BENIGN": "9",                           # an ordinary knob: still applied
    })
    for refused in ("ANDREA_AUTH_MODE", "ANDREA_SVC_KEYS", "ANDREA_SVC_API_KEY", "OIDC_ISSUER",
                    "OIDC_JWKS_URL", "ANDREA_CREDS_HONOR_WITHDRAWAL"):
        assert refused not in os.environ, refused
    assert os.environ.get("CFG_BENIGN") == "9"       # backward compat: ordinary knobs still overlay
    assert applied == 1                              # the count is what was APPLIED, not offered


def test_one_config_service_row_cannot_flip_the_auth_mode_for_the_fleet():
    """End-to-end shape of the fleet-wide defect: payload -> os.environ -> auth_mode() -> anon 200
    on ~23 services. The chain must be broken at the first link."""
    os.environ.pop("ANDREA_AUTH_MODE", None)
    configmod.apply_to_env("svc", prefetched={"ANDREA_AUTH_MODE": "dev-open"})
    assert os.environ.get("ANDREA_AUTH_MODE") is None
    # settings has no ANDREA_AUTH_MODE in the hermetic test config, so auth_mode() takes exactly
    # the env fallback the defect exploited.
    assert permmod.auth_mode() == "auto"


def test_config_service_payload_cannot_mirror_the_auth_mode_onto_settings(tmp_path):
    """The SECOND route to the same switch: creds._mirror_to_settings() mirrors any changed name
    onto a same-named Django setting, and settings.ANDREA_AUTH_MODE is such a name. A tier the
    config plane controls must not be able to write the setting the auth path reads."""
    s, f = _secrets(tmp_path, "ANDREA_AUTH_MODE=auto\nOTHER=1\n")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY, OIDC_ISSUER="", OIDC_JWKS_URL="",
                           ANDREA_AUTH_MODE="auto"):
        f.write_text("ANDREA_AUTH_MODE=dev-open\nOTHER=2\n")
        s.reload()
        assert dj.ANDREA_AUTH_MODE == "auto"
        assert permmod.auth_mode() == "auto"
        assert _data_get().status_code == 401         # the data plane stayed closed


# ══════════════════════════════════════════════════════════════════════════════════════════════
# D3 — a withdrawal must never blank a live credential, and an empty read is not a withdrawal
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_withdrawal_never_blanks_the_live_service_key(tmp_path):
    """ANDREA_CREDS_HONOR_WITHDRAWAL=1 + a rewrite that drops the key blanked
    settings.ANDREA_SVC_KEYS, and the restore guard (`if key:`) could never fire on an empty
    string. The service went from enforcing to anonymous-200 without anybody authenticating —
    and the trigger was an unauthenticated GET on the PASSIVE path."""
    s, f = _secrets(tmp_path, f"ANDREA_SVC_KEYS={KEY}\nOTHER=1\n",
                    env={"ANDREA_CREDS_HONOR_WITHDRAWAL": "1"})
    before = s.digest("ANDREA_SVC_KEYS")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY, OIDC_ISSUER="", OIDC_JWKS_URL=""):
        assert _data_get().status_code == 401                  # start: enforcing
        f.write_text("OTHER=2\n")                              # the key is withdrawn upstream
        assert _data_get().status_code == 401                  # passive path must NOT open it
        assert bool(dj.ANDREA_SVC_KEYS) is True                # presence, never the value
        assert s.digest("ANDREA_SVC_KEYS") == before           # and not blanked underneath


def test_a_truncated_env_file_is_not_a_withdrawal(tmp_path):
    """The race: a non-atomic rewrite observed mid-flight (or a stray `> file`) reads as 0 bytes.
    parse_env_file returns present=True for it, and the withdrawal decision never consulted that
    flag — so an empty read blanked the key and opened the service.
    rules/an-empty-read-is-not-evidence-of-absence: a file that reads empty is NOT evidence the
    secret was withdrawn."""
    s, f = _secrets(tmp_path, f"ANDREA_SVC_KEYS={KEY}\nOTHER=1\n",
                    env={"ANDREA_CREDS_HONOR_WITHDRAWAL": "1"})
    before = s.digest("ANDREA_SVC_KEYS")
    with override_settings(SECRETS=s, ANDREA_SVC_KEYS=KEY, OIDC_ISSUER="", OIDC_JWKS_URL=""):
        assert _data_get().status_code == 401
        f.write_text("")                                       # caught mid-truncation
        assert _data_get().status_code == 401                  # STILL closed
        assert bool(dj.ANDREA_SVC_KEYS) is True
        assert s.digest("ANDREA_SVC_KEYS") == before
    assert s.status()["tiers"][str(f)] == "empty"              # readable, but carrying nothing


def test_an_empty_read_does_not_withdraw_an_ordinary_credential_either(tmp_path):
    """The rule is about EVIDENCE, not about the auth key specifically: zero parsed keys is not a
    removal, whatever the name. It must still be REPORTED (withdrawal is never silent)."""
    s, f = _secrets(tmp_path, "TOK=a\n", env={"ANDREA_CREDS_HONOR_WITHDRAWAL": "1"})
    before = s.digest("TOK")
    f.write_text("")
    report = s.reload()
    assert "TOK" in report.withdrawn                           # VISIBLE
    assert s.get("TOK") is not None                            # but NOT stripped
    assert s.digest("TOK") == before


# ══════════════════════════════════════════════════════════════════════════════════════════════
# POSITIVE controls — the fix must not cost the fleet anything it actually uses
# ══════════════════════════════════════════════════════════════════════════════════════════════
def test_the_service_key_still_arrives_from_the_config_service(tmp_path, monkeypatch):
    """apply_to_env() no longer writes the key into os.environ. That must NOT mean the key stops
    being delivered: ServiceSecrets resolves the same tier-1 payload in memory, WITH provenance,
    which is where settings.py reads it from. Refusing the env write moves the key to the right
    door; it does not close the door."""
    monkeypatch.setattr(credsmod, "fetch_config_result",
                        lambda *a, **k: configmod.ConfigFetch(status="ok",
                                                              payload={"ANDREA_SVC_API_KEY": KEY}))
    s = ServiceSecrets("failopen-svc", config_service=True, env={},
                       state_path=str(tmp_path / "stamp.json"))
    s.load()
    assert s.get("ANDREA_SVC_API_KEY") is not None                  # presence, never the value
    assert s.source_of("ANDREA_SVC_API_KEY") == credsmod.CONFIG_SERVICE
    assert s.configured is True


def test_the_legacy_reload_path_still_rotates_a_key_but_refuses_policy(monkeypatch):
    """The unmigrated services (no settings.SECRETS) rotate through reload.reload_config(), whose
    only delivery mechanism IS os.environ. Refusing the KEY there would break rotation for exactly
    the services that have no other path — so policy is refused and material is delivered."""
    from andrea_svc_web import reload as reloadmod

    monkeypatch.delenv("ANDREA_SVC_KEYS", raising=False)
    os.environ.pop("ANDREA_AUTH_MODE", None)
    configmod.record_applied({"ANDREA_SVC_API_KEY": "previous-sentinel"})
    monkeypatch.setattr(reloadmod, "fetch_config",
                        lambda *a, **k: {"ANDREA_SVC_API_KEY": "rotated-sentinel",
                                         "ANDREA_AUTH_MODE": "dev-open"})
    stale = credsmod.digest_of("previous-sentinel")
    with override_settings(ANDREA_SVC_KEYS="previous-sentinel"):
        out = reloadmod.reload_config()
        # rotation still works — asserted by CHANGE (digest inequality), never by value
        assert bool(dj.ANDREA_SVC_KEYS) is True
        assert credsmod.digest_of(dj.ANDREA_SVC_KEYS) != stale
    assert out["refused_auth_control"] == ["ANDREA_AUTH_MODE"]      # policy did not
    assert "ANDREA_AUTH_MODE" not in os.environ
    assert "ANDREA_AUTH_MODE" not in configmod.applied_snapshot()   # nor in the diff baseline


def test_the_env_fallback_may_harden_the_auth_mode_but_never_weaken_it(monkeypatch):
    """auth_mode() falls back to os.environ for services that do not inherit this settings module.
    That fallback is the tier this contract trusts least, so it may select a HARDER posture and
    never a softer one."""
    assert not hasattr(dj, "ANDREA_AUTH_MODE")                      # the fallback is what runs here
    monkeypatch.setenv("ANDREA_AUTH_MODE", "enforce")
    assert permmod.auth_mode() == "enforce"                         # hardening: honoured
    monkeypatch.setenv("ANDREA_AUTH_MODE", "dev-open")
    assert permmod.auth_mode() == "auto"                            # weakening: refused


def test_the_opt_in_allowlist_makes_apply_to_env_default_deny(monkeypatch):
    """The migration path off the denylist floor: one env var per unit, no code change."""
    monkeypatch.setenv(configmod.ENV_ALLOW_ENV, "CFG_WANTED")
    for k in ("CFG_WANTED", "CFG_UNDECLARED"):
        os.environ.pop(k, None)
    assert configmod.apply_to_env("svc", prefetched={"CFG_WANTED": "1", "CFG_UNDECLARED": "2"}) == 1
    assert os.environ.get("CFG_WANTED") == "1"
    assert "CFG_UNDECLARED" not in os.environ


def test_an_unreachable_config_service_is_not_a_withdrawal(tmp_path, monkeypatch):
    """Guard on the same evidence rule from the other direction (this one already held): a tier-1
    outage must not read as 'every config-service credential was removed'."""
    monkeypatch.setattr(credsmod, "fetch_config_result",
                        lambda *a, **k: configmod.ConfigFetch(status="ok", payload={"TOK": "a"}))
    s = ServiceSecrets("failopen-svc", config_service=True, env={"ANDREA_CREDS_HONOR_WITHDRAWAL": "1"},
                       state_path=str(tmp_path / "stamp.json"))
    s.load()
    before = s.digest("TOK")
    monkeypatch.setattr(credsmod, "fetch_config_result",
                        lambda *a, **k: configmod.ConfigFetch(status="unreachable", detail="OSError"))
    s.reload()
    assert s.get("TOK") is not None and s.digest("TOK") == before
