# #region Test.BackupPlugin [C:3] [TYPE Module] [SEMANTICS test, backup, plugin, coverage]
# @BRIEF Unit tests for BackupPlugin — properties, get_schema, execute, and edge cases.
# @RELATION BINDS_TO -> [BackupPlugin]
# @TEST_EDGE: missing_env -> Raises KeyError
# @TEST_EDGE: no_dashboards -> Returns NO_DASHBOARDS status
# @TEST_EDGE: partial_failure -> Returns PARTIAL_SUCCESS with failed_dashboards
# @TEST_EDGE: empty_environments -> get_schema returns empty list

import pytest
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
from pathlib import Path
from requests.exceptions import RequestException

from src.plugins.backup import BackupPlugin


# ── Helpers ──


def _make_env(id_val="env-1", name="Test Env"):
    env = MagicMock()
    env.id = id_val
    env.name = name
    return env


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


def _make_dashboard(dash_id=1, title="Test Dash"):
    return {"id": dash_id, "dashboard_title": title}


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

    def test_id(self):
        plugin = BackupPlugin()
        assert plugin.id == "superset-backup"

    def test_name(self):
        plugin = BackupPlugin()
        assert plugin.name == "Superset Dashboard Backup"

    def test_description(self):
        plugin = BackupPlugin()
        assert plugin.description == "Backs up all dashboards from a Superset instance."

    def test_version(self):
        plugin = BackupPlugin()
        assert plugin.version == "1.0.0"

    def test_ui_route(self):
        plugin = BackupPlugin()
        assert plugin.ui_route == "/tools/backups"


class TestBackupPluginGetSchema:
    """Verify get_schema — dynamic schema based on environments."""

    def test_get_schema_with_envs(self):
        plugin = BackupPlugin()
        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = [_make_env("e1", "Dev"), _make_env("e2", "Prod")]

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm):
            schema = plugin.get_schema()

        assert schema["type"] == "object"
        assert schema["properties"]["env"]["enum"] == ["Dev", "Prod"]
        assert "env" in schema["required"]

    def test_get_schema_no_envs(self):
        """Fallback to empty list when no environments configured."""
        plugin = BackupPlugin()
        mock_cm = MagicMock()
        mock_cm.get_environments.return_value = []

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm):
            schema = plugin.get_schema()

        assert schema["properties"]["env"]["enum"] == []


