"""P4A: Non-Windows secure storage at-rest hardening for Linux/Docker.

Tests prove:
- Docker no longer auto-allows raw fallback
- Non-Windows with no secure secret generates a local passphrase file
- Non-Windows with MESH_SECURE_STORAGE_SECRET works (passphrase provider)
- Passphrase-protected envelopes round-trip correctly (master + domain)
- Raw-to-passphrase migration works when secret is supplied
- Explicit raw fallback still works only when deliberately enabled
- Windows DPAPI path not regressed (skipped on non-Windows)
- Wrong passphrase fails closed
"""

import json
import os
from types import SimpleNamespace

import pytest


def _reset(mod):
    mod._MASTER_KEY_CACHE = None
    mod._DOMAIN_KEY_CACHE.clear()


class TestDockerNoAutoRawFallback:
    """Docker containers must no longer auto-allow raw fallback."""

    def test_docker_container_does_not_auto_allow_raw(self, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.setattr(mesh_secure_storage, "_is_docker_container", lambda: True)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET", raising=False)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="",
            ),
        )

        assert mesh_secure_storage._raw_fallback_allowed() is False

    def test_docker_with_explicit_opt_in_still_allows_raw(self, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.setattr(mesh_secure_storage, "_is_docker_container", lambda: True)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=True,
                MESH_SECURE_STORAGE_SECRET="",
            ),
        )

        assert mesh_secure_storage._raw_fallback_allowed() is True


class TestGeneratedLocalSecretWithoutOperatorSecret:
    """Non-Windows with no supplied secret generates a local passphrase file."""

    def test_master_key_creation_uses_generated_local_secret(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET", raising=False)
        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET_FILE", raising=False)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="",
            ),
        )
        _reset(mesh_secure_storage)

        key = mesh_secure_storage._load_master_key()
        assert len(key) == 32
        assert (tmp_path / "secure_storage_secret.key").exists()
        envelope = json.loads((tmp_path / "master.key").read_text(encoding="utf-8"))
        assert envelope["provider"] == "passphrase"
        assert "key" not in envelope

    def test_domain_key_creation_uses_generated_local_secret(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET", raising=False)
        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET_FILE", raising=False)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="",
            ),
        )
        _reset(mesh_secure_storage)

        key = mesh_secure_storage._load_domain_key("test_domain", base_dir=tmp_path)
        assert len(key) == 32
        assert (tmp_path / "secure_storage_secret.key").exists()
        envelope = json.loads((tmp_path / "_domain_keys" / "test_domain.key").read_text(encoding="utf-8"))
        assert envelope["provider"] == "passphrase"
        assert "key" not in envelope


class TestPassphraseProvider:
    """Passphrase-based provider works for master and domain keys."""

    def test_master_key_round_trip_with_passphrase(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "test-secret-phrase-1234")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="test-secret-phrase-1234",
            ),
        )
        _reset(mesh_secure_storage)

        # Create master key
        key1 = mesh_secure_storage._load_master_key()
        assert len(key1) == 32

        # Verify envelope is passphrase-protected
        envelope = json.loads((tmp_path / "master.key").read_text(encoding="utf-8"))
        assert envelope["provider"] == "passphrase"
        assert "salt" in envelope
        assert "key" not in envelope  # No raw key exposed

        # Clear cache, reload
        _reset(mesh_secure_storage)
        key2 = mesh_secure_storage._load_master_key()
        assert key1 == key2

    def test_domain_key_round_trip_with_passphrase(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "test-secret-phrase-1234")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="test-secret-phrase-1234",
            ),
        )
        _reset(mesh_secure_storage)

        key1 = mesh_secure_storage._load_domain_key("testdomain", base_dir=tmp_path)
        assert len(key1) == 32

        # Verify envelope
        key_file = tmp_path / "_domain_keys" / "testdomain.key"
        envelope = json.loads(key_file.read_text(encoding="utf-8"))
        assert envelope["provider"] == "passphrase"
        assert envelope["domain"] == "testdomain"
        assert "key" not in envelope

        # Clear cache, reload
        _reset(mesh_secure_storage)
        key2 = mesh_secure_storage._load_domain_key("testdomain", base_dir=tmp_path)
        assert key1 == key2

    def test_secure_json_end_to_end_with_passphrase(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "test-secret-phrase-1234")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="test-secret-phrase-1234",
            ),
        )
        _reset(mesh_secure_storage)

        path = tmp_path / "secret.json"
        mesh_secure_storage.write_secure_json(path, {"wormhole": "data"})

        # Ciphertext on disk, not plaintext
        raw = path.read_text(encoding="utf-8")
        assert "wormhole" not in raw

        _reset(mesh_secure_storage)
        data = mesh_secure_storage.read_secure_json(path, lambda: {})
        assert data == {"wormhole": "data"}

    def test_domain_json_end_to_end_with_passphrase(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "test-secret-phrase-1234")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="test-secret-phrase-1234",
            ),
        )
        _reset(mesh_secure_storage)

        mesh_secure_storage.write_domain_json("gate_persona", "gate.json", {"gate": "secure"}, base_dir=tmp_path)

        _reset(mesh_secure_storage)
        data = mesh_secure_storage.read_domain_json("gate_persona", "gate.json", lambda: {}, base_dir=tmp_path)
        assert data == {"gate": "secure"}


