# #region Test.MapperPlugin [C:3] [TYPE Module] [SEMANTICS test, mapper, plugin, dataset, superset]
# @BRIEF Unit tests for MapperPlugin — properties, schema, execute with sqllab/excel sources, error branches.
# @RELATION BINDS_TO -> [MapperPlugin]
# @TEST_EDGE: missing_params -> raises ValueError
# @TEST_EDGE: env_not_found -> raises ValueError
# @TEST_EDGE: sqllab_missing_database_id -> raises ValueError
# @TEST_EDGE: upstream_exception -> re-raises

from __future__ import annotations

from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.plugins.mapper import MapperPlugin


# ── Helpers ──


def _mock_env_config(name: str = "dev"):
    env = MagicMock()
    env.id = f"env-{name}"
    env.name = name
    return env


def _mock_config_manager(env=None):
    cm = MagicMock()
    if env:
        cm.get_environment.return_value = env
    else:
        cm.get_environment.return_value = None
    cm.get_config.return_value = MagicMock()
    return cm


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


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

    def test_id(self):
        plugin = MapperPlugin()
        assert plugin.id == "dataset-mapper"

    def test_name(self):
        plugin = MapperPlugin()
        assert plugin.name == "Dataset Mapper"

    def test_description(self):
        plugin = MapperPlugin()
        assert plugin.description == "Map dataset column verbose names using Superset SQL Lab or Excel files."

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

    def test_ui_route(self):
        plugin = MapperPlugin()
        assert plugin.ui_route == "/tools/mapper"


class TestMapperPluginGetSchema:
    """Verify get_schema output."""

    def test_get_schema(self):
        plugin = MapperPlugin()
        schema = plugin.get_schema()

        assert schema["type"] == "object"
        props = schema["properties"]
        assert "env" in props
        assert "dataset_id" in props
        assert "source" in props
        assert props["source"]["enum"] == ["sqllab", "excel"]
        assert props["source"]["default"] == "sqllab"
        assert schema["required"] == ["env", "dataset_id", "source"]


