"""SP26 — Docker-secrets file fallback in cyclone.secrets.get_secret().

SP25 added a plain env-var tier ahead of the macOS Keychain lookup.
SP26 adds a further tier above that: an `<env_name>_FILE` env var
pointing at a file on disk — the standard Docker-secrets pattern.
The file takes precedence over the plain env var (matches the
``auth/bootstrap.py:_read_secret`` convention).
"""
from __future__ import annotations

import importlib
from pathlib import Path

import pytest


# A secret name we use throughout the unmapped cases. Because it is not
# in ``_ENV_NAME_FOR``, ``get_secret()`` will read it as
# ``os.environ.get("cyclone.test.file.password")`` directly (and the
# ``_FILE`` companion is the same name with ``_FILE`` appended). This
# mirrors SP25's ``test_secrets_envvar.py`` convention.
_UNMAPPED_NAME = "cyclone.test.file.password"


@pytest.fixture
def secrets_module(monkeypatch):
    """Reload cyclone.secrets with a clean keyring state per test."""
    import cyclone.secrets as secrets_mod
    importlib.reload(secrets_mod)
    yield secrets_mod
    importlib.reload(secrets_mod)


def _write_secret_file(tmp_path: Path, name: str, contents: str) -> Path:
    f = tmp_path / name
    f.write_text(contents)
    return f


def test_file_env_var_returns_file_contents(
    tmp_path: Path, monkeypatch, secrets_module,
) -> None:
    """_FILE set, file exists → returns file contents (no Keychain lookup)."""
    f = _write_secret_file(tmp_path, "pw", "from-file\n")
    monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(f))
    monkeypatch.delenv(_UNMAPPED_NAME, raising=False)
    monkeypatch.setattr(
        secrets_module.keyring, "get_password", lambda s, n: None,
    )
    assert secrets_module.get_secret(_UNMAPPED_NAME) == "from-file"


def test_file_env_var_strips_trailing_newline(
    tmp_path: Path, monkeypatch, secrets_module,
) -> None:
    """Docker secret files commonly end with \\n — strip it."""
    f = _write_secret_file(tmp_path, "pw", "supersecret\n")
    monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(f))
    monkeypatch.delenv(_UNMAPPED_NAME, raising=False)
    monkeypatch.setattr(
        secrets_module.keyring, "get_password", lambda s, n: None,
    )
    assert secrets_module.get_secret(_UNMAPPED_NAME) == "supersecret"


def test_file_env_var_strips_leading_and_trailing_whitespace(
    tmp_path: Path, monkeypatch, secrets_module,
) -> None:
    f = _write_secret_file(tmp_path, "pw", "  supersecret  \n")
    monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(f))
    monkeypatch.delenv(_UNMAPPED_NAME, raising=False)
    monkeypatch.setattr(
        secrets_module.keyring, "get_password", lambda s, n: None,
    )
    assert secrets_module.get_secret(_UNMAPPED_NAME) == "supersecret"


def test_file_env_var_wins_over_plain_env_var(
    tmp_path: Path, monkeypatch, secrets_module,
) -> None:
    """When both _FILE and the plain env var are set, _FILE wins."""
    f = _write_secret_file(tmp_path, "pw", "from-file\n")
    monkeypatch.setenv(_UNMAPPED_NAME, "from-env")
    monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(f))
    monkeypatch.setattr(
        secrets_module.keyring, "get_password", lambda s, n: None,
    )
    assert secrets_module.get_secret(_UNMAPPED_NAME) == "from-file"


def test_file_env_var_missing_file_raises_runtime_error(
    tmp_path: Path, monkeypatch, secrets_module,
) -> None:
    """If _FILE points at a non-existent path, raise RuntimeError."""
    missing = tmp_path / "does-not-exist"
    monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", str(missing))
    monkeypatch.delenv(_UNMAPPED_NAME, raising=False)
    monkeypatch.setattr(
        secrets_module.keyring, "get_password", lambda s, n: None,
    )
    with pytest.raises(RuntimeError, match=f"{_UNMAPPED_NAME}_FILE"):
        secrets_module.get_secret(_UNMAPPED_NAME)


def test_file_env_var_empty_string_treated_as_unset(
    monkeypatch, secrets_module,
) -> None:
    """An empty _FILE env var falls through to the plain env var (SP25 rule)."""
    monkeypatch.setenv(f"{_UNMAPPED_NAME}_FILE", "")
    monkeypatch.setenv(_UNMAPPED_NAME, "from-env")
    monkeypatch.setattr(
        secrets_module.keyring, "get_password", lambda s, n: None,
    )
    assert secrets_module.get_secret(_UNMAPPED_NAME) == "from-env"


def test_gainwell_file_env_var_full_chain(
    tmp_path: Path, monkeypatch, secrets_module,
) -> None:
    """Setting CYCLONE_SFTP_PASSWORD_FILE makes get_secret('sftp.gainwell.password')
    return the file's contents — the operator-visible chain works end-to-end."""
    f = _write_secret_file(tmp_path, "sftp_pw", "real-mft-password\n")
    monkeypatch.setenv("CYCLONE_SFTP_PASSWORD_FILE", str(f))
    monkeypatch.delenv("CYCLONE_SFTP_PASSWORD", raising=False)
    monkeypatch.setattr(
        secrets_module.keyring, "get_password", lambda s, n: None,
    )
    assert secrets_module.get_secret("sftp.gainwell.password") == "real-mft-password"