class TestWrongPassphraseFails:
    """Wrong passphrase must fail closed."""

    def test_wrong_passphrase_rejects_master_key(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "correct-secret")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="correct-secret",
            ),
        )
        _reset(mesh_secure_storage)

        mesh_secure_storage._load_master_key()

        # Now try with wrong secret
        _reset(mesh_secure_storage)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "wrong-secret")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="wrong-secret",
            ),
        )

        with pytest.raises(mesh_secure_storage.SecureStorageError, match="Failed to unwrap"):
            mesh_secure_storage._load_master_key()

    def test_missing_passphrase_rejects_passphrase_envelope(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "a-secret")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="a-secret",
            ),
        )
        _reset(mesh_secure_storage)

        mesh_secure_storage._load_master_key()

        # Remove the secret
        _reset(mesh_secure_storage)
        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET", raising=False)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="",
            ),
        )

        with pytest.raises(mesh_secure_storage.SecureStorageError, match="Failed to unwrap"):
            mesh_secure_storage._load_master_key()


class TestRawToPassphraseMigration:
    """Existing raw envelopes migrate to passphrase when secret is supplied."""

    def test_master_key_migrates_from_raw_to_passphrase(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)

        # Step 1: create raw envelope (simulate old Docker behavior)
        raw_key = os.urandom(32)
        envelope = mesh_secure_storage._master_envelope_for_fallback(raw_key)
        (tmp_path / "master.key").write_text(json.dumps(envelope), encoding="utf-8")

        # Step 2: now set up with secret and no raw fallback
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "migration-secret")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="migration-secret",
            ),
        )
        _reset(mesh_secure_storage)

        loaded_key = mesh_secure_storage._load_master_key()
        assert loaded_key == raw_key

        # Verify file is now passphrase-protected
        migrated = json.loads((tmp_path / "master.key").read_text(encoding="utf-8"))
        assert migrated["provider"] == "passphrase"
        assert "key" not in migrated

        # Verify it still loads correctly
        _reset(mesh_secure_storage)
        assert mesh_secure_storage._load_master_key() == raw_key

    def test_domain_key_migrates_from_raw_to_passphrase(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)

        # Create raw domain key
        raw_key = os.urandom(32)
        domain = "testdomain"
        envelope = mesh_secure_storage._domain_key_envelope_for_fallback(domain, raw_key)
        key_dir = tmp_path / "_domain_keys"
        key_dir.mkdir(parents=True, exist_ok=True)
        key_file = key_dir / f"{domain}.key"
        key_file.write_text(json.dumps(envelope), encoding="utf-8")

        # Set up with secret
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "migration-secret")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=False,
                MESH_SECURE_STORAGE_SECRET="migration-secret",
            ),
        )
        _reset(mesh_secure_storage)

        loaded_key = mesh_secure_storage._load_domain_key(domain, base_dir=tmp_path)
        assert loaded_key == raw_key

        migrated = json.loads(key_file.read_text(encoding="utf-8"))
        assert migrated["provider"] == "passphrase"
        assert "key" not in migrated


