# File: tests/unit/infrastructure/test_config_manager.py
# Purpose: Validate configuration manager behaviours across file, env, and defaults.
# Context: Exercises path bootstrapping, YAML loading, and environment overrides.
"""Tests for the configuration manager layer and its supporting helpers.

The suite covers default configuration values, file persistence, and environment
variable precedence to ensure consistent application behaviour.
"""

import os
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch

import pytest
import yaml

# Add parent directory to path
# PathManager統一パス管理システムを使用(事前にパスを設定)
sys.path.insert(0, str(Path(__file__).parent.parent))  # scripts/を追加
from noveler.infrastructure.utils.path_manager import ensure_imports

ensure_imports()

from noveler.infrastructure.config.config_manager import (
    AppConfig,
    ConfigManager,
    create_production_config,
    create_test_config,
)


class TestAppConfig:
    """AppConfig データクラスのテスト"""

    def test_default_values(self) -> None:
        """デフォルト値のテスト"""
        config = AppConfig()

        assert config.mode == "production"
        assert config.test_mode is False
        assert config.debug is False
        assert config.log_level == "INFO"
        assert config.use_mock_services is False
        assert config.quality_threshold == 80
        assert config.auto_fix is False
        assert config.batch_size == 100
        assert config.timeout_seconds == 30

    def test_custom_values(self) -> None:
        """カスタム値のテスト"""
        config = AppConfig(
            mode="test",
            test_mode=True,
            debug=True,
            log_level="DEBUG",
            quality_threshold=70,
        )

        assert config.mode == "test"
        assert config.test_mode is True
        assert config.debug is True
        assert config.log_level == "DEBUG"
        assert config.quality_threshold == 70


