"""Tests for safeconfig.core.config.SafeConfig."""

from __future__ import annotations

from pathlib import Path

import pytest

from pysafeconfigx import SafeConfig
from pysafeconfigx.core.config import ENCRYPTED_PREFIX
from pysafeconfigx.core.exceptions import (
    ConfigurationError,
    InvalidEnvironmentError,
    KeyNotFoundError,
    MissingRequiredKeyError,
)
from pysafeconfigx.core.crypto import encrypt_value


class TestInitialisation:
    def test_default_environment_is_development(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.delenv("APP_ENV", raising=False)
        monkeypatch.delenv("ENVIRONMENT", raising=False)
        cfg = SafeConfig(auto_load_env=False)
        assert cfg.environment == "development"

    def test_environment_from_kwarg(self) -> None:
        cfg = SafeConfig(environment="production", auto_load_env=False)
        assert cfg.environment == "production"

    def test_environment_from_env_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
        monkeypatch.setenv("APP_ENV", "staging")
        cfg = SafeConfig(auto_load_env=False)
        assert cfg.environment == "staging"

    def test_invalid_environment_raises(self) -> None:
        with pytest.raises(InvalidEnvironmentError):
            SafeConfig(environment="unknown_env", auto_load_env=False)

    def test_not_loaded_on_init(self) -> None:
        cfg = SafeConfig(auto_load_env=False)
        assert cfg.is_loaded is False


class TestLoading:
    def test_load_returns_self(self, populated_env_file: Path) -> None:
        cfg = SafeConfig(auto_load_env=False)
        cfg.add_loader(__import__("safeconfig.core.loaders", fromlist=["DotEnvLoader"]).DotEnvLoader(populated_env_file))
        result = cfg.load()
        assert result is cfg

    def test_is_loaded_after_load(self, populated_env_file: Path) -> None:
        from pysafeconfigx.core.loaders import DotEnvLoader
        cfg = SafeConfig(auto_load_env=False)
        cfg.add_loader(DotEnvLoader(populated_env_file))
        cfg.load()
        assert cfg.is_loaded is True

    def test_second_load_uses_cache(self, populated_env_file: Path) -> None:
        from pysafeconfigx.core.loaders import DotEnvLoader
        cfg = SafeConfig(auto_load_env=False)
        cfg.add_loader(DotEnvLoader(populated_env_file))
        cfg.load()
        data1 = cfg.all()
        cfg.load()  # Should be a no-op
        data2 = cfg.all()
        assert data1 == data2

    def test_require_raises_missing_required(self, tmp_path: Path) -> None:
        from pysafeconfigx.core.loaders import DotEnvLoader
        env = tmp_path / ".env"
        env.write_text("EXISTING_KEY=value\n", encoding="utf-8")
        cfg = SafeConfig(
            auto_load_env=False,
            required_keys=["EXISTING_KEY", "MISSING_KEY"],
        )
        cfg.add_loader(DotEnvLoader(env))
        with pytest.raises(MissingRequiredKeyError) as exc_info:
            cfg.load()
        assert "MISSING_KEY" in str(exc_info.value)

    def test_accessing_before_load_raises(self) -> None:
        cfg = SafeConfig(auto_load_env=False)
        with pytest.raises(RuntimeError, match="load\\(\\)"):
            cfg.get("ANY_KEY")


class TestGet:
    def setup_method(self) -> None:
        from pysafeconfigx.core.loaders import DotEnvLoader
        self.env_content = (
            "DATABASE_URL=postgres://localhost/testdb\n"
            "API_KEY=test_api_key_12345\n"
            "DEBUG=true\n"
            "MAX_WORKERS=4\n"
            "TAGS=web,api,backend\n"
        )

    def _make_cfg(self, tmp_path: Path) -> SafeConfig:
        from pysafeconfigx.core.loaders import DotEnvLoader
        p = tmp_path / ".env"
        p.write_text(self.env_content, encoding="utf-8")
        cfg = SafeConfig(auto_load_env=False)
        cfg.add_loader(DotEnvLoader(p))
        cfg.load()
        return cfg

    def test_get_existing_key(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert cfg.get("DATABASE_URL") == "postgres://localhost/testdb"

    def test_get_missing_returns_default(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert cfg.get("NONEXISTENT", "default_value") == "default_value"

    def test_get_missing_returns_none(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert cfg.get("NONEXISTENT") is None

    def test_require_existing(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert cfg.require("DATABASE_URL") == "postgres://localhost/testdb"

    def test_require_missing_raises(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        with pytest.raises(KeyNotFoundError):
            cfg.require("MISSING")

    def test_get_bool_true(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert cfg.get_bool("DEBUG") is True

    def test_get_int(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert cfg.get_int("MAX_WORKERS") == 4

    def test_get_list(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert cfg.get_list("TAGS") == ["web", "api", "backend"]

    def test_contains(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert "DATABASE_URL" in cfg
        assert "NONEXISTENT" not in cfg

    def test_getitem(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        assert cfg["DATABASE_URL"] == "postgres://localhost/testdb"

    def test_getitem_missing_raises(self, tmp_path: Path) -> None:
        cfg = self._make_cfg(tmp_path)
        with pytest.raises(KeyNotFoundError):
            _ = cfg["MISSING"]


class TestEncryptDecryptIntegration:
    def test_get_with_decrypt_flag(self, tmp_path: Path, master_key: str, monkeypatch: pytest.MonkeyPatch) -> None:
        from pysafeconfigx.core.loaders import DotEnvLoader
        monkeypatch.setenv("SAFECONFIG_MASTER_KEY", master_key)

        plaintext = "super_secret_db_password"
        blob = encrypt_value(plaintext, master_key, config_key="DB_PASS")
        stored = f"{ENCRYPTED_PREFIX}{blob}"

        p = tmp_path / ".env"
        p.write_text(f"DB_PASS={stored}\n", encoding="utf-8")

        cfg = SafeConfig(auto_load_env=False, master_key=master_key)
        cfg.add_loader(DotEnvLoader(p))
        cfg.load()

        recovered = cfg.get("DB_PASS", decrypt=True)
        assert recovered == plaintext

    def test_auto_decrypt_enc_prefix(self, tmp_path: Path, master_key: str) -> None:
        from pysafeconfigx.core.loaders import DotEnvLoader
        plaintext = "my_token"
        blob = encrypt_value(plaintext, master_key, config_key="TOKEN")
        stored = f"enc:{blob}"

        p = tmp_path / ".env"
        p.write_text(f"TOKEN={stored}\n", encoding="utf-8")

        cfg = SafeConfig(auto_load_env=False, master_key=master_key)
        cfg.add_loader(DotEnvLoader(p))
        cfg.load()

        # Even without decrypt=True, enc: prefix triggers auto-decryption
        recovered = cfg.get("TOKEN")
        assert recovered == plaintext

    def test_encrypt_method(self, tmp_path: Path, master_key: str) -> None:
        from pysafeconfigx.core.loaders import DotEnvLoader
        p = tmp_path / ".env"
        p.write_text("SECRET=plaintext\n", encoding="utf-8")

        cfg = SafeConfig(auto_load_env=False, master_key=master_key)
        cfg.add_loader(DotEnvLoader(p))
        cfg.load()

        blob = cfg.encrypt("SECRET")
        assert blob  # Non-empty string
        # After encrypting, the stored value should be decryptable
        recovered = cfg.get("SECRET", decrypt=True)
        assert recovered == "plaintext"


class TestRepr:
    def test_repr_contains_environment(self) -> None:
        cfg = SafeConfig(environment="production", auto_load_env=False)
        assert "production" in repr(cfg)