class TestBackupPluginExecute:
    """Verify BackupPlugin.execute with various scenarios."""

    # ── Missing env → KeyError ──

    @pytest.mark.asyncio
    async def test_execute_missing_env(self):
        """Missing env param raises KeyError."""
        plugin = BackupPlugin()
        with pytest.raises(KeyError, match="env"):
            await plugin.execute({})

    @pytest.mark.asyncio
    async def test_execute_missing_env_with_ctx(self):
        """Missing env param raises KeyError even with context."""
        plugin = BackupPlugin()
        ctx = _make_task_context()
        with pytest.raises(KeyError, match="env"):
            await plugin.execute({"other": "value"}, context=ctx)

    # ── No dashboards ──

    @pytest.mark.asyncio
    async def test_execute_no_dashboards(self):
        """Zero dashboards returns NO_DASHBOARDS status."""
        plugin = BackupPlugin()
        env = _make_env("env-1", "Source")
        mock_cm = MagicMock()
        mock_cm.has_environments.return_value = True
        mock_cm.get_environments.return_value = [env]
        mock_cm.get_environment.return_value = env
        mock_cfg = MagicMock()
        mock_cfg.settings.storage.root_path = "/tmp"
        mock_cm.get_config.return_value = mock_cfg

        mock_client = MagicMock()
        mock_client.get_dashboards = AsyncMock(return_value=(0, []))

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.backup.SupersetClient', return_value=mock_client):
            result = await plugin.execute({"env": "Source"})

        assert result["status"] == "NO_DASHBOARDS"
        assert result["total_dashboards"] == 0
        assert result["backed_up_dashboards"] == 0
        assert result["failed_dashboards"] == 0

    # ── Partial success ──

    @pytest.mark.asyncio
    async def test_execute_partial_failure(self):
        """One dashboard fails, one succeeds → PARTIAL_SUCCESS."""
        plugin = BackupPlugin()
        env = _make_env("env-1", "Source")
        mock_cm = MagicMock()
        mock_cm.has_environments.return_value = True
        mock_cm.get_environments.return_value = [env]
        mock_cm.get_environment.return_value = env
        mock_cfg = MagicMock()
        mock_cfg.settings.storage.root_path = "/tmp"
        mock_cm.get_config.return_value = mock_cfg

        dashboards = [
            _make_dashboard(1, "Good Dash"),
            _make_dashboard(2, "Bad Dash"),
        ]
        mock_client = MagicMock()
        mock_client.get_dashboards = AsyncMock(return_value=(2, dashboards))
        mock_client.export_dashboard = AsyncMock(side_effect=[
            (b"zip_content", "good.zip"),
            RequestException("Export failed"),
        ])

        ctx = _make_task_context()
        ctx.logger.progress = MagicMock()

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.backup.SupersetClient', return_value=mock_client), \
             patch('src.plugins.backup.run_blocking') as mock_run:
            mock_run.return_value = None

            result = await plugin.execute({"env": "Source"}, context=ctx)

        assert result["status"] == "PARTIAL_SUCCESS"
        assert result["total_dashboards"] == 2
        assert result["backed_up_dashboards"] == 1
        assert result["failed_dashboards"] == 1
        assert result["dashboards"][0]["title"] == "Good Dash"
        assert result["failures"][0]["title"] == "Bad Dash"

    # ── Full success with environment_id resolution ──

    @pytest.mark.asyncio
    async def test_execute_with_environment_id(self):
        """environment_id param resolves to env name."""
        plugin = BackupPlugin()
        env = _make_env("env-abc", "ResolvedEnv")
        mock_cm = MagicMock()
        mock_cm.has_environments.return_value = True
        mock_cm.get_environments.return_value = [env]
        mock_cm.get_environment.return_value = env
        mock_cfg = MagicMock()
        mock_cfg.settings.storage.root_path = "/tmp"
        mock_cm.get_config.return_value = mock_cfg

        mock_client = MagicMock()
        mock_client.get_dashboards = AsyncMock(return_value=(0, []))

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.backup.SupersetClient', return_value=mock_client):
            result = await plugin.execute({"environment_id": "env-abc"})

        assert result["status"] == "NO_DASHBOARDS"
        assert result["environment"] == "ResolvedEnv"

    # ── Dashboard ID filter ──

    @pytest.mark.asyncio
    async def test_execute_dashboard_ids_filter(self):
        """Filtering by specific dashboard IDs."""
        plugin = BackupPlugin()
        env = _make_env("env-1", "Source")
        mock_cm = MagicMock()
        mock_cm.has_environments.return_value = True
        mock_cm.get_environments.return_value = [env]
        mock_cm.get_environment.return_value = env
        mock_cfg = MagicMock()
        mock_cfg.settings.storage.root_path = "/tmp"
        mock_cm.get_config.return_value = mock_cfg

        all_dashboards = [
            _make_dashboard(1, "Dash A"),
            _make_dashboard(2, "Dash B"),
            _make_dashboard(3, "Dash C"),
        ]
        mock_client = MagicMock()
        mock_client.get_dashboards = AsyncMock(return_value=(3, all_dashboards))
        mock_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta.zip"))

        ctx = _make_task_context()
        ctx.logger.progress = MagicMock()

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.backup.SupersetClient', return_value=mock_client), \
             patch('src.plugins.backup.run_blocking') as mock_run:
            mock_run.return_value = None

            result = await plugin.execute({"env": "Source", "dashboard_ids": ["1", "3"]}, context=ctx)

        assert result["status"] == "SUCCESS"
        assert result["total_dashboards"] == 2
        assert result["dashboards"][0]["id"] == 1
        assert result["dashboards"][1]["id"] == 3

    # ── No env configured ──

    @pytest.mark.asyncio
    async def test_execute_no_environments_configured(self):
        """has_environments returns False → raise ValueError."""
        plugin = BackupPlugin()
        mock_cm = MagicMock()
        mock_cm.has_environments.return_value = False
        mock_cfg = MagicMock()
        mock_cfg.settings.storage.root_path = "/tmp"
        mock_cm.get_config.return_value = mock_cfg

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \
             pytest.raises(ValueError, match="No Superset environments configured"):
            await plugin.execute({"env": "Source"})

    # ── Environment not found ──

    @pytest.mark.asyncio
    async def test_execute_environment_not_found(self):
        """get_environment returns None → raise ValueError."""
        plugin = BackupPlugin()
        mock_cm = MagicMock()
        mock_cm.has_environments.return_value = True
        mock_cm.get_environments.return_value = [_make_env("other", "Other")]
        mock_cm.get_environment.return_value = None
        mock_cfg = MagicMock()
        mock_cfg.settings.storage.root_path = "/tmp"
        mock_cm.get_config.return_value = mock_cfg

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \
             pytest.raises(ValueError, match="not found"):
            await plugin.execute({"env": "NonExistent"})

    # ── Fatal error wraps OSError ──

    @pytest.mark.asyncio
    async def test_execute_oserror_raised(self):
        """OSError during execution propagates."""
        plugin = BackupPlugin()
        env = _make_env("env-1", "Source")
        mock_cm = MagicMock()
        mock_cm.has_environments.return_value = True
        mock_cm.get_environments.return_value = [env]
        mock_cm.get_environment.return_value = env
        mock_cfg = MagicMock()
        mock_cfg.settings.storage.root_path = "/tmp"
        mock_cm.get_config.return_value = mock_cfg

        mock_client = MagicMock()
        mock_client.get_dashboards = AsyncMock(side_effect=OSError("Disk full"))

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.backup.SupersetClient', return_value=mock_client), \
             pytest.raises(OSError, match="Disk full"):
            await plugin.execute({"env": "Source"})

    # ── Missing dashboard_id in metadata ──

    @pytest.mark.asyncio
    async def test_execute_skip_dashboard_without_id(self):
        """Dashboard without id field is skipped."""
        plugin = BackupPlugin()
        env = _make_env("env-1", "Source")
        mock_cm = MagicMock()
        mock_cm.has_environments.return_value = True
        mock_cm.get_environments.return_value = [env]
        mock_cm.get_environment.return_value = env
        mock_cfg = MagicMock()
        mock_cfg.settings.storage.root_path = "/tmp"
        mock_cm.get_config.return_value = mock_cfg

        dashboards = [
            {"dashboard_title": "No ID Dash"},  # no 'id' key
            _make_dashboard(2, "Valid Dash"),
        ]
        mock_client = MagicMock()
        mock_client.get_dashboards = AsyncMock(return_value=(2, dashboards))
        mock_client.export_dashboard = AsyncMock(return_value=(b"zip", "meta.zip"))

        ctx = _make_task_context()
        ctx.logger.progress = MagicMock()

        with patch('src.plugins.backup.get_config_manager', return_value=mock_cm), \
             patch('src.plugins.backup.SupersetClient', return_value=mock_client), \
             patch('src.plugins.backup.run_blocking') as mock_run:
            mock_run.return_value = None

            result = await plugin.execute({"env": "Source"}, context=ctx)

        assert result["status"] == "SUCCESS"
        assert result["total_dashboards"] == 2
        assert result["backed_up_dashboards"] == 1
        assert result["dashboards"][0]["title"] == "Valid Dash"
