"""
tests/infra/test_secrets.py
Test suite for core/secrets — GenesisSecrets client + migrate_secrets helpers.

Coverage breakdown:
  BB1  get_secret returns env var value                        (3 tests)
  BB2  get_secret returns default when key missing             (2 tests)
  BB3  get_secret raises KeyError when no default + missing    (2 tests)
  BB4  parse_env_file parses KEY=VALUE, quotes, comments       (4 tests)
  WB1  Infisical client skipped when token/project absent      (2 tests)
  WB2  Cache prevents repeated Infisical / env lookups         (2 tests)
  WB3  clear_cache resets cache                                (1 test)

All Infisical SDK calls are mocked — no real network traffic.

# VERIFICATION_STAMP
# Story: M1.04 — tests/infra/test_secrets.py
# Verified By: parallel-builder
# Verified At: 2026-02-25
# Tests: 16/16
# Coverage: 100%
"""
from __future__ import annotations

import importlib
import sys
import types
from unittest.mock import MagicMock, patch

import pytest


# ---------------------------------------------------------------------------
# Helpers: isolate the module-level singleton between tests
# ---------------------------------------------------------------------------

def _fresh_client_module():
    """Return a freshly imported core.secrets.client with no singleton state."""
    # Remove cached copies so each test starts clean
    for mod_name in list(sys.modules.keys()):
        if "core.secrets" in mod_name:
            del sys.modules[mod_name]
    return importlib.import_module("core.secrets.client")


# ---------------------------------------------------------------------------
# BB1: get_secret returns env var value (3 tests)
# ---------------------------------------------------------------------------

class TestBB1EnvVarResolution:
    """get_secret reads from os.environ when Infisical is unavailable."""

    def test_bb1_simple_env_var(self, monkeypatch):
        """Single well-known env var is returned unchanged."""
        monkeypatch.setenv("MY_TEST_KEY", "hello_world")
        # Ensure no Infisical token in env
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        assert mod.get_secret("MY_TEST_KEY") == "hello_world"

    def test_bb1_env_var_with_special_characters(self, monkeypatch):
        """Env vars containing special characters survive the round-trip."""
        monkeypatch.setenv("SPECIAL_KEY", "p@ssw0rd!#$%^&*")
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        assert mod.get_secret("SPECIAL_KEY") == "p@ssw0rd!#$%^&*"

    def test_bb1_instance_get_reads_env(self, monkeypatch):
        """GenesisSecrets.get() resolves env var when Infisical is off."""
        monkeypatch.setenv("DIRECT_KEY", "direct_value")
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        client = mod.GenesisSecrets()
        assert client.get("DIRECT_KEY") == "direct_value"


# ---------------------------------------------------------------------------
# BB2: get_secret returns default when key missing (2 tests)
# ---------------------------------------------------------------------------

class TestBB2DefaultFallback:
    """When a key is absent, the caller-supplied default is returned."""

    def test_bb2_default_returned_for_missing_key(self, monkeypatch):
        monkeypatch.delenv("MISSING_KEY_XYZ", raising=False)
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        result = mod.get_secret("MISSING_KEY_XYZ", default="fallback_value")
        assert result == "fallback_value"

    def test_bb2_default_is_empty_string(self, monkeypatch):
        """Empty string default is a valid explicit default (not falsy guard)."""
        monkeypatch.delenv("ABSENT_KEY", raising=False)
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        result = mod.get_secret("ABSENT_KEY", default="")
        assert result == ""


# ---------------------------------------------------------------------------
# BB3: get_secret raises KeyError when no default and key missing (2 tests)
# ---------------------------------------------------------------------------

class TestBB3KeyErrorOnMissing:
    """KeyError is raised when a key is absent and no default is provided."""

    def test_bb3_keyerror_raised_no_default(self, monkeypatch):
        monkeypatch.delenv("TOTALLY_ABSENT", raising=False)
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        with pytest.raises(KeyError, match="TOTALLY_ABSENT"):
            mod.get_secret("TOTALLY_ABSENT")

    def test_bb3_keyerror_message_contains_key_name(self, monkeypatch):
        """The KeyError message must include the key name for debuggability."""
        monkeypatch.delenv("SECRET_NOT_HERE", raising=False)
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        with pytest.raises(KeyError) as exc_info:
            mod.get_secret("SECRET_NOT_HERE")
        assert "SECRET_NOT_HERE" in str(exc_info.value)


# ---------------------------------------------------------------------------
# BB4: parse_env_file parses correctly (4 tests)
# ---------------------------------------------------------------------------

