# #region TestStorageConfigMigration [C:3] [TYPE Module] [SEMANTICS test,storage,config,git]
# @BRIEF Verify StorageConfig root_path change from "backups" to "/app/storage",
#        ConfigManager fallback without config.json, validate_path behaviour, and
#        GitPlugin shared-config-manager integration.
# @RELATION BINDS_TO -> [StorageModels]
# @RELATION BINDS_TO -> [ConfigManager]
# @RELATION BINDS_TO -> [GitPluginModule]
# @TEST_EDGE: missing_config_json -> ConfigManager falls back to _default_config when config.json absent
# @TEST_EDGE: default_root_path -> StorageConfig().root_path == "/app/storage" (was "backups")
# @TEST_EDGE: invalid_path -> validate_path returns False for /nonexistent/path
# @TEST_EDGE: git_plugin_uses_shared_config -> GitPlugin.__init__ uses shared config_manager from src.dependencies
# @TEST_INVARIANT: INV_7 (<400 lines) -> VERIFIED_BY: module_length_within_limits
# @RATIONALE Orthogonal coverage across 4 projections:
#   P1 (model contract), P4 (traceability from @POST),
#   P2 (decision-memory — GitPlugin no longer reads config.json fallback),
#   P5 (architecture realism — tests match real runtime paths).
import os
from pathlib import Path
import sys
from unittest.mock import MagicMock, patch

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

import pytest

# #region TestStorageConfigDefaults [C:2] [TYPE Class]
# @BRIEF Verify StorageConfig default field values after the root_path change.

class TestStorageConfigDefaults:
    """Verify StorageConfig default field values."""

    # #region test_root_path_default [C:2] [TYPE Function]
    # @BRIEF StorageConfig().root_path must equal "/app/storage" (was "backups").
    def test_root_path_default(self):
        from src.models.storage import StorageConfig

        config = StorageConfig()
        assert config.root_path == "/app/storage"
    # #endregion test_root_path_default

    # #region test_root_path_is_string [C:2] [TYPE Function]
    # @BRIEF root_path is always a str (type safety).
    def test_root_path_is_string(self):
        from src.models.storage import StorageConfig

        config = StorageConfig()
        assert isinstance(config.root_path, str)
    # #endregion test_root_path_is_string

    # #region test_other_defaults_unchanged [C:2] [TYPE Function]
    # @BRIEF Other StorageConfig fields were not affected by the root_path change.
    def test_other_defaults_unchanged(self):
        from src.models.storage import StorageConfig

        config = StorageConfig()
        assert config.backup_path == "backups"
        assert config.repo_path == "repositories"
        assert config.filename_pattern == "{name}_{timestamp}"
        assert config.backup_structure_pattern == "{category}/"
    # #endregion test_other_defaults_unchanged

    # #region test_global_settings_propagates_storage [C:2] [TYPE Function]
    # @BRIEF GlobalSettings().storage.root_path inherits the StorageConfig default.
    def test_global_settings_propagates_storage(self):
        from src.core.config_models import GlobalSettings
        from src.models.storage import StorageConfig

        settings = GlobalSettings()
        assert settings.storage.root_path == "/app/storage"
        assert isinstance(settings.storage, StorageConfig)
    # #endregion test_global_settings_propagates_storage

    # #region test_override_root_path [C:2] [TYPE Function]
    # @BRIEF Explicit root_path overrides the default.
    def test_override_root_path(self):
        from src.models.storage import StorageConfig

        config = StorageConfig(root_path="/custom/path")
        assert config.root_path == "/custom/path"
        # Other fields should remain at their defaults
        assert config.backup_path == "backups"
    # #endregion test_override_root_path


# #endregion TestStorageConfigDefaults


# #region TestConfigManagerDefaults [C:2] [TYPE Class]
# @BRIEF Verify ConfigManager produces correct defaults when no config.json exists.