class TestConfigManager:
    """ConfigManager クラスのテスト"""

    def test_config_manager_without_file(self) -> None:
        """設定ファイルなしでの初期化テスト"""
        with (
            patch("common.config_manager.ConfigManager._find_config_file", return_value=None),
            patch("common.config_manager.os.getenv") as mock_getenv,
        ):
            # pytest関連の環境変数検出を無効化
            def mock_getenv_side_effect(key: object, default=None):
                # pytest検出を回避するため、これらの変数はNoneを返す
                if key == "PYTEST_CURRENT_TEST":
                    return None
                if key == "_":
                    return "/usr/bin/python3"  # pytestを含まない値
                # その他の設定関連環境変数もNoneを返す
                if key in [
                    "APP_MODE",
                    "TEST_MODE",
                    "DEBUG",
                    "LOG_LEVEL",
                    "USE_MOCK_SERVICES",
                    "TEST_PROJECT_ROOT",
                    "TEST_DATA_DIR",
                    "QUALITY_THRESHOLD",
                    "AUTO_FIX",
                    "BATCH_SIZE",
                    "TIMEOUT_SECONDS",
                ]:
                    return None
                return default

            mock_getenv.side_effect = mock_getenv_side_effect

            manager = ConfigManager()
            config = manager.get_config()

            # デフォルト値が使用されることを確認
            assert config.mode == "production"
            assert config.test_mode is False

    def test_config_from_file(self) -> None:
        """設定ファイルからの読み込みテスト"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            config_data = {
                "mode": "test",
                "test_mode": True,
                "debug": True,
                "quality_threshold": 75,
                "auto_fix": True,
            }
            yaml.dump(config_data, f)
            config_path = Path(f.name)

        try:
            manager = ConfigManager(config_path)
            config = manager.get_config()

            assert config.mode == "test"
            assert config.test_mode is True
            assert config.debug is True
            assert config.quality_threshold == 75
            assert config.auto_fix is True
        finally:
            config_path.unlink()

    def test_environment_variable_override(self) -> None:
        """環境変数による設定上書きテスト"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            config_data = {
                "mode": "production",
                "test_mode": False,
                "quality_threshold": 80,
            }
            yaml.dump(config_data, f)
            config_path = Path(f.name)

        # 環境変数を設定
        env_vars = {
            "APP_MODE": "test",
            "TEST_MODE": "true",
            "QUALITY_THRESHOLD": "70",
        }

        try:
            with patch.dict(os.environ, env_vars):
                manager = ConfigManager(config_path)
                config = manager.get_config()

                # 環境変数の値が優先されることを確認
                assert config.mode == "test"
                assert config.test_mode is True
                assert config.quality_threshold == 70
        finally:
            config_path.unlink()

    def test_pytest_auto_detection(self) -> None:
        """pytest実行時の自動検出テスト"""
        with patch.dict(os.environ, {"PYTEST_CURRENT_TEST": "test_something.py::test_method"}):
            manager = ConfigManager(None)
            config = manager.get_config()

            assert config.mode == "test"
            assert config.test_mode is True
            assert config.use_mock_services is True

    def test_mode_detection_methods(self) -> None:
        """モード検出メソッドのテスト"""
        # テストモード
        test_config = AppConfig(mode="test", test_mode=True)
        manager = ConfigManager(None)
        manager.config = test_config

        assert manager.is_test_mode() is True
        assert manager.is_production_mode() is False
        assert manager.should_use_mock_services() is True

        # 本番モード
        prod_config = AppConfig(mode="production", test_mode=False)
        manager.config = prod_config

        assert manager.is_test_mode() is False
        assert manager.is_production_mode() is True
        assert manager.should_use_mock_services() is False

    def test_log_level_adjustment(self) -> None:
        """ログレベル調整のテスト"""
        # テストモード時のログレベル調整
        test_config = AppConfig(mode="test", test_mode=True, log_level="DEBUG")
        manager = ConfigManager(None)
        manager.config = test_config

        assert manager.get_log_level() == "WARNING"  # テスト時は抑制

        # 本番モード時はそのまま
        prod_config = AppConfig(mode="production", test_mode=False, log_level="DEBUG")
        manager.config = prod_config

        assert manager.get_log_level() == "DEBUG"

    def test_save_config(self, tmp_path) -> None:
        """設定保存のテスト"""
        save_path = tmp_path / "test_config.yaml"

        manager = ConfigManager(None)
        manager.config = AppConfig(
            mode="test",
            test_mode=True,
            quality_threshold=75,
        )

        manager.save_config(save_path)

        # 保存されたファイルを確認
        assert save_path.exists()

        with Path(save_path).open(encoding="utf-8") as f:
            saved_data = yaml.safe_load(f)

        assert saved_data["mode"] == "test"
        assert saved_data["test_mode"] is True
        assert saved_data["quality_threshold"] == 75

    def test_get_prefers_override_to_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """ランタイムオーバーライドが環境変数より優先されることを確認"""
        monkeypatch.setenv("DYNAMIC_SETTING", "env")
        manager = ConfigManager(None)

        manager.set("DYNAMIC_SETTING", "override", persist_env=False)

        assert manager.get("DYNAMIC_SETTING") == "override"
        # 環境変数は書き換えられていない
        assert os.environ["DYNAMIC_SETTING"] == "env"

    def test_get_with_cast_and_default(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """castパラメータで型変換され、失敗時はdefaultへフォールバックすることを確認"""
        manager = ConfigManager(None)
        monkeypatch.setenv("TIMEOUT_LIMIT", "45")

        assert manager.get("TIMEOUT_LIMIT", default=30, cast=int) == 45

        monkeypatch.setenv("TIMEOUT_LIMIT_INVALID", "oops")
        assert manager.get("TIMEOUT_LIMIT_INVALID", default=30, cast=int) == 30

    def test_get_bool_interprets_strings(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """get_boolが文字列の真偽値判定を統一することを確認"""
        manager = ConfigManager(None)

        monkeypatch.setenv("FEATURE_TOGGLE", "yes")
        assert manager.get_bool("FEATURE_TOGGLE") is True

        monkeypatch.setenv("FEATURE_TOGGLE", "0")
        assert manager.get_bool("FEATURE_TOGGLE", default=True) is False

    def test_get_respects_allow_empty_flag(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """allow_empty=False の場合は空文字をdefaultへフォールバック"""
        manager = ConfigManager(None)
        monkeypatch.setenv("EMPTY_OPTION", "")

        assert manager.get("EMPTY_OPTION", default="fallback", allow_empty=False) == "fallback"

    def test_set_updates_config_without_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """persist_env デフォルトFalse時はAppConfigのみ更新し環境は触らない"""
        monkeypatch.delenv("TIMEOUT_SECONDS", raising=False)

        manager = ConfigManager(None)
        manager.set("timeout_seconds", "45")

        assert manager.config.timeout_seconds == 45
        assert manager.get("timeout_seconds") == 45
        assert "TIMEOUT_SECONDS" not in os.environ

    def test_set_without_persist_env(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """persist_env=False の場合は環境変数を書き換えない"""
        monkeypatch.delenv("CUSTOM_KEY", raising=False)

        manager = ConfigManager(None)
        manager.set("CUSTOM_KEY", "value", persist_env=False)

        assert "CUSTOM_KEY" not in os.environ
        assert manager.get("CUSTOM_KEY") == "value"

    def test_restore_persisted_environment(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """restore_persisted_environmentが変更済み環境変数を復元することを確認"""
        monkeypatch.setenv("PROJECT_ROOT", "/original/root")
        manager = ConfigManager(None)

        manager.set("PROJECT_ROOT", "/tmp/project", persist_env=True)
        assert os.environ["PROJECT_ROOT"] == "/tmp/project"

        manager.restore_persisted_environment()
        assert os.environ["PROJECT_ROOT"] == "/original/root"
        assert not manager._persisted_env_snapshots  # type: ignore[attr-defined]

    def test_require_returns_value_or_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
        """require が値取得と KeyError を正しく扱うことを確認"""
        monkeypatch.setenv("REQUIRED_KEY", "configured")
        manager = ConfigManager(None)

        assert manager.require("REQUIRED_KEY") == "configured"

        monkeypatch.delenv("REQUIRED_KEY", raising=False)
        manager.reload_config()

        with pytest.raises(KeyError):
            manager.require("REQUIRED_KEY")


class TestGlobalFunctions:
    """グローバル関数のテスト"""

    def test_create_test_config(self) -> None:
        """テスト用設定作成のテスト"""
        config = create_test_config()

        assert config.mode == "test"
        assert config.test_mode is True
        assert config.use_mock_services is True
        assert config.log_level == "WARNING"
        assert config.batch_size == 10  # テスト用は小さく

    def test_create_production_config(self) -> None:
        """本番用設定作成のテスト"""
        config = create_production_config()

        assert config.mode == "production"
        assert config.test_mode is False
        assert config.use_mock_services is False
        assert config.log_level == "INFO"
        assert config.auto_fix is True
        assert config.batch_size == 100


class TestConfigIntegration:
    """設定の統合テスト"""

    def test_config_file_search(self, tmp_path) -> None:
        """設定ファイル検索のテスト"""
        # 設定ファイルを作成
        config_file = tmp_path / "app_config.yaml"
        config_data = {"mode": "test", "debug": True}

        with Path(config_file).open("w", encoding="utf-8") as f:
            yaml.dump(config_data, f)

        # 該当ディレクトリから実行
        with patch("pathlib.Path.cwd", return_value=tmp_path):
            manager = ConfigManager()
            config = manager.get_config()

            assert config.mode == "test"
            assert config.debug is True

    def test_multiple_config_files(self, tmp_path) -> None:
        """複数の設定ファイル名での検索テスト"""
        # 優先度の低い設定ファイル
        config_file1 = tmp_path / "settings.yaml"
        with Path(config_file1).open("w", encoding="utf-8") as f:
            yaml.dump({"mode": "production"}, f)

        # 優先度の高い設定ファイル
        config_file2 = tmp_path / "app_config.yaml"
        with Path(config_file2).open("w", encoding="utf-8") as f:
            yaml.dump({"mode": "test"}, f)

        with patch("pathlib.Path.cwd", return_value=tmp_path):
            manager = ConfigManager()

            # app_config.yamlが優先されることを確認
            assert manager.config_path == config_file2

    def test_environment_priority(self) -> None:
        """環境変数の優先度テスト"""
        with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
            # ファイルでは production
            yaml.dump({"mode": "production", "debug": False}, f)
            config_path = Path(f.name)

        try:
            # 環境変数では test
            with patch.dict(os.environ, {"APP_MODE": "test", "DEBUG": "true"}):
                manager = ConfigManager(config_path)
                config = manager.get_config()

                # 環境変数が優先されることを確認
                assert config.mode == "test"
                assert config.debug is True
        finally:
            config_path.unlink()


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
