"""v0.4 Phase D pin tests.

Typed vault configs (the closed-box headliner). Three new public
classes — :class:`EnvVaultConfig`, :class:`KeyringVaultConfig`,
:class:`EncryptedFileVaultConfig` — replace the v0.3
``VaultSettings.config: dict[str, Any]`` leak. Built-in backends
now register a ``config_model``, so a dict-shaped ``config`` passed
to :class:`VaultSettings` is validated and coerced to the typed
instance at construction. Custom backends registered without a
``config_model`` keep the bare-dict shape; the factory owns its
own validation.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from pydantic import ValidationError

from mgf.common._warnings import MgfDeprecationWarning
from mgf.common.settings import VaultSettings
from mgf.common.vault import (
    BuiltinVaultBackendConfig,  # noqa: F401 — pin re-export
    EncryptedFileVault,
    EncryptedFileVaultConfig,
    EnvVault,
    EnvVaultConfig,
    KeyringVaultConfig,
    register_vault_backend,
    unregister_vault_backend,
)
from mgf.common.vault._registry import _registered_vault_backends

if TYPE_CHECKING:
    from pathlib import Path


# ---------------------------------------------------------------------------
# D.1 — Typed config models
# ---------------------------------------------------------------------------


class TestEnvVaultConfig:
    """:class:`EnvVaultConfig` — typed config for the ``env`` backend."""

    def test_default_prefix(self) -> None:
        c = EnvVaultConfig()
        assert c.backend_type == "env"
        assert c.prefix == "MGF_VAULT_"

    def test_custom_prefix(self) -> None:
        c = EnvVaultConfig(prefix="MYAPP_VAULT_")
        assert c.prefix == "MYAPP_VAULT_"

    def test_extra_field_forbidden(self) -> None:
        """Typo'd field name MUST fail loudly at construction."""
        with pytest.raises(ValidationError):
            EnvVaultConfig(prefx="MYAPP_VAULT_")  # type: ignore[call-arg]

    def test_frozen_after_construction(self) -> None:
        """Configs are immutable — mutation raises."""
        c = EnvVaultConfig()
        with pytest.raises(ValidationError):
            c.prefix = "NEW_"  # type: ignore[misc]

    def test_backend_type_is_pinned(self) -> None:
        """``backend_type`` is a Literal — only ``"env"`` accepted."""
        with pytest.raises(ValidationError):
            EnvVaultConfig(backend_type="keyring")  # type: ignore[arg-type]


class TestKeyringVaultConfig:
    """:class:`KeyringVaultConfig` — typed config for the ``keyring`` backend."""

    def test_default_service(self) -> None:
        c = KeyringVaultConfig()
        assert c.backend_type == "keyring"
        assert c.service == "mgf-common"

    def test_custom_service(self) -> None:
        c = KeyringVaultConfig(service="myapp")
        assert c.service == "myapp"

    def test_extra_field_forbidden(self) -> None:
        with pytest.raises(ValidationError):
            KeyringVaultConfig(srvc="myapp")  # type: ignore[call-arg]


class TestEncryptedFileVaultConfig:
    """:class:`EncryptedFileVaultConfig` — typed config for the ``file`` backend."""

    def test_required_fields(self, tmp_path: Path) -> None:
        c = EncryptedFileVaultConfig(
            path=tmp_path / "vault.dat",
            passphrase="hunter2",
        )
        assert c.backend_type == "file"
        assert c.path == tmp_path / "vault.dat"
        # passphrase is a SecretStr (CF-05/SC-01) — masked on dump, real
        # value at the crypto boundary via .get_secret_value().
        assert c.passphrase.get_secret_value() == "hunter2"

    def test_path_required(self) -> None:
        """``path`` is required — pydantic enforces (vs the v0.3
        runtime check in the factory)."""
        with pytest.raises(ValidationError):
            EncryptedFileVaultConfig(passphrase="hunter2")  # type: ignore[call-arg]

    def test_passphrase_required(self, tmp_path: Path) -> None:
        with pytest.raises(ValidationError):
            EncryptedFileVaultConfig(path=tmp_path / "v")  # type: ignore[call-arg]


# ---------------------------------------------------------------------------
# D.2 — Built-in factories accept typed configs
# ---------------------------------------------------------------------------