class TestMapperPluginExecute:
    """Verify MapperPlugin.execute with various scenarios."""

    # ── Missing params ──

    @pytest.mark.asyncio
    async def test_execute_missing_all_params(self):
        """No params at all raises ValueError."""
        plugin = MapperPlugin()
        with pytest.raises(ValueError, match="Missing required parameters"):
            await plugin.execute({})

    @pytest.mark.asyncio
    async def test_execute_missing_env(self):
        """Missing env param raises ValueError."""
        plugin = MapperPlugin()
        with pytest.raises(ValueError, match="Missing required parameters"):
            await plugin.execute({"dataset_id": 1, "source": "sqllab"})

    @pytest.mark.asyncio
    async def test_execute_missing_dataset_id(self):
        """None dataset_id raises ValueError."""
        plugin = MapperPlugin()
        with pytest.raises(ValueError, match="Missing required parameters"):
            await plugin.execute({"env": "dev", "source": "sqllab"})

    @pytest.mark.asyncio
    async def test_execute_missing_source(self):
        """Missing source raises ValueError."""
        plugin = MapperPlugin()
        with pytest.raises(ValueError, match="Missing required parameters"):
            await plugin.execute({"env": "dev", "dataset_id": 1})

    @pytest.mark.asyncio
    async def test_execute_missing_params_with_context(self):
        """Missing params also fails when context is provided."""
        plugin = MapperPlugin()
        ctx = _mock_task_context()
        with pytest.raises(ValueError, match="Missing required parameters"):
            await plugin.execute({"env": "dev"}, context=ctx)

    # ── Environment not found ──

    @pytest.mark.asyncio
    async def test_execute_env_not_found(self):
        """Environment not in config raises ValueError."""
        plugin = MapperPlugin()
        mock_cm = _mock_config_manager(env=None)  # get_environment returns None

        with patch("src.dependencies.get_config_manager", return_value=mock_cm):
            with pytest.raises(ValueError, match="not found"):
                await plugin.execute({"env": "nonexistent", "dataset_id": 1, "source": "sqllab"})

    # ── SQLLab source — missing database_id ──

    @pytest.mark.asyncio
    async def test_execute_sqllab_missing_database_id(self):
        """SQLLab source without database_id raises ValueError."""
        plugin = MapperPlugin()
        env = _mock_env_config("dev")
        mock_cm = _mock_config_manager(env=env)

        mock_client = MagicMock()
        mock_client.authenticate = MagicMock()

        with (
            patch("src.dependencies.get_config_manager", return_value=mock_cm),
            patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
        ):
            with pytest.raises(ValueError, match="database_id is required"):
                await plugin.execute({"env": "dev", "dataset_id": 1, "source": "sqllab"})

    # ── SQLLab source — success ──

    @pytest.mark.asyncio
    async def test_execute_sqllab_success(self):
        """SQLLab source completes successfully."""
        plugin = MapperPlugin()
        env = _mock_env_config("dev")
        mock_cm = _mock_config_manager(env=env)

        mock_client = MagicMock()
        mock_client.authenticate = MagicMock()

        mock_executor = AsyncMock()
        mock_executor.resolve_database_id = AsyncMock()

        mock_mapper = MagicMock()
        mock_mapper.run_mapping = AsyncMock()

        with (
            patch("src.dependencies.get_config_manager", return_value=mock_cm),
            patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
            patch("src.plugins.translate.superset_executor.SupersetSqlLabExecutor", return_value=mock_executor),
            patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
        ):
            result = await plugin.execute({
                "env": "dev",
                "dataset_id": 42,
                "source": "sqllab",
                "database_id": 10,
                "sql_query": "SELECT * FROM cols",
            })

        assert result["status"] == "success"
        assert result["dataset_id"] == 42
        mock_executor.resolve_database_id.assert_called_once_with(target_database_id="10")
        mock_mapper.run_mapping.assert_called_once()

    @pytest.mark.asyncio
    async def test_execute_sqllab_success_with_context(self):
        """SQLLab source with TaskContext."""
        plugin = MapperPlugin()
        env = _mock_env_config("dev")
        mock_cm = _mock_config_manager(env=env)

        mock_client = MagicMock()
        mock_client.authenticate = MagicMock()

        mock_executor = AsyncMock()
        mock_executor.resolve_database_id = AsyncMock()

        mock_mapper = MagicMock()
        mock_mapper.run_mapping = AsyncMock()

        ctx = _mock_task_context()

        with (
            patch("src.dependencies.get_config_manager", return_value=mock_cm),
            patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
            patch("src.plugins.translate.superset_executor.SupersetSqlLabExecutor", return_value=mock_executor),
            patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
        ):
            result = await plugin.execute({
                "env": "dev",
                "dataset_id": 7,
                "source": "sqllab",
                "database_id": 3,
            }, context=ctx)

        assert result["status"] == "success"
        assert result["dataset_id"] == 7
        ctx.logger.with_source.assert_called()

    # ── Excel source — success ──

    @pytest.mark.asyncio
    async def test_execute_excel_success(self):
        """Excel source completes successfully."""
        plugin = MapperPlugin()
        env = _mock_env_config("dev")
        mock_cm = _mock_config_manager(env=env)

        mock_client = MagicMock()
        mock_client.authenticate = MagicMock()

        mock_mapper = MagicMock()
        mock_mapper.run_mapping = AsyncMock()

        with (
            patch("src.dependencies.get_config_manager", return_value=mock_cm),
            patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
            patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
        ):
            result = await plugin.execute({
                "env": "dev",
                "dataset_id": 99,
                "source": "excel",
                "excel_path": "/data/mapping.xlsx",
            })

        assert result["status"] == "success"
        assert result["dataset_id"] == 99
        mock_mapper.run_mapping.assert_called_once_with(
            superset_client=mock_client,
            dataset_id=99,
            source="excel",
            excel_path="/data/mapping.xlsx",
        )

    @pytest.mark.asyncio
    async def test_execute_excel_with_file_data(self):
        """Excel source uses file_data fallback when excel_path is missing."""
        plugin = MapperPlugin()
        env = _mock_env_config("dev")
        mock_cm = _mock_config_manager(env=env)

        mock_client = MagicMock()
        mock_client.authenticate = MagicMock()

        mock_mapper = MagicMock()
        mock_mapper.run_mapping = AsyncMock()

        with (
            patch("src.dependencies.get_config_manager", return_value=mock_cm),
            patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
            patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
        ):
            result = await plugin.execute({
                "env": "dev",
                "dataset_id": 100,
                "source": "excel",
                "file_data": b"raw_excel_data",
            })

        assert result["status"] == "success"
        mock_mapper.run_mapping.assert_called_once_with(
            superset_client=mock_client,
            dataset_id=100,
            source="excel",
            excel_path=b"raw_excel_data",
        )

    # ── Upstream exception propagation ──

    @pytest.mark.asyncio
    async def test_execute_upstream_exception_propagates(self):
        """Exception from SupersetClient.authenticate re-raises."""
        plugin = MapperPlugin()
        env = _mock_env_config("dev")
        mock_cm = _mock_config_manager(env=env)

        mock_client = MagicMock()
        mock_client.authenticate = MagicMock(side_effect=ConnectionError("API unreachable"))

        with (
            patch("src.dependencies.get_config_manager", return_value=mock_cm),
            patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
        ):
            with pytest.raises(ConnectionError, match="API unreachable"):
                await plugin.execute({"env": "dev", "dataset_id": 1, "source": "sqllab"})

    @pytest.mark.asyncio
    async def test_execute_mapping_failure_propagates(self):
        """Exception from run_mapping re-raises."""
        plugin = MapperPlugin()
        env = _mock_env_config("dev")
        mock_cm = _mock_config_manager(env=env)

        mock_client = MagicMock()
        mock_client.authenticate = MagicMock()

        mock_executor = AsyncMock()
        mock_executor.resolve_database_id = AsyncMock()

        mock_mapper = MagicMock()
        mock_mapper.run_mapping = AsyncMock(side_effect=RuntimeError("Mapping crashed"))

        with (
            patch("src.dependencies.get_config_manager", return_value=mock_cm),
            patch("src.plugins.mapper.SupersetClient", return_value=mock_client),
            patch("src.plugins.translate.superset_executor.SupersetSqlLabExecutor", return_value=mock_executor),
            patch("src.plugins.mapper.DatasetMapper", return_value=mock_mapper),
        ):
            with pytest.raises(RuntimeError, match="Mapping crashed"):
                await plugin.execute({
                    "env": "dev", "dataset_id": 1,
                    "source": "sqllab", "database_id": 5,
                })
# #endregion Test.MapperPlugin
