"""Tests for ``DjangoSettingsBridge`` — typed read of django.conf.settings."""

from __future__ import annotations

import pytest
from django.test import override_settings

from mgf.django import DjangoSettingsBridge
from mgf.django._errors import DjangoSettingTypeError


class TestDjangoSettingsBridge:
    def test_from_django_reads_live_settings(self) -> None:
        bridge = DjangoSettingsBridge.from_django()

        # Conftest configures these:
        assert bridge.debug is False
        assert "*" in bridge.allowed_hosts
        assert "django.contrib.contenttypes" in bridge.installed_apps
        assert "mgf.django" in bridge.installed_apps
        assert bridge.time_zone == "UTC"
        assert bridge.use_tz is True
        assert bridge.language_code == "en-us"

    def test_secret_key_never_in_bridge(self) -> None:
        # The bridge MUST NOT carry SECRET_KEY (too sensitive).
        # It exposes a presence boolean instead.
        bridge = DjangoSettingsBridge.from_django()

        assert bridge.secret_key_present is True
        assert not hasattr(bridge, "secret_key")

        # Defensive: dumping the bridge MUST NOT expose the secret.
        dumped = bridge.model_dump_json()
        assert "test-secret-key-not-for-production" not in dumped

    def test_mgf_common_settings_exposed(self) -> None:
        bridge = DjangoSettingsBridge.from_django()
        assert bridge.mgf_app_name == "test-app"
        assert bridge.mgf_app_version == "0.0.1"

    def test_bridge_is_a_baseappsettings(self) -> None:
        # AP-04 contract: bridge MUST be a BaseAppSettings so
        # ``mgf-common explain`` can introspect it.
        from mgf.common.config import BaseAppSettings

        bridge = DjangoSettingsBridge.from_django()
        assert isinstance(bridge, BaseAppSettings)

    def test_bridge_is_constructable_directly(self) -> None:
        # The bridge can be constructed from kwargs (not just from_django).
        # Useful in tests where the consumer doesn't want to mock Django.
        bridge = DjangoSettingsBridge(
            debug=True,
            allowed_hosts=("example.com",),
            installed_apps=("myapp",),
            secret_key_present=True,
        )
        assert bridge.debug is True
        assert bridge.allowed_hosts == ("example.com",)


class TestSecretKeyContainment:
    """SH-03: SECRET_KEY MUST NEVER appear in any bridge output, on
    any field, in any serialised form. A future refactor that adds
    ``secret_key: str`` to the bridge would silently leak the secret
    via ``mgf-common explain`` — that's the surface where a regression
    would land on an operator's screen. This test class is the
    contract that catches such a refactor at PR time.

    Tests run against the conftest-configured Django, where
    ``SECRET_KEY = "test-secret-key-not-for-production"``.
    """

    _SECRET = "test-secret-key-not-for-production"

    def test_no_secret_key_field_on_bridge(self) -> None:
        """The bridge MUST NOT expose a `secret_key` attribute at all."""
        bridge = DjangoSettingsBridge.from_django()
        assert not hasattr(bridge, "secret_key"), (
            "SH-03: DjangoSettingsBridge now exposes a `secret_key` "
            "field. SECRET_KEY MUST stay out of the bridge — use the "
            "`secret_key_present: bool` presence-only signal instead. "
            "If a new field genuinely needs the raw value, route it "
            "through mgf-common's vault, not the bridge."
        )

    def test_secret_not_in_model_dump_json(self) -> None:
        bridge = DjangoSettingsBridge.from_django()
        dumped = bridge.model_dump_json()
        assert self._SECRET not in dumped, (
            f"SH-03: SECRET_KEY appeared in `bridge.model_dump_json()`. "
            f"The bridge is consumed by `mgf-common explain` which prints "
            f"to operator terminals. Output: {dumped!r}"
        )

    def test_secret_not_in_model_dump(self) -> None:
        """Same check, on the dict form (used by structured logging)."""
        bridge = DjangoSettingsBridge.from_django()
        dumped = bridge.model_dump()
        # Flatten the dict to strings and search.
        flat = repr(dumped)
        assert self._SECRET not in flat, (
            f"SH-03: SECRET_KEY appeared in `bridge.model_dump()`. Output: {flat!r}"
        )

    def test_secret_not_in_repr(self) -> None:
        bridge = DjangoSettingsBridge.from_django()
        as_repr = repr(bridge)
        assert self._SECRET not in as_repr, (
            f"SH-03: SECRET_KEY appeared in `repr(bridge)`. "
            f"repr is what a `breakpoint()` user sees. Output: {as_repr!r}"
        )

    def test_secret_not_in_str(self) -> None:
        bridge = DjangoSettingsBridge.from_django()
        as_str = str(bridge)
        assert self._SECRET not in as_str, (
            f"SH-03: SECRET_KEY appeared in `str(bridge)`. Output: {as_str!r}"
        )

    def test_secret_not_in_any_exposed_field(self) -> None:
        """Iterates every declared field — catches the case where a
        future refactor adds a new field that happens to interpolate
        SECRET_KEY (e.g., a hash, a derived signing key, a JWT issuer
        string built from SECRET_KEY). The presence-only `bool` is the
        ONLY signal the bridge may carry about SECRET_KEY's state.
        """
        bridge = DjangoSettingsBridge.from_django()
        for field_name in type(bridge).model_fields:
            value = getattr(bridge, field_name)
            # Stringify each value and search for the secret substring.
            # Covers strings, tuples-of-strings, anything that has a
            # repr. We want to catch the case where SECRET_KEY ended
            # up embedded inside another field accidentally.
            value_str = repr(value)
            assert self._SECRET not in value_str, (
                f"SH-03: SECRET_KEY appeared in field `{field_name}`. Value: {value_str!r}"
            )


# ---------------------------------------------------------------------------
# RA-04: non-string MGF_COMMON_APP_NAME / _VERSION is rejected with TypeError.
# ---------------------------------------------------------------------------


class TestNonStringMgfSettingsRejected:
    """RA-04: previously ``_str_or_none`` called ``str(value)`` on any
    non-None input — a list-valued MGF_COMMON_APP_NAME silently became
    a stringified repr. The bridge now rejects non-str types so the
    misconfiguration fails loud at app-config time.
    """

    @override_settings(MGF_COMMON_APP_NAME=["foo"])
    def test_list_app_name_rejected(self) -> None:
        with pytest.raises(DjangoSettingTypeError, match=r"must be a str or None"):
            DjangoSettingsBridge.from_django()

    @override_settings(MGF_COMMON_APP_VERSION=42)
    def test_int_app_version_rejected(self) -> None:
        with pytest.raises(DjangoSettingTypeError, match=r"must be a str or None"):
            DjangoSettingsBridge.from_django()

    @override_settings(MGF_COMMON_APP_NAME="my-app")
    def test_str_app_name_accepted(self) -> None:
        bridge = DjangoSettingsBridge.from_django()
        assert bridge.mgf_app_name == "my-app"
