# #region Test.StoragePlugin [C:3] [TYPE Module] [SEMANTICS test, storage, plugin, filesystem]
# @BRIEF Unit tests for StoragePlugin — properties, path resolution, file operations, and edge cases.
# @RELATION BINDS_TO -> [StoragePlugin]
# @TEST_EDGE: path_traversal -> validate_path raises ValueError
# @TEST_EDGE: missing_variable -> resolve_path falls back to stripped literal
# @TEST_EDGE: category_mismatch -> delete_file/get_file_path raise ValueError
# @TEST_EDGE: file_not_found -> delete_file/get_file_path raise FileNotFoundError
# @TEST_EDGE: missing_category -> list_files root view returns category directories

from __future__ import annotations

from datetime import datetime
from pathlib import Path
import tempfile
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock

import pytest

from src.plugins.storage.plugin import StoragePlugin, _copy_upload_to_disk
from src.models.storage import FileCategory, StoredFile


# ── Helpers ──


def _mock_config_manager(storage_root: str | None = None):
    """Create a mock ConfigManager with storage root settings."""
    cm = MagicMock()
    cfg = MagicMock()
    if storage_root:
        cfg.settings.storage.root_path = storage_root
    else:
        cfg.settings.storage.root_path = "/app/storage"
    cm.get_config.return_value = cfg
    return cm


def _mock_task_context():
    ctx = MagicMock()
    ctx.logger = MagicMock()
    ctx.logger.with_source.return_value = MagicMock()
    return ctx


class TestStoragePluginProperties:
    """Verify static property values."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_id(self, mock_get_cm):
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        assert plugin.id == "storage-manager"

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_name(self, mock_get_cm):
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        assert plugin.name == "Storage Manager"

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_description(self, mock_get_cm):
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        assert plugin.description == "Manages local file storage for backups and repositories."

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_version(self, mock_get_cm):
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        assert plugin.version == "1.0.0"

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_ui_route(self, mock_get_cm):
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        assert plugin.ui_route == "/tools/storage"


class TestStoragePluginGetSchema:
    """Verify get_schema output."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_get_schema(self, mock_get_cm):
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        schema = plugin.get_schema()

        assert schema["type"] == "object"
        assert "category" in schema["properties"]
        assert schema["properties"]["category"]["type"] == "string"
        assert schema["properties"]["category"]["enum"] == [c.value for c in FileCategory]
        assert schema["required"] == ["category"]


class TestStoragePluginExecute:
    """Verify StoragePlugin.execute with various scenarios."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    @pytest.mark.asyncio
    async def test_execute_basic(self, mock_get_cm):
        """Execute with basic params."""
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        result = await plugin.execute({"category": "backups"})
        assert result is None

    @patch("src.plugins.storage.plugin.get_config_manager")
    @pytest.mark.asyncio
    async def test_execute_with_context(self, mock_get_cm):
        """Execute with TaskContext — uses context.logger."""
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        ctx = _mock_task_context()
        result = await plugin.execute({"category": "backups"}, context=ctx)
        assert result is None
        ctx.logger.with_source.assert_called()


class TestStoragePluginGetStorageRoot:
    """Verify get_storage_root path resolution."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_absolute_path(self, mock_get_cm):
        """Absolute storage root returns directly."""
        mock_get_cm.return_value = _mock_config_manager(storage_root="/tmp/test_storage")
        plugin = StoragePlugin()
        root = plugin.get_storage_root()
        assert root == Path("/tmp/test_storage")

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_relative_path(self, mock_get_cm):
        """Relative storage root resolves against project root."""
        mock_get_cm.return_value = _mock_config_manager(storage_root="relative/storage")
        plugin = StoragePlugin()
        root = plugin.get_storage_root()
        # Should resolve relative to project root (parents[3] of plugin.py)
        assert root.is_absolute()
        assert "relative/storage" in str(root)


