"""Tests for DataSanitizer.redact_settings_snapshot.

The plain DataSanitizer.redact() does NOT redact secrets in nested-with-metadata
snapshots like {"llm.openai.api_key": {"value": "sk-...", "ui_element": "password"}}
— the outer compound key isn't in the sensitive-name set, and the inner key
"value" isn't sensitive either. redact_settings_snapshot fixes that for the
specific shape produced by SettingsManager.get_all_settings().
"""

from local_deep_research.security.data_sanitizer import DataSanitizer


def test_redacts_value_when_ui_element_is_password():
    snap = {
        "llm.openai.api_key": {
            "value": "sk-secret123",
            "ui_element": "password",
            "type": "LLM",
        }
    }
    out = DataSanitizer.redact_settings_snapshot(snap)
    assert out["llm.openai.api_key"]["value"] == "[REDACTED]"


def test_preserves_metadata_alongside_redacted_value():
    """ui_element/type/etc. survive so YAML diffs still show the field exists."""
    snap = {
        "llm.openai.api_key": {
            "value": "sk-secret",
            "ui_element": "password",
            "type": "LLM",
            "description": "OpenAI key",
        }
    }
    out = DataSanitizer.redact_settings_snapshot(snap)
    assert out["llm.openai.api_key"]["ui_element"] == "password"
    assert out["llm.openai.api_key"]["type"] == "LLM"
    assert out["llm.openai.api_key"]["description"] == "OpenAI key"


def test_does_not_redact_non_secret_settings():
    snap = {
        "search.fetch.mode": {
            "value": "summary_focus_query",
            "ui_element": "select",
            "type": "SEARCH",
        }
    }
    out = DataSanitizer.redact_settings_snapshot(snap)
    assert out["search.fetch.mode"]["value"] == "summary_focus_query"


def test_empty_secret_value_stays_empty():
    """An unset API key reads as "" — leaving it empty is more useful than
    "[REDACTED]" when diffing two runs to spot which had the key set."""
    snap = {"llm.lmstudio.api_key": {"value": "", "ui_element": "password"}}
    out = DataSanitizer.redact_settings_snapshot(snap)
    assert out["llm.lmstudio.api_key"]["value"] == ""


def test_none_secret_value_stays_none():
    snap = {"llm.lmstudio.api_key": {"value": None, "ui_element": "password"}}
    out = DataSanitizer.redact_settings_snapshot(snap)
    assert out["llm.lmstudio.api_key"]["value"] is None


def test_defense_in_depth_via_key_suffix():
    """ui_element=text + key suffix in DEFAULT_SENSITIVE_KEYS still redacts.

    Catches developer error: a future plugin author who registers
    `plugin.x.api_key` with the wrong ui_element should still have the
    secret redacted because `api_key` is in DEFAULT_SENSITIVE_KEYS.
    """
    snap = {"plugin.x.api_key": {"value": "sk-leaked", "ui_element": "text"}}
    out = DataSanitizer.redact_settings_snapshot(snap)
    assert out["plugin.x.api_key"]["value"] == "[REDACTED]"


def test_input_is_not_mutated():
    """Pure-function contract — the in-memory snapshot used by the running
    benchmark thread must remain unredacted; only the persisted copy is
    redacted."""
    snap = {
        "llm.openai.api_key": {
            "value": "sk-secret",
            "ui_element": "password",
        }
    }
    DataSanitizer.redact_settings_snapshot(snap)
    assert snap["llm.openai.api_key"]["value"] == "sk-secret"


def test_passes_through_non_metadata_entries():
    """Tolerates flat-shape entries (bare key→value) so the helper is safe
    on mixed snapshots without crashing."""
    snap = {"flat_key": "raw_value", "no_value_key": {"ui_element": "text"}}
    out = DataSanitizer.redact_settings_snapshot(snap)
    assert out["flat_key"] == "raw_value"
    assert out["no_value_key"] == {"ui_element": "text"}


def test_passes_through_non_dict_input():
    """Defensive — a None or list snapshot returns unchanged."""
    assert DataSanitizer.redact_settings_snapshot(None) is None
    assert DataSanitizer.redact_settings_snapshot([1, 2, 3]) == [1, 2, 3]


def test_custom_redaction_text():
    snap = {"llm.openai.api_key": {"value": "sk-x", "ui_element": "password"}}
    out = DataSanitizer.redact_settings_snapshot(snap, redaction_text="***")
    assert out["llm.openai.api_key"]["value"] == "***"


def test_is_sensitive_setting_predicate():
    """The shared predicate the GET redactor and the write-back guards both
    use: password ui_element OR a sensitive key suffix."""
    # ui_element wins regardless of key
    assert DataSanitizer.is_sensitive_setting("anything.here", "password")
    # sensitive suffix wins regardless of ui_element
    assert DataSanitizer.is_sensitive_setting("llm.openai.api_key", "text")
    assert DataSanitizer.is_sensitive_setting("x.password", None)
    assert DataSanitizer.is_sensitive_setting("x.secret", "textarea")
    # neither -> not sensitive
    assert not DataSanitizer.is_sensitive_setting("llm.model", "text")
    assert not DataSanitizer.is_sensitive_setting("search.tool", None)


def test_redactor_and_predicate_agree():
    """Anything the predicate calls sensitive (and has a non-empty value)
    must be redacted by redact_settings_snapshot — they share one source."""
    snap = {
        "llm.openai.api_key": {"value": "sk-x", "ui_element": "text"},
        "foo.password": {"value": "pw", "ui_element": "text"},
        "llm.model": {"value": "gpt", "ui_element": "text"},
    }
    out = DataSanitizer.redact_settings_snapshot(snap)
    for key, entry in snap.items():
        sensitive = DataSanitizer.is_sensitive_setting(key, entry["ui_element"])
        if sensitive:
            assert out[key]["value"] == DataSanitizer.REDACTION_TEXT
        else:
            assert out[key]["value"] == entry["value"]


# ---------------------------------------------------------------------------
# redact_value — the single-value primitive that the singular GET, the bulk
# GET and redact_settings_snapshot all delegate to.
# ---------------------------------------------------------------------------


def test_redact_value_masks_password_ui_element():
    """A set secret on a password-typed setting returns the sentinel."""
    assert (
        DataSanitizer.redact_value("llm.openai.api_key", "password", "sk-real")
        == DataSanitizer.REDACTION_TEXT
    )


def test_redact_value_masks_by_suffix_when_no_ui_element():
    """The bulk GET has no ui_element, so redact_value falls back to the
    suffix arm of the predicate (key ends in a sensitive name)."""
    assert (
        DataSanitizer.redact_value("llm.openai.api_key", None, "sk-real")
        == DataSanitizer.REDACTION_TEXT
    )


def test_redact_value_passes_through_non_secret():
    """A non-sensitive setting is returned unchanged."""
    assert DataSanitizer.redact_value("llm.model", "text", "gpt") == "gpt"


def test_redact_value_leaves_empty_values_readable():
    """Empty values are never masked — the UI must tell 'unset' from 'set'
    without the sentinel implying a secret exists."""
    for empty in (None, "", [], {}):
        assert (
            DataSanitizer.redact_value("llm.openai.api_key", "password", empty)
            == empty
        )


def test_redact_value_respects_custom_redaction_text():
    """Callers can override the sentinel."""
    assert (
        DataSanitizer.redact_value(
            "x.password", None, "pw", redaction_text="***"
        )
        == "***"
    )