class TestBuiltinFactoriesWithTypedConfigs:
    """Built-in factories require typed configs (the legacy
    bare-dict-with-deprecation-warning shape was removed pre-v1.0;
    the dict→typed coercion now happens at the
    :class:`VaultSettings` layer via the backend's registered
    ``config_model``)."""

    def test_env_factory_accepts_typed(self) -> None:
        registry = _registered_vault_backends()
        v = registry["env"](EnvVaultConfig(prefix="X_"))
        assert isinstance(v, EnvVault)
        assert v._prefix == "X_"

    def test_env_factory_typed_emits_no_warning(
        self,
        recwarn: pytest.WarningsRecorder,
    ) -> None:
        """Typed config path MUST NOT emit MgfDeprecationWarning."""
        registry = _registered_vault_backends()
        registry["env"](EnvVaultConfig(prefix="X_"))
        deprecations = [w for w in recwarn.list if issubclass(w.category, MgfDeprecationWarning)]
        assert deprecations == [], (
            f"Typed config triggered a deprecation warning: "
            f"{[str(d.message) for d in deprecations]}"
        )

    def test_file_factory_typed_path(self, tmp_path: Path) -> None:
        """Typed config bypasses any factory-level runtime checks —
        pydantic already enforced the shape."""
        registry = _registered_vault_backends()
        cfg = EncryptedFileVaultConfig(
            path=tmp_path / "v.dat",
            passphrase="pass",
        )
        v = registry["file"](cfg)
        assert isinstance(v, EncryptedFileVault)

    def test_file_config_missing_required_keys_raises_at_construction(
        self,
    ) -> None:
        """Required pydantic fields are enforced at config-construction
        time — there is no factory-side runtime check for them."""
        with pytest.raises(ValidationError):
            EncryptedFileVaultConfig()  # type: ignore[call-arg]


# ---------------------------------------------------------------------------
# D.3 — VaultSettings accepts typed config
# ---------------------------------------------------------------------------


class TestVaultSettingsTypedConfig:
    """``VaultSettings.config`` accepts typed configs OR dicts."""

    def test_default_config_coerces_to_typed_for_builtin_default_backend(
        self,
    ) -> None:
        """``VaultSettings()`` defaults to ``backend='env'``; the
        default empty-dict ``config`` is validated against the
        built-in ``env`` config_model and stored as a default-
        constructed :class:`EnvVaultConfig`."""
        s = VaultSettings()
        assert isinstance(s.config, EnvVaultConfig)
        assert s.config.prefix == "MGF_VAULT_"

    def test_typed_env_config(self) -> None:
        s = VaultSettings(config=EnvVaultConfig(prefix="X_"))
        assert isinstance(s.config, EnvVaultConfig)
        assert s.config.prefix == "X_"

    def test_typed_keyring_config(self) -> None:
        s = VaultSettings(
            backend="keyring",
            config=KeyringVaultConfig(service="myapp"),
        )
        assert isinstance(s.config, KeyringVaultConfig)
        assert s.config.service == "myapp"

    def test_typed_file_config(self, tmp_path: Path) -> None:
        s = VaultSettings(
            backend="file",
            config=EncryptedFileVaultConfig(
                path=tmp_path / "v.dat",
                passphrase="hunter2",
            ),
        )
        assert isinstance(s.config, EncryptedFileVaultConfig)

    def test_dict_coerced_to_typed_config_for_builtin(self) -> None:
        """For a built-in backend, a dict-shaped ``config`` is
        validated against the backend's registered ``config_model``
        at ``VaultSettings`` construction and stored as the typed
        instance. The legacy "dict passes through to the factory"
        path was removed pre-v1.0."""
        from mgf.common.vault import EnvVaultConfig

        s = VaultSettings(backend="env", config={"prefix": "X_"})
        assert isinstance(s.config, EnvVaultConfig)
        assert s.config.prefix == "X_"

    def test_custom_backend_dict_supported(self) -> None:
        """Custom backend names accept dict configs without
        emitting deprecation warnings — dict is the supported
        shape for them in v0.4."""

        @register_vault_backend("phase_d_custom")
        def _make(config: dict[str, object]) -> object:
            return ("phase_d_custom", config)

        try:
            s = VaultSettings(
                backend="phase_d_custom",
                config={"url": "https://example", "token": "xxx"},
            )
            assert s.backend == "phase_d_custom"
            assert s.config == {"url": "https://example", "token": "xxx"}

            registry = _registered_vault_backends()
            # The custom factory is called with the raw dict — no
            # warning emitted because it's not in
            # _BUILTIN_TYPED_CONFIG_NAMES.
            result = registry["phase_d_custom"](s.config)
            assert result == ("phase_d_custom", s.config)
        finally:
            unregister_vault_backend("phase_d_custom")