class TestStoragePluginResolvePath:
    """Verify resolve_path pattern substitution."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_resolve_with_variables(self, mock_get_cm):
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        result = plugin.resolve_path("backups/{name}_{timestamp}", {"name": "test"})
        # {timestamp} is auto-filled, {name} comes from variables
        assert result.startswith("backups/test_")
        # Verify timestamp format (YYYYMMDDTHHMMSS)
        timestamp_part = result.split("_", 1)[1]
        assert len(timestamp_part) == 15  # YYYYMMDDTHHMMSS

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_resolve_clean_double_slashes(self, mock_get_cm):
        """Double slashes are normalized."""
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        result = plugin.resolve_path("backups//{name}", {"name": "test"})
        assert "//" not in result
        assert result == "backups/test"

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_resolve_missing_variable(self, mock_get_cm):
        """Missing variable falls back to stripped literal."""
        mock_get_cm.return_value = _mock_config_manager()
        plugin = StoragePlugin()
        result = plugin.resolve_path("backups/{name}_{missing}", {"name": "test"})
        # Fallback: strip braces from the pattern
        assert "{" not in result
        assert "}" not in result


class TestStoragePluginValidatePath:
    """Verify validate_path prevents traversal and returns valid paths."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_valid_path(self, mock_get_cm):
        """Path within storage root is accepted."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            # ensure_directories creates category subdirs — don't rmdir parent
            plugin = StoragePlugin()
            valid = Path(tmpdir) / "test_file.txt"
            valid.touch()
            result = plugin.validate_path(valid)
            assert result == valid.resolve()

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_path_traversal_raises(self, mock_get_cm):
        """Path outside storage root raises ValueError."""
        mock_get_cm.return_value = _mock_config_manager(storage_root="/tmp/test_storage_traversal")
        plugin = StoragePlugin()
        outside = Path("/etc/passwd")
        with pytest.raises(ValueError, match="Access denied"):
            plugin.validate_path(outside)


class TestStoragePluginEnsureDirectories:
    """Verify ensure_directories creates category subfolders."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_ensure_directories_creates_folders(self, mock_get_cm):
        """Category directories are created under storage root."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            plugin = StoragePlugin()
            # ensure_directories called in __init__ already
            for cat in FileCategory:
                assert (Path(tmpdir) / cat.value).exists()


class TestStoragePluginListFiles:
    """Verify list_files in various modes."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_root_view(self, mock_get_cm):
        """Root view (no category) returns category directories."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            # Create category directories
            for cat in FileCategory:
                (Path(tmpdir) / cat.value).mkdir(parents=True, exist_ok=True)

            plugin = StoragePlugin()
            files = plugin.list_files()

            assert len(files) == len(FileCategory)
            names = {f.name for f in files}
            for cat in FileCategory:
                assert cat.value in names
            # All returned as directories
            for f in files:
                assert f.mime_type == "directory"
                assert f.size == 0

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_with_category(self, mock_get_cm):
        """Filter by category returns files in that category."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)
            (backup_dir / "file1.txt").write_text("hello")
            (backup_dir / "file2.txt").write_text("world")

            plugin = StoragePlugin()
            files = plugin.list_files(category=FileCategory.BACKUP)

            assert len(files) == 2
            assert files[0].name == "file1.txt"
            assert files[1].name == "file2.txt"

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_recursive(self, mock_get_cm):
        """Recursive mode scans subdirectories."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            nested = backup_dir / "sub" / "dir"
            nested.mkdir(parents=True, exist_ok=True)
            (nested / "deep.txt").write_text("deep")

            plugin = StoragePlugin()
            files = plugin.list_files(category=FileCategory.BACKUP, recursive=True)

            assert len(files) == 1
            assert files[0].name == "deep.txt"

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_missing_category_dir(self, mock_get_cm):
        """Missing category directory returns empty list."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            # Don't create any directories
            plugin = StoragePlugin()
            files = plugin.list_files(category=FileCategory.BACKUP)
            assert files == []

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_skip_logs(self, mock_get_cm):
        """Files/dirs containing 'Logs' are skipped."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)
            (backup_dir / "good.txt").write_text("good")
            (backup_dir / "LogsFile.txt").write_text("logs")
            log_dir = backup_dir / "Logs"
            log_dir.mkdir()

            plugin = StoragePlugin()
            files = plugin.list_files(category=FileCategory.BACKUP)

            # Only good.txt should appear
            names = [f.name for f in files]
            assert "good.txt" in names
            assert "LogsFile.txt" not in names

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_skip_logs_recursive(self, mock_get_cm):
        """Recursive scan skips files with 'Logs' in their filename (L284)."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            nested = backup_dir / "daily"
            nested.mkdir(parents=True, exist_ok=True)
            (nested / "report.txt").write_text("clean")
            (nested / "TransactionLogs.csv").write_text("log data")

            plugin = StoragePlugin()
            files = plugin.list_files(category=FileCategory.BACKUP, recursive=True)

            assert len(files) == 1
            assert files[0].name == "report.txt"

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_recursive_missing_dir(self, mock_get_cm):
        """Recursive mode with missing category dir continues silently (L274)."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            # Patch ensure_directories so category dirs are NOT created
            with patch.object(StoragePlugin, "ensure_directories"):
                plugin = StoragePlugin()
            files = plugin.list_files(category=FileCategory.BACKUP, recursive=True)
            assert files == []

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_root_view_missing_categories(self, mock_get_cm):
        """Root view listing skips missing category dirs via continue (L249)."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            # Patch ensure_directories so only ONE category exists on disk
            with patch.object(StoragePlugin, "ensure_directories"):
                plugin = StoragePlugin()
            # Manually create only one category dir
            (Path(tmpdir) / "backups").mkdir()

            files = plugin.list_files()

            # Only "backups" should appear; "repositories" is missing → hits L249 continue
            assert len(files) == 1
            assert files[0].name == "backups"

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_list_files_with_subpath(self, mock_get_cm):
        """Subpath within category narrows listing."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            nested = Path(tmpdir) / "backups" / "monthly" / "week1"
            nested.mkdir(parents=True, exist_ok=True)
            (nested / "report.txt").write_text("report")

            plugin = StoragePlugin()
            files = plugin.list_files(
                category=FileCategory.BACKUP,
                subpath="monthly/week1",
            )

            assert len(files) == 1
            assert files[0].name == "report.txt"


class TestStoragePluginSaveFile:
    """Verify save_file writes uploaded content to disk."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    @pytest.mark.asyncio
    async def test_save_file_without_subpath(self, mock_get_cm):
        """File saved directly under category directory."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)

            upload_file = MagicMock()
            upload_file.filename = "test_doc.pdf"
            upload_file.content_type = "application/pdf"
            mock_file_obj = MagicMock()
            mock_file_obj.read.return_value = b""
            upload_file.file = mock_file_obj

            plugin = StoragePlugin()
            result = await plugin.save_file(
                file=upload_file,
                category=FileCategory.BACKUP,
            )

            assert result.name == "test_doc.pdf"
            assert result.category == FileCategory.BACKUP
            assert result.mime_type == "application/pdf"
            assert result.path.startswith("backups/")
            assert isinstance(result, StoredFile)

    @patch("src.plugins.storage.plugin.get_config_manager")
    @pytest.mark.asyncio
    async def test_save_file_with_subpath(self, mock_get_cm):
        """File saved under category/subpath."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)

            upload_file = MagicMock()
            upload_file.filename = "report.xlsx"
            upload_file.content_type = (
                "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
            )
            mock_file_obj = MagicMock()
            mock_file_obj.read.return_value = b""
            upload_file.file = mock_file_obj

            plugin = StoragePlugin()
            result = await plugin.save_file(
                file=upload_file,
                category=FileCategory.REPOSITORY,
                subpath="archived/2024",
            )

            assert result.name == "report.xlsx"
            assert result.category == FileCategory.REPOSITORY
            assert "archived/2024" in result.path