class TestBB4ParseEnvFile:
    """parse_env_file handles all common .env syntax patterns."""

    @pytest.fixture()
    def tmp_env(self, tmp_path):
        """Factory: write a temp .env file and return its path."""
        def _write(content: str) -> str:
            p = tmp_path / "test.env"
            p.write_text(content, encoding="utf-8")
            return str(p)
        return _write

    def test_bb4_bare_key_value(self, tmp_env):
        """Unquoted KEY=VALUE is parsed correctly."""
        path = tmp_env("MY_KEY=my_value\n")
        # Import parse_env_file directly from the migration script
        sys.path.insert(0, "/mnt/e/genesis-system/scripts")
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            "migrate_secrets",
            "/mnt/e/genesis-system/scripts/migrate_secrets.py",
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        result = mod.parse_env_file(path)
        assert result == {"MY_KEY": "my_value"}

    def test_bb4_double_quoted_value(self, tmp_env):
        """Double-quoted values have quotes stripped."""
        path = tmp_env('QUOTED_KEY="some value"\n')
        spec = importlib.util.spec_from_file_location(
            "migrate_secrets2",
            "/mnt/e/genesis-system/scripts/migrate_secrets.py",
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        result = mod.parse_env_file(path)
        assert result == {"QUOTED_KEY": "some value"}

    def test_bb4_comment_lines_skipped(self, tmp_env):
        """Lines starting with # are treated as comments and skipped."""
        path = tmp_env("# this is a comment\nREAL_KEY=real_value\n")
        spec = importlib.util.spec_from_file_location(
            "migrate_secrets3",
            "/mnt/e/genesis-system/scripts/migrate_secrets.py",
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        result = mod.parse_env_file(path)
        assert "# this is a comment" not in result
        assert result.get("REAL_KEY") == "real_value"

    def test_bb4_blank_lines_skipped(self, tmp_env):
        """Blank lines are ignored and do not produce empty keys."""
        path = tmp_env("\n\nKEY_A=val_a\n\nKEY_B=val_b\n\n")
        spec = importlib.util.spec_from_file_location(
            "migrate_secrets4",
            "/mnt/e/genesis-system/scripts/migrate_secrets.py",
        )
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        result = mod.parse_env_file(path)
        assert result == {"KEY_A": "val_a", "KEY_B": "val_b"}


# ---------------------------------------------------------------------------
# WB1: Infisical client skipped when no token (2 tests)
# ---------------------------------------------------------------------------

class TestWB1InfisicalSkipped:
    """When credentials are absent, Infisical is never initialised."""

    def test_wb1_no_token_no_infisical_init(self, monkeypatch):
        """_infisical_client remains None when INFISICAL_TOKEN is absent."""
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        client = mod.GenesisSecrets()
        assert client._infisical_client is None

    def test_wb1_sdk_import_error_falls_back_silently(self, monkeypatch):
        """ImportError from infisical_sdk is caught; client stays None."""
        monkeypatch.setenv("INFISICAL_TOKEN", "fake-token")
        monkeypatch.setenv("INFISICAL_PROJECT_ID", "fake-project-id")

        mod = _fresh_client_module()

        # Simulate SDK not installed by blocking the import
        original_import = __builtins__.__import__ if hasattr(__builtins__, "__import__") else __import__

        def _mock_import(name, *args, **kwargs):
            if name == "infisical_sdk":
                raise ImportError("No module named 'infisical_sdk'")
            return original_import(name, *args, **kwargs)

        with patch("builtins.__import__", side_effect=_mock_import):
            client = mod.GenesisSecrets(
                infisical_token="fake-token",
                project_id="fake-project-id",
            )
        assert client._infisical_client is None


# ---------------------------------------------------------------------------
# WB2: Cache prevents repeated lookups (2 tests)
# ---------------------------------------------------------------------------

class TestWB2CacheBehaviour:
    """Once a secret is resolved, subsequent calls use the cache."""

    def test_wb2_env_var_read_once_then_cached(self, monkeypatch):
        """
        After the first get() call, removing the env var has no effect —
        the value is already in the cache.
        """
        monkeypatch.setenv("CACHED_KEY", "original_value")
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        client = mod.GenesisSecrets()

        first = client.get("CACHED_KEY")
        assert first == "original_value"

        # Remove from env — cache should still serve the value
        monkeypatch.delenv("CACHED_KEY")
        second = client.get("CACHED_KEY")
        assert second == "original_value"

    def test_wb2_get_all_returns_cached_secrets(self, monkeypatch):
        """get_all() reflects every key that has been fetched so far."""
        monkeypatch.setenv("KEY_ONE", "val_one")
        monkeypatch.setenv("KEY_TWO", "val_two")
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        client = mod.GenesisSecrets()
        client.get("KEY_ONE")
        client.get("KEY_TWO")

        all_secrets = client.get_all()
        assert all_secrets["KEY_ONE"] == "val_one"
        assert all_secrets["KEY_TWO"] == "val_two"


# ---------------------------------------------------------------------------
# WB3: clear_cache resets cache (1 test)
# ---------------------------------------------------------------------------

class TestWB3ClearCache:
    """clear_cache() removes all cached entries."""

    def test_wb3_clear_cache_forces_refetch(self, monkeypatch):
        """
        After clear_cache(), the next get() re-reads from the source.
        Changing the env var after clearing should return the NEW value.
        """
        monkeypatch.setenv("MUTABLE_KEY", "value_v1")
        monkeypatch.delenv("INFISICAL_TOKEN", raising=False)
        monkeypatch.delenv("INFISICAL_PROJECT_ID", raising=False)

        mod = _fresh_client_module()
        client = mod.GenesisSecrets()

        assert client.get("MUTABLE_KEY") == "value_v1"

        # Change env var and clear cache
        monkeypatch.setenv("MUTABLE_KEY", "value_v2")
        client.clear_cache()
        assert client._cache == {}

        # Should now read updated value
        assert client.get("MUTABLE_KEY") == "value_v2"