class TestConfigManagerDefaults:
    """Verify ConfigManager initialisation without config.json (DB + file fallback)."""

    # #region test_init_falls_to_defaults [C:2] [TYPE Function]
    # @BRIEF ConfigManager(…nonexistent…) loads defaults when both DB and config.json are absent.
    # @TEST_FIXTURE: tmp_path + mocked SessionLocal returning no record.
    # @RATIONALE Full path: _load_config → no DB record → _load_from_legacy_file (absent)
    #            → _default_config().  Ensures the entire chain works without config.json.
    def test_init_falls_to_defaults(self, tmp_path: Path):
        nonexistent = tmp_path / "no_config.json"

        with patch("src.core.config_manager.SessionLocal") as mock_sl:
            mock_session = MagicMock()
            mock_sl.return_value = mock_session
            mock_session.query.return_value.filter.return_value.first.return_value = None

            from src.core.config_manager import ConfigManager

            cm = ConfigManager(config_path=str(nonexistent))
            config = cm.get_config()

            # Core assertions
            assert config.settings.storage.root_path == "/app/storage"
            assert config.settings.logging.level == "INFO"
            assert config.environments == []
    # #endregion test_init_falls_to_defaults

    # #region test_default_config_root_path [C:2] [TYPE Function]
    # @BRIEF _default_config() hardcodes root_path == "/app/storage" via StorageConfig default.
    def test_default_config_root_path(self):
        from src.core.config_manager import ConfigManager

        with patch("src.core.config_manager.SessionLocal") as mock_sl:
            mock_session = MagicMock()
            mock_sl.return_value = mock_session
            mock_session.query.return_value.filter.return_value.first.return_value = None

            cm = ConfigManager(config_path="/nonexistent-test-defaults.json")
            config = cm.get_config()

            assert config.settings.storage.root_path == "/app/storage"
    # #endregion test_default_config_root_path

    # #region test_features_from_env_still_applied [C:2] [TYPE Function]
    # @BRIEF Even without config.json, _apply_features_from_env runs during default init.
    def test_features_from_env_still_applied(self, tmp_path: Path):
        nonexistent = tmp_path / "no_config.json"

        with patch("src.core.config_manager.SessionLocal") as mock_sl:
            mock_session = MagicMock()
            mock_sl.return_value = mock_session
            mock_session.query.return_value.filter.return_value.first.return_value = None

            with patch.dict(os.environ, {"FEATURES__DATASET_REVIEW": "false"}, clear=False):
                from src.core.config_manager import ConfigManager

                cm = ConfigManager(config_path=str(nonexistent))
                config = cm.get_config()

                assert config.settings.features.dataset_review is False
                assert config.settings.storage.root_path == "/app/storage"
    # #endregion test_features_from_env_still_applied


# #endregion TestConfigManagerDefaults


# #region TestValidatePath [C:2] [TYPE Class]
# @BRIEF Verify ConfigManager.validate_path contract.

class TestValidatePath:
    """Verify validate_path creates writable directories and rejects invalid paths."""

    # #region test_validate_path_creates_directory [C:2] [TYPE Function]
    # @BRIEF validate_path creates the target directory and returns (True, "OK").
    # @TEST_FIXTURE: tmp_path ensures no side effects on real storage.
    def test_validate_path_creates_directory(self, tmp_path: Path):
        target = tmp_path / "app" / "storage"
        assert not target.exists()

        with patch("src.core.config_manager.SessionLocal") as mock_sl:
            mock_session = MagicMock()
            mock_sl.return_value = mock_session
            mock_session.query.return_value.filter.return_value.first.return_value = None

            from src.core.config_manager import ConfigManager

            cm = ConfigManager(config_path=str(tmp_path / "cfg.json"))

            is_valid, message = cm.validate_path(str(target))

            assert is_valid is True
            assert message == "OK"
            assert target.exists()
            assert target.is_dir()
    # #endregion test_validate_path_creates_directory

    # #region test_validate_path_already_exists [C:2] [TYPE Function]
    # @BRIEF validate_path succeeds when the directory already exists.
    def test_validate_path_already_exists(self, tmp_path: Path):
        existing = tmp_path / "existing" / "storage"
        existing.mkdir(parents=True)

        with patch("src.core.config_manager.SessionLocal") as mock_sl:
            mock_session = MagicMock()
            mock_sl.return_value = mock_session
            mock_session.query.return_value.filter.return_value.first.return_value = None

            from src.core.config_manager import ConfigManager

            cm = ConfigManager(config_path=str(tmp_path / "cfg.json"))

            is_valid, message = cm.validate_path(str(existing))

            assert is_valid is True
            assert message == "OK"
    # #endregion test_validate_path_already_exists

    # #region test_validate_path_nonexistent_root [C:2] [TYPE Function]
    # @BRIEF validate_path returns False for a path under / (non-writable by non-root).
    # @RATIONALE /nonexistent/path cannot be created by a non-root user.
    #            Skip when running as root because root can create directories anywhere.
    @pytest.mark.skipif(
        os.geteuid() == 0,
        reason="Cannot test permission-denied path as root",
    )
    def test_validate_path_nonexistent_root(self):
        with patch("src.core.config_manager.SessionLocal") as mock_sl:
            mock_session = MagicMock()
            mock_sl.return_value = mock_session
            mock_session.query.return_value.filter.return_value.first.return_value = None

            from src.core.config_manager import ConfigManager

            cm = ConfigManager(config_path="/tmp/__nonexistent_test_cfg.json")

            is_valid, message = cm.validate_path("/nonexistent/path")

            assert is_valid is False
            assert isinstance(message, str)
            assert len(message) > 0
    # #endregion test_validate_path_nonexistent_root

    # #region test_validate_path_read_only_parent [C:2] [TYPE Function]
    # @BRIEF validate_path returns False when the parent directory is not writable.
    def test_validate_path_read_only_parent(self, tmp_path: Path):
        import stat

        read_only_parent = tmp_path / "readonly_parent"
        read_only_parent.mkdir()
        # Remove write permission from parent
        read_only_parent.chmod(stat.S_IRUSR | stat.S_IXUSR)

        child = read_only_parent / "subdir"

        with patch("src.core.config_manager.SessionLocal") as mock_sl:
            mock_session = MagicMock()
            mock_sl.return_value = mock_session
            mock_session.query.return_value.filter.return_value.first.return_value = None

            from src.core.config_manager import ConfigManager

            cm = ConfigManager(config_path=str(tmp_path / "cfg.json"))

            is_valid, message = cm.validate_path(str(child))

            assert is_valid is False
            assert isinstance(message, str)
    # #endregion test_validate_path_read_only_parent