class TestStoragePluginDeleteFile:
    """Verify delete_file removes files and directories."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    @pytest.mark.asyncio
    async def test_delete_file_success(self, mock_get_cm):
        """Existing file is deleted."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)
            target = backup_dir / "to_delete.txt"
            target.write_text("bye")

            plugin = StoragePlugin()
            await plugin.delete_file(FileCategory.BACKUP, "backups/to_delete.txt")

            assert not target.exists()

    @patch("src.plugins.storage.plugin.get_config_manager")
    @pytest.mark.asyncio
    async def test_delete_directory_success(self, mock_get_cm):
        """Existing directory is removed via shutil.rmtree."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            target_dir = backup_dir / "old_backup"
            target_dir.mkdir(parents=True, exist_ok=True)
            (target_dir / "inner.txt").write_text("inner")

            plugin = StoragePlugin()
            await plugin.delete_file(FileCategory.BACKUP, "backups/old_backup")

            assert not target_dir.exists()

    @patch("src.plugins.storage.plugin.get_config_manager")
    @pytest.mark.asyncio
    async def test_delete_file_not_found(self, mock_get_cm):
        """Non-existent file raises FileNotFoundError."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)

            plugin = StoragePlugin()
            with pytest.raises(FileNotFoundError, match="not found"):
                await plugin.delete_file(FileCategory.BACKUP, "backups/ghost.txt")

    @patch("src.plugins.storage.plugin.get_config_manager")
    @pytest.mark.asyncio
    async def test_delete_file_category_mismatch(self, mock_get_cm):
        """Path not starting with category value raises ValueError."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)
            target = backup_dir / "file.txt"
            target.write_text("data")

            plugin = StoragePlugin()
            with pytest.raises(ValueError, match="does not belong to category"):
                await plugin.delete_file(FileCategory.BACKUP, "repositories/file.txt")


class TestStoragePluginGetFilePath:
    """Verify get_file_path returns valid paths."""

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_get_file_path_success(self, mock_get_cm):
        """Existing file returns resolved path."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)
            target = backup_dir / "download.txt"
            target.write_text("content")

            plugin = StoragePlugin()
            result = plugin.get_file_path(FileCategory.BACKUP, "backups/download.txt")

            assert result == target.resolve()

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_get_file_path_directory_raises(self, mock_get_cm):
        """Directory path raises FileNotFoundError (not a file)."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)

            plugin = StoragePlugin()
            with pytest.raises(FileNotFoundError, match="not found"):
                plugin.get_file_path(FileCategory.BACKUP, "backups")

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_get_file_path_not_found(self, mock_get_cm):
        """Non-existent file raises FileNotFoundError."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)

            plugin = StoragePlugin()
            with pytest.raises(FileNotFoundError, match="not found"):
                plugin.get_file_path(FileCategory.BACKUP, "backups/missing.txt")

    @patch("src.plugins.storage.plugin.get_config_manager")
    def test_get_file_path_category_mismatch(self, mock_get_cm):
        """Path not starting with category value raises ValueError."""
        with tempfile.TemporaryDirectory() as tmpdir:
            mock_get_cm.return_value = _mock_config_manager(storage_root=tmpdir)
            backup_dir = Path(tmpdir) / "backups"
            backup_dir.mkdir(parents=True, exist_ok=True)

            plugin = StoragePlugin()
            with pytest.raises(ValueError, match="does not belong to category"):
                plugin.get_file_path(FileCategory.BACKUP, "repositories/download.txt")


class TestCopyUploadToDisk:
    """Verify the helper function _copy_upload_to_disk."""

    def test_copy_upload_to_disk(self):
        """File content is copied from source to destination."""
        with tempfile.TemporaryDirectory() as tmpdir:
            dest = Path(tmpdir) / "output.bin"
            source = MagicMock()
            # Return data once, then empty bytes to signal EOF
            source.read = MagicMock(side_effect=[b"test data", b""])

            _copy_upload_to_disk(dest, source)

            assert dest.read_bytes() == b"test data"

    def test_copy_upload_to_disk_empty(self):
        """Empty source file is handled."""
        with tempfile.TemporaryDirectory() as tmpdir:
            dest = Path(tmpdir) / "empty.bin"
            source = MagicMock()
            source.read = MagicMock(return_value=b"")

            _copy_upload_to_disk(dest, source)

            assert dest.read_bytes() == b""
# #endregion Test.StoragePlugin