class TestExplicitRawFallbackStillWorks:
    """Explicit MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=true still works."""

    def test_raw_fallback_with_opt_in(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET", raising=False)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=True,
                MESH_SECURE_STORAGE_SECRET="",
            ),
        )
        _reset(mesh_secure_storage)

        key = mesh_secure_storage._load_master_key()
        assert len(key) == 32

        envelope = json.loads((tmp_path / "master.key").read_text(encoding="utf-8"))
        assert envelope["provider"] == "raw"

    def test_passphrase_preferred_over_raw_even_with_opt_in(self, tmp_path, monkeypatch):
        """When both secret and raw opt-in are set, passphrase is used for new keys."""
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "a-secret")
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(
                MESH_ALLOW_RAW_SECURE_STORAGE_FALLBACK=True,
                MESH_SECURE_STORAGE_SECRET="a-secret",
            ),
        )
        _reset(mesh_secure_storage)

        mesh_secure_storage._load_master_key()

        envelope = json.loads((tmp_path / "master.key").read_text(encoding="utf-8"))
        assert envelope["provider"] == "passphrase"


class TestWindowsDPAPINotRegressed:
    """Windows DPAPI path must not be affected by P4A changes."""

    @pytest.mark.skipif(os.name != "nt", reason="DPAPI only available on Windows")
    def test_windows_creates_dpapi_envelope(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage

        monkeypatch.setattr(mesh_secure_storage, "DATA_DIR", tmp_path)
        monkeypatch.setattr(mesh_secure_storage, "MASTER_KEY_FILE", tmp_path / "master.key")
        _reset(mesh_secure_storage)

        key = mesh_secure_storage._load_master_key()
        assert len(key) == 32

        envelope = json.loads((tmp_path / "master.key").read_text(encoding="utf-8"))
        assert envelope["provider"] == "dpapi-machine"


class TestPassphraseWrapUnwrap:
    """Unit tests for the passphrase wrap/unwrap primitives."""

    def test_wrap_unwrap_round_trip(self):
        from services.mesh.mesh_secure_storage import _passphrase_wrap, _passphrase_unwrap

        key = os.urandom(32)
        secret = "test-passphrase"
        wrapped = _passphrase_wrap(key, secret)
        assert "salt" in wrapped
        assert "nonce" in wrapped
        assert "protected_key" in wrapped

        unwrapped = _passphrase_unwrap(wrapped, secret)
        assert unwrapped == key

    def test_wrong_secret_fails(self):
        from services.mesh.mesh_secure_storage import _passphrase_wrap, _passphrase_unwrap

        key = os.urandom(32)
        wrapped = _passphrase_wrap(key, "correct")
        with pytest.raises(Exception):
            _passphrase_unwrap(wrapped, "incorrect")

    def test_deterministic_with_same_salt(self):
        from services.mesh.mesh_secure_storage import _passphrase_wrap, _passphrase_unwrap

        key = os.urandom(32)
        salt = os.urandom(32)
        wrapped1 = _passphrase_wrap(key, "same-secret", salt=salt)
        # Different nonce means different ciphertext, but both unwrap to same key
        wrapped2 = _passphrase_wrap(key, "same-secret", salt=salt)

        assert _passphrase_unwrap(wrapped1, "same-secret") == key
        assert _passphrase_unwrap(wrapped2, "same-secret") == key


class TestGetStorageSecret:
    """_get_storage_secret reads from env and config correctly."""

    def test_reads_from_env(self, monkeypatch):
        from services.mesh import mesh_secure_storage

        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET", "env-secret")
        assert mesh_secure_storage._get_storage_secret() == "env-secret"

    def test_returns_none_when_empty(self, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET", raising=False)
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: True)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(MESH_SECURE_STORAGE_SECRET=""),
        )
        assert mesh_secure_storage._get_storage_secret() is None

    def test_generates_local_secret_file_on_non_windows(self, tmp_path, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        secret_file = tmp_path / "generated_secret.key"
        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET", raising=False)
        monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
        monkeypatch.setenv("MESH_SECURE_STORAGE_SECRET_FILE", str(secret_file))
        monkeypatch.setattr(mesh_secure_storage, "_is_windows", lambda: False)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(MESH_SECURE_STORAGE_SECRET=""),
        )

        first = mesh_secure_storage._get_storage_secret()
        second = mesh_secure_storage._get_storage_secret()
        assert first
        assert second == first
        assert secret_file.read_text(encoding="utf-8").strip() == first

    def test_falls_back_to_config(self, monkeypatch):
        from services.mesh import mesh_secure_storage
        from services import config as config_mod

        monkeypatch.delenv("MESH_SECURE_STORAGE_SECRET", raising=False)
        monkeypatch.setattr(
            config_mod,
            "get_settings",
            lambda: SimpleNamespace(MESH_SECURE_STORAGE_SECRET="config-secret"),
        )
        assert mesh_secure_storage._get_storage_secret() == "config-secret"