# #endregion TestValidatePath


# #region TestGitPluginConfigIntegration [C:2] [TYPE Class]
# @BRIEF Verify GitPlugin uses shared config_manager from src.dependencies (no config.json fallback).

class TestGitPluginConfigIntegration:
    """Verify GitPlugin shares config_manager without re-reading config.json."""

    # #region test_git_plugin_uses_shared_config_manager [C:2] [TYPE Function]
    # @BRIEF GitPlugin.__init__ picks up the shared config_manager from src.dependencies.
    # @TEST_FIXTURE: mock GitService to avoid real git initialisation.
    def test_git_plugin_uses_shared_config_manager(self):
        mock_config_manager = MagicMock()
        mock_config_manager.get_environments.return_value = []

        with (
            patch("src.plugins.git_plugin.GitService") as mock_git_svc,
            patch("src.dependencies.config_manager", mock_config_manager),
        ):
            from src.plugins.git_plugin import GitPlugin

            plugin = GitPlugin()

            # Prove the shared instance is used, NOT a freshly-created ConfigManager
            assert plugin.config_manager is mock_config_manager
            assert plugin.config_manager.get_environments() == []
            mock_git_svc.assert_called_once()
    # #endregion test_git_plugin_uses_shared_config_manager

    # #region test_git_plugin_fallback_creates_new [C:2] [TYPE Function]
    # @BRIEF When from src.dependencies import config_manager fails,
    #        GitPlugin falls back to ConfigManager().
    # @RATIONALE The try/except in GitPlugin.__init__ guards against import
    #            failure. This test forces the fallback by removing config_manager
    #            from dependencies module dict before the import executes.
    def test_git_plugin_fallback_creates_new(self):
        import src.dependencies

        # Remove config_manager so 'from src.dependencies import config_manager' raises ImportError
        saved = getattr(src.dependencies, "config_manager", None)
        if "config_manager" in src.dependencies.__dict__:
            del src.dependencies.__dict__["config_manager"]

        with (
            patch("src.plugins.git_plugin.GitService") as mock_git_svc,
            patch("src.plugins.git_plugin.ConfigManager") as mock_cm_cls,
        ):
            from src.plugins.git_plugin import GitPlugin

            plugin = GitPlugin()

            mock_cm_cls.assert_called_once()
            assert plugin.config_manager is mock_cm_cls.return_value
            mock_git_svc.assert_called_once()
        # Restore to avoid side effects in other tests
        src.dependencies.config_manager = saved
    # #endregion test_git_plugin_fallback_creates_new


# #endregion TestGitPluginConfigIntegration


# #region TestRegressionGuard [C:2] [TYPE Class]
# @BRIEF Guard against regressions in existing test contracts and stale assertions.

class TestRegressionGuard:
    """Detect stale assertions in sibling test files."""

    # #region test_existing_test_would_fail [C:2] [TYPE Function]
    # @BRIEF Existing test_storage_audit_fixes.py still asserts root_path=="backups" (WRONG).
    # @RATIONALE The sibling test at test_storage_audit_fixes.py:66 contains
    #            `assert config.root_path == "backups"` which is now a stale assertion.
    #            This test documents the intentional break and serves as a canary
    #            until that file is updated.
    def test_existing_test_would_fail(self):
        from src.models.storage import StorageConfig

        config = StorageConfig()
        # The OLD default was "backups"; the NEW default is "/app/storage".
        # This assertion does NOT match the old test!
        assert config.root_path == "/app/storage"
        assert config.root_path != "backups", (
            "Stale assertion in test_storage_audit_fixes.py:66 expects 'backups'. "
            "Update that file after confirming this change is intentional."
        )
    # #endregion test_existing_test_would_fail

    # #region test_storage_settings_endpoint_shape [C:2] [TYPE Function]
    # @BRIEF The API response shape for storage settings is still StorageConfig.
    def test_storage_settings_endpoint_shape(self):
        from src.models.storage import StorageConfig

        schema = StorageConfig.model_json_schema()
        props = schema.get("properties", {})
        assert "root_path" in props
        assert props["root_path"]["default"] == "/app/storage"
        assert props["backup_path"]["default"] == "backups"
    # #endregion test_storage_settings_endpoint_shape


# #endregion TestRegressionGuard
# #endregion TestStorageConfigMigration
