# #region Test.Assistant.EdgeCases [C:2] [TYPE Module] [SEMANTICS test,assistant,routes,dispatch,resolvers,tools,edge]
# @BRIEF Edge-case coverage for _routes.py, _dispatch.py, _resolvers.py, and tool files.
#        Covers confirmation/cancel routes, dry-run summary, resolver fallbacks,
#        and tool handler error paths.
# @RELATION BINDS_TO -> [AssistantRoutes]
# @RELATION BINDS_TO -> [AssistantDispatch]
# @RELATION BINDS_TO -> [AssistantResolvers]
# @RELATION BINDS_TO -> [AssistantToolBackup]
# @RELATION BINDS_TO -> [AssistantToolMigration]
# @RELATION BINDS_TO -> [AssistantToolSearchDashboards]
# @RELATION BINDS_TO -> [AssistantToolTaskStatus]
# @RELATION BINDS_TO -> [AssistantToolRegistry]

from pathlib import Path
import sys

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

from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock
import pytest
from fastapi import HTTPException


# ── Shared helpers ─────────────────────────────────────────────────


def _make_env(id_: str, name_: str, stage="DEV", is_production=False, is_default=False):
    env = MagicMock()
    env.id = id_
    env.name = name_
    env.stage = stage
    env.is_production = is_production
    env.is_default = is_default
    return env


# ── _routes.py: confirm_operation and cancel_operation ────────────


class TestAssistantRoutesConfirmation:
    """confirm_operation and cancel_operation endpoints."""

    @pytest.fixture
    def current_user(self):
        from src.schemas.auth import User
        return User(id="user-1", username="test", email="test@x.com", is_active=True,
                    auth_source="internal", created_at=datetime.now(UTC),
                    last_login=datetime.now(UTC), roles=[])

    # ── confirm_operation ──

    @pytest.mark.asyncio
    async def test_confirm_found_in_memory(self, current_user):
        """Confirm from in-memory CONFIRMATIONS store."""
        from src.api.routes.assistant._routes import confirm_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS, ConfirmationRecord

        CONFIRMATIONS.clear()
        cid = "cf-test-1"
        CONFIRMATIONS[cid] = ConfirmationRecord(
            id=cid,
            user_id="user-1",
            conversation_id="conv-1",
            intent={"operation": "show_capabilities", "entities": {}},
            dispatch={"intent": {}},
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )

        with patch("src.api.routes.assistant._routes.dispatch",
                   new=AsyncMock(return_value=("done", "task-1", []))) as mock_dispatch, \
             patch("src.api.routes.assistant._routes._append_history"), \
             patch("src.api.routes.assistant._routes._persist_message"), \
             patch("src.api.routes.assistant._routes._audit"), \
             patch("src.api.routes.assistant._routes._persist_audit"), \
             patch("src.api.routes.assistant._routes._update_confirmation_state"):

            resp = await confirm_operation(
                confirmation_id=cid,
                current_user=current_user,
                task_manager=MagicMock(),
                config_manager=MagicMock(),
                db=MagicMock(),
            )
            assert resp.state in ("success", "started")
            mock_dispatch.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_confirm_not_found_404(self, current_user):
        """Missing confirmation_id returns 404."""
        from src.api.routes.assistant._routes import confirm_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS

        CONFIRMATIONS.clear()

        with patch("src.api.routes.assistant._routes._load_confirmation_from_db", return_value=None):
            with pytest.raises(HTTPException) as exc:
                await confirm_operation(
                    confirmation_id="nonexistent",
                    current_user=current_user,
                    task_manager=MagicMock(),
                    config_manager=MagicMock(),
                    db=MagicMock(),
                )
            assert exc.value.status_code == 404

    @pytest.mark.asyncio
    async def test_confirm_wrong_user_403(self, current_user):
        """Confirmation belonging to another user returns 403."""
        from src.api.routes.assistant._routes import confirm_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS, ConfirmationRecord

        CONFIRMATIONS.clear()
        cid = "cf-wrong-user"
        CONFIRMATIONS[cid] = ConfirmationRecord(
            id=cid, user_id="user-2", conversation_id="conv-1",
            intent={}, dispatch={},
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )

        with pytest.raises(HTTPException) as exc:
            await confirm_operation(
                confirmation_id=cid, current_user=current_user,
                task_manager=MagicMock(), config_manager=MagicMock(), db=MagicMock(),
            )
        assert exc.value.status_code == 403

    @pytest.mark.asyncio
    async def test_confirm_already_consumed_400(self, current_user):
        """Already consumed/expired confirmation returns 400."""
        from src.api.routes.assistant._routes import confirm_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS, ConfirmationRecord

        CONFIRMATIONS.clear()
        cid = "cf-consumed"
        CONFIRMATIONS[cid] = ConfirmationRecord(
            id=cid, user_id="user-1", conversation_id="conv-1",
            intent={}, dispatch={}, state="consumed",
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )

        with pytest.raises(HTTPException) as exc:
            await confirm_operation(
                confirmation_id=cid, current_user=current_user,
                task_manager=MagicMock(), config_manager=MagicMock(), db=MagicMock(),
            )
        assert exc.value.status_code == 400
        assert "already" in str(exc.value.detail)

    @pytest.mark.asyncio
    async def test_confirm_expired_400(self, current_user):
        """Expired confirmation returns 400."""
        from src.api.routes.assistant._routes import confirm_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS, ConfirmationRecord

        CONFIRMATIONS.clear()
        cid = "cf-expired"
        CONFIRMATIONS[cid] = ConfirmationRecord(
            id=cid, user_id="user-1", conversation_id="conv-1",
            intent={}, dispatch={}, state="pending",
            expires_at=datetime.now(UTC) - timedelta(minutes=5),
            created_at=datetime.now(UTC) - timedelta(minutes=10),
        )

        with patch("src.api.routes.assistant._routes._update_confirmation_state"):
            with pytest.raises(HTTPException) as exc:
                await confirm_operation(
                    confirmation_id=cid, current_user=current_user,
                    task_manager=MagicMock(), config_manager=MagicMock(), db=MagicMock(),
                )
            assert exc.value.status_code == 400
            assert "expired" in str(exc.value.detail)

    @pytest.mark.asyncio
    async def test_confirm_loaded_from_db(self, current_user):
        """Confirmation loaded from DB when not in memory."""
        from src.api.routes.assistant._routes import confirm_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS, ConfirmationRecord

        CONFIRMATIONS.clear()
        cid = "cf-db-loaded"

        db_record = ConfirmationRecord(
            id=cid, user_id="user-1", conversation_id="conv-1",
            intent={"operation": "show_capabilities", "entities": {}}, dispatch={},
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )

        with patch("src.api.routes.assistant._routes._load_confirmation_from_db", return_value=db_record), \
             patch("src.api.routes.assistant._routes.dispatch",
                   new=AsyncMock(return_value=("done", None, []))), \
             patch("src.api.routes.assistant._routes._append_history"), \
             patch("src.api.routes.assistant._routes._persist_message"), \
             patch("src.api.routes.assistant._routes._audit"), \
             patch("src.api.routes.assistant._routes._persist_audit"), \
             patch("src.api.routes.assistant._routes._update_confirmation_state"):

            resp = await confirm_operation(
                confirmation_id=cid, current_user=current_user,
                task_manager=MagicMock(), config_manager=MagicMock(), db=MagicMock(),
            )
            assert resp.state in ("success", "started")

    # ── cancel_operation ──

    @pytest.mark.asyncio
    async def test_cancel_success(self, current_user):
        """Cancel pending confirmation."""
        from src.api.routes.assistant._routes import cancel_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS, ConfirmationRecord

        CONFIRMATIONS.clear()
        cid = "cf-cancel-1"
        CONFIRMATIONS[cid] = ConfirmationRecord(
            id=cid, user_id="user-1", conversation_id="conv-1",
            intent={"operation": "deploy_dashboard"}, dispatch={},
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )

        with patch("src.api.routes.assistant._routes._update_confirmation_state"), \
             patch("src.api.routes.assistant._routes._append_history"), \
             patch("src.api.routes.assistant._routes._persist_message"), \
             patch("src.api.routes.assistant._routes._audit"), \
             patch("src.api.routes.assistant._routes._persist_audit"):

            resp = await cancel_operation(
                confirmation_id=cid, current_user=current_user, db=MagicMock(),
            )
            assert resp.state == "success"
            assert "отменена" in resp.text

    @pytest.mark.asyncio
    async def test_cancel_not_found_404(self, current_user):
        """Cancel nonexistent confirmation."""
        from src.api.routes.assistant._routes import cancel_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS

        CONFIRMATIONS.clear()

        with patch("src.api.routes.assistant._routes._load_confirmation_from_db", return_value=None):
            with pytest.raises(HTTPException) as exc:
                await cancel_operation(
                    confirmation_id="nonexistent", current_user=current_user, db=MagicMock(),
                )
            assert exc.value.status_code == 404

    @pytest.mark.asyncio
    async def test_cancel_wrong_user_403(self, current_user):
        """Cancel belonging to another user."""
        from src.api.routes.assistant._routes import cancel_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS, ConfirmationRecord

        CONFIRMATIONS.clear()
        cid = "cf-cancel-wrong"
        CONFIRMATIONS[cid] = ConfirmationRecord(
            id=cid, user_id="user-2", conversation_id="conv-1",
            intent={}, dispatch={},
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )

        with pytest.raises(HTTPException) as exc:
            await cancel_operation(
                confirmation_id=cid, current_user=current_user, db=MagicMock(),
            )
        assert exc.value.status_code == 403

    @pytest.mark.asyncio
    async def test_cancel_already_consumed_400(self, current_user):
        """Cancel already consumed confirmation."""
        from src.api.routes.assistant._routes import cancel_operation
        from src.api.routes.assistant._schemas import CONFIRMATIONS, ConfirmationRecord

        CONFIRMATIONS.clear()
        cid = "cf-cancel-consumed"
        CONFIRMATIONS[cid] = ConfirmationRecord(
            id=cid, user_id="user-1", conversation_id="conv-1",
            intent={}, dispatch={}, state="consumed",
            expires_at=datetime.now(UTC) + timedelta(minutes=5),
            created_at=datetime.now(UTC),
        )

        with pytest.raises(HTTPException) as exc:
            await cancel_operation(
                confirmation_id=cid, current_user=current_user, db=MagicMock(),
            )
        assert exc.value.status_code == 400

    # ── _routes.py line 150: failed state (non-clarification HTTPException) ──

    @pytest.mark.asyncio
    async def test_send_message_failed_state(self):
        """Non-403, non-clarification HTTPException results in 'failed' state."""
        from src.api.routes.assistant._routes import send_message
        from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
        from src.api.routes.assistant._tool_registry import _tools

        CONFIRMATIONS.clear()
        _tools.clear()

        req = AssistantMessageRequest(message="do something")

        with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"), \
             patch("src.api.routes.assistant._routes._append_history"), \
             patch("src.api.routes.assistant._routes._persist_message"), \
             patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[]), \
             patch("src.api.routes.assistant._routes._plan_intent_with_llm",
                   return_value={"domain": "d", "operation": "op", "entities": {}, "confidence": 0.9, "risk_level": "guarded"}), \
             patch("src.api.routes.assistant._routes._authorize_intent"), \
             patch("src.api.routes.assistant._routes.get_safe_ops", return_value={"op"}), \
             patch("src.api.routes.assistant._routes.dispatch",
                   side_effect=HTTPException(status_code=500, detail="Internal error")), \
             patch("src.api.routes.assistant._routes._audit"), \
             patch("src.api.routes.assistant._routes._persist_audit"), \
             patch("src.api.routes.assistant._routes._load_dataset_review_context", return_value=None):

            current_user = MagicMock(id="user-1")
            resp = await send_message(
                req, current_user, MagicMock(), MagicMock(), MagicMock()
            )
            assert resp.state == "failed"

    @pytest.mark.asyncio
    async def test_send_message_dataset_review_overrides_intent(self):
        """dataset_review_intent overrides intent when context is active."""
        from src.api.routes.assistant._routes import send_message
        from src.api.routes.assistant._schemas import AssistantMessageRequest, CONFIRMATIONS
        from src.api.routes.assistant._tool_registry import _tools

        CONFIRMATIONS.clear()
        _tools.clear()

        req = AssistantMessageRequest(message="show filters", dataset_review_session_id="sess-1")

        with patch("src.api.routes.assistant._routes._resolve_or_create_conversation", return_value="conv-1"), \
             patch("src.api.routes.assistant._routes._append_history"), \
             patch("src.api.routes.assistant._routes._persist_message"), \
             patch("src.api.routes.assistant._routes._build_tool_catalog", return_value=[]), \
             patch("src.api.routes.assistant._routes._plan_intent_with_llm",
                   return_value={"domain": "original", "operation": "orig_op", "entities": {}, "confidence": 0.9}), \
             patch("src.api.routes.assistant._routes._parse_command",
                   return_value={"domain": "unknown", "operation": "clarify", "entities": {}, "confidence": 0.3}), \
             patch("src.api.routes.assistant._routes._plan_dataset_review_intent",
                   return_value={"domain": "dataset_review", "operation": "dataset_review_answer_context",
                                 "entities": {"summary": "test"}, "confidence": 0.8, "risk_level": "safe",
                                 "requires_confirmation": False}), \
             patch("src.api.routes.assistant._routes._authorize_intent"), \
             patch("src.api.routes.assistant._routes.get_safe_ops", return_value={"dataset_review_answer_context"}), \
             patch("src.api.routes.assistant._routes.dispatch",
                   new=AsyncMock(return_value=("context answer", None, []))), \
             patch("src.api.routes.assistant._routes._audit"), \
             patch("src.api.routes.assistant._routes._persist_audit"), \
             patch("src.api.routes.assistant._routes._load_dataset_review_context",
                   return_value={"session_id": "sess-1"}):

            current_user = MagicMock(id="user-1")
            resp = await send_message(
                req, current_user, MagicMock(), MagicMock(), MagicMock()
            )
            assert resp.state in ("success", "started")


# ── _dispatch.py: dry-run code path ──────────────────────────────


class TestDispatchDryRunSummary:
    """_async_confirmation_summary with dry_run enabled."""

    @pytest.fixture
    def config_manager(self):
        cm = MagicMock()
        env_dev = _make_env("env-dev", "Development")
        env_staging = _make_env("env-staging", "Staging")
        cm.get_environments = MagicMock(return_value=[env_dev, env_staging])
        return cm

    @pytest.mark.asyncio
    async def test_migration_with_dry_run_success(self, config_manager):
        """Execute migration with dry_run=True triggers dry-run report."""
        from src.api.routes.assistant._dispatch import _async_confirmation_summary

        db = MagicMock()
        intent = {
            "operation": "execute_migration",
            "entities": {
                "dashboard_id": "42",
                "source_env": "dev",
                "target_env": "staging",
                "replace_db_config": True,
                "fix_cross_filters": True,
                "dry_run": True,
            },
        }

        with patch("src.api.routes.assistant._dispatch._resolve_dashboard_id_entity",
                   new=AsyncMock(return_value=42)), \
             patch("src.api.routes.assistant._dispatch._resolve_env_id") as mock_resolve_env, \
             patch("src.core.migration.dry_run_orchestrator.MigrationDryRunService") as mock_dry_cls, \
             patch("src.core.async_superset_client.AsyncSupersetClient"):

            mock_resolve_env.side_effect = lambda t, cm: {"dev": "env-dev", "staging": "env-staging", "prod": "env-prod"}.get(t)

            mock_dry = MagicMock()
            mock_dry_cls.return_value = mock_dry
            mock_dry.run.return_value = {
                "summary": {
                    "dashboards": {"create": 1, "update": 2, "delete": 0},
                    "charts": {"create": 3, "update": 4, "delete": 1},
                    "datasets": {"create": 0, "update": 0, "delete": 0},
                }
            }

            text = await _async_confirmation_summary(intent, config_manager, db)
            assert "dry-run" in text.lower() or "dry" in text.lower()
            assert "создано" in text

    @pytest.mark.asyncio
    async def test_migration_dry_run_missing_env_mismatch(self, config_manager):
        """Dry-run report gracefully handles environment mismatch."""
        from src.api.routes.assistant._dispatch import _async_confirmation_summary

        db = MagicMock()
        intent = {
            "operation": "execute_migration",
            "entities": {
                "dashboard_id": "42",
                "source_env": "prod",
                "target_env": "prod",
                "dry_run": True,
            },
        }

        with patch("src.api.routes.assistant._dispatch._resolve_dashboard_id_entity",
                   new=AsyncMock(return_value=None)):
            text = await _async_confirmation_summary(intent, config_manager, db)
            assert "Подтвердите" in text or "Выполнить" in text

    @pytest.mark.asyncio
    async def test_migration_dry_run_exception_handling(self, config_manager):
        """Dry-run exception is caught gracefully."""
        from src.api.routes.assistant._dispatch import _async_confirmation_summary

        db = MagicMock()
        intent = {
            "operation": "execute_migration",
            "entities": {
                "dashboard_id": "42",
                "source_env": "dev",
                "target_env": "staging",
                "dry_run": True,
            },
        }

        with patch("src.api.routes.assistant._dispatch._resolve_dashboard_id_entity",
                   new=AsyncMock(side_effect=Exception("Unexpected error"))):
            text = await _async_confirmation_summary(intent, config_manager, db)
            assert "Подтвердите" in text or "Выполнить" in text


# ── _resolvers.py: edge cases ────────────────────────────────────


class TestAssistantResolversExtended:
    """Edge cases for resolver functions."""

    def test_is_production_env_no_env_found(self):
        """_is_production_env returns False when token doesn't match any env."""
        from src.api.routes.assistant._resolvers import _is_production_env

        config_manager = MagicMock()
        env1 = _make_env("env-dev", "Development")
        config_manager.get_environments = MagicMock(return_value=[env1])

        # Token matches env id but env name doesn't contain "prod"
        assert _is_production_env("env-dev", config_manager) is False

    def test_resolve_provider_id_with_bound_provider(self):
        """_resolve_provider_id uses resolve_bound_provider_id when config_manager and task_key given."""
        from src.api.routes.assistant._resolvers import _resolve_provider_id

        db = MagicMock()
        config_manager = MagicMock()
        p1 = MagicMock(id="p1", name="OpenAI", is_active=True)

        with patch("src.api.routes.assistant._resolvers.LLMProviderService") as mock_svc_cls:
            svc = MagicMock()
            mock_svc_cls.return_value = svc
            svc.get_all_providers.return_value = [p1]

            with patch("src.api.routes.assistant._resolvers.resolve_bound_provider_id",
                       return_value="p1"):
                result = _resolve_provider_id(None, db, config_manager, "validation")
                assert result == "p1"

    def test_resolve_provider_id_bound_provider_not_found(self):
        """_resolve_provider_id falls back to active when bound provider not found."""
        from src.api.routes.assistant._resolvers import _resolve_provider_id

        db = MagicMock()
        config_manager = MagicMock()
        p1 = MagicMock(id="p1", name="OpenAI", is_active=True)

        with patch("src.api.routes.assistant._resolvers.LLMProviderService") as mock_svc_cls:
            svc = MagicMock()
            mock_svc_cls.return_value = svc
            svc.get_all_providers.return_value = [p1]

            with patch("src.api.routes.assistant._resolvers.resolve_bound_provider_id",
                       return_value="nonexistent"):
                result = _resolve_provider_id(None, db, config_manager, "validation")
                assert result == "p1"

    def test_resolve_provider_id_bound_provider_exception(self):
        """_resolve_provider_id handles exception from resolve_bound_provider_id."""
        from src.api.routes.assistant._resolvers import _resolve_provider_id

        db = MagicMock()
        config_manager = MagicMock()
        p1 = MagicMock(id="p1", name="OpenAI", is_active=True)

        with patch("src.api.routes.assistant._resolvers.LLMProviderService") as mock_svc_cls:
            svc = MagicMock()
            mock_svc_cls.return_value = svc
            svc.get_all_providers.return_value = [p1]

            with patch("src.api.routes.assistant._resolvers.resolve_bound_provider_id",
                       side_effect=Exception("Config error")):
                result = _resolve_provider_id(None, db, config_manager, "validation")
                assert result == "p1"

    def test_get_default_environment_id_error_in_get_config(self):
        """_get_default_environment_id handles get_config exception."""
        from src.api.routes.assistant._resolvers import _get_default_environment_id

        config_manager = MagicMock()
        env1 = _make_env("env-dev", "Dev", is_default=True)
        config_manager.get_environments = MagicMock(return_value=[env1])
        config_manager.get_config.side_effect = Exception("Config error")

        result = _get_default_environment_id(config_manager)
        assert result == "env-dev"  # falls back to explicit default

    def test_get_default_environment_id_no_explicit_default(self):
        """_get_default_environment_id falls back to first env."""
        from src.api.routes.assistant._resolvers import _get_default_environment_id

        config_manager = MagicMock()
        env1 = _make_env("env-dev", "Dev", is_default=False)
        env2 = _make_env("env-prod", "Prod", is_default=False)
        config_manager.get_environments = MagicMock(return_value=[env1, env2])

        result = _get_default_environment_id(config_manager)
        assert result == "env-dev"  # first in list

    @pytest.mark.asyncio
    async def test_resolve_dashboard_id_by_ref_with_exception(self):
        """_resolve_dashboard_id_by_ref handles SupersetClient exception."""
        from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref

        config_manager = MagicMock()
        env = _make_env("env-dev", "Development")
        config_manager.get_environments = MagicMock(return_value=[env])

        with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_client_cls:
            client = MagicMock()
            mock_client_cls.return_value = client
            client.get_dashboards = AsyncMock(side_effect=Exception("API error"))

            result = await _resolve_dashboard_id_by_ref("my-dash", "env-dev", config_manager)
            assert result is None

    @pytest.mark.asyncio
    async def test_resolve_dashboard_id_by_ref_partial_match(self):
        """_resolve_dashboard_id_by_ref returns single partial match."""
        from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref

        config_manager = MagicMock()
        env = _make_env("env-dev", "Development")
        config_manager.get_environments = MagicMock(return_value=[env])

        with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_client_cls:
            client = MagicMock()
            mock_client_cls.return_value = client
            client.get_dashboards = AsyncMock(return_value=(None, [
                {"id": 10, "dashboard_title": "COVID Dashboard", "slug": "covid"},
                {"id": 20, "dashboard_title": "Sales Dashboard", "slug": "sales"},
            ]))

            result = await _resolve_dashboard_id_by_ref("covid", "env-dev", config_manager)
            assert result == 10

    @pytest.mark.asyncio
    async def test_resolve_dashboard_id_by_ref_partial_match_by_title(self):
        """_resolve_dashboard_id_by_ref matches by title substring."""
        from src.api.routes.assistant._resolvers import _resolve_dashboard_id_by_ref

        config_manager = MagicMock()
        env = _make_env("env-dev", "Development")
        config_manager.get_environments = MagicMock(return_value=[env])

        with patch("src.api.routes.assistant._resolvers.SupersetClient") as mock_client_cls:
            client = MagicMock()
            mock_client_cls.return_value = client
            client.get_dashboards = AsyncMock(return_value=(None, [
                {"id": 1, "dashboard_title": "COVID Dashboard", "slug": "covid"},
                {"id": 2, "dashboard_title": "COVID Overview", "slug": "covid-overview"},
            ]))

            # Partial match with multiple results -> None
            result = await _resolve_dashboard_id_by_ref("covid", "env-dev", config_manager)
            assert result == 1  # exact slug match "covid"

    def test_extract_result_deep_links_migration_params_fallback(self):
        """Deep links fallback to params selected_ids when migrated list is empty."""
        from src.api.routes.assistant._resolvers import _extract_result_deep_links

        config_manager = MagicMock()
        env = _make_env("env-prod", "Production")
        config_manager.get_environments = MagicMock(return_value=[env])

        task = MagicMock(
            plugin_id="superset-migration",
            params={"selected_ids": [123], "target_env_id": "env-prod"},
            result={"migrated_dashboards": None},
        )
        actions = _extract_result_deep_links(task, config_manager)
        assert len(actions) >= 1

    def test_extract_result_deep_links_backup_params_fallback(self):
        """Deep links fallback to params dashboard_ids when result list is empty."""
        from src.api.routes.assistant._resolvers import _extract_result_deep_links

        config_manager = MagicMock()
        env = _make_env("env-dev", "Dev")
        config_manager.get_environments = MagicMock(return_value=[env])

        task = MagicMock(
            plugin_id="superset-backup",
            params={"dashboard_ids": [456], "environment_id": "env-dev"},
            result={"dashboards": None},
        )
        actions = _extract_result_deep_links(task, config_manager)
        assert len(actions) >= 1

    def test_extract_result_deep_links_backup_env_from_result(self):
        """Backup deep links resolves environment from result when not in params."""
        from src.api.routes.assistant._resolvers import _extract_result_deep_links

        config_manager = MagicMock()
        env = _make_env("env-dev", "Dev")
        config_manager.get_environments = MagicMock(return_value=[env])

        task = MagicMock(
            plugin_id="superset-backup",
            params={},
            result={"dashboards": [{"id": 456}], "environment": "dev"},
        )
        with patch("src.api.routes.assistant._resolvers._resolve_env_id", return_value="env-dev"):
            actions = _extract_result_deep_links(task, config_manager)
            assert len(actions) >= 1


# ── _tool_backup.py: dashboard_id resolution failure ────────────


class TestToolBackupEdge:
    """Edge cases for backup tool."""

    @pytest.mark.asyncio
    async def test_run_backup_dashboard_resolution_failure(self):
        """Backup with dashboard_ref that fails resolution raises 422."""
        from src.api.routes.assistant._tool_backup import handle_run_backup
        from src.schemas.auth import User

        user = User(id="user-1", username="test", email="test@x.com", is_active=True,
                    auth_source="internal", created_at=datetime.now(UTC),
                    last_login=datetime.now(UTC), roles=[])
        task_manager = MagicMock()
        config_manager = MagicMock()
        db = MagicMock()

        intent = {
            "entities": {
                "environment": "dev",
                "dashboard_ref": "nonexistent",
            }
        }

        with patch("src.api.routes.assistant._tool_backup._check_any_permission"), \
             patch("src.api.routes.assistant._tool_backup._resolve_env_id", return_value="env-dev"), \
             patch("src.api.routes.assistant._tool_backup._resolve_dashboard_id_entity",
                   new=AsyncMock(return_value=None)):

            with pytest.raises(HTTPException) as exc:
                await handle_run_backup(intent, user, task_manager, config_manager, db)
            assert exc.value.status_code == 422


# ── _tool_migration.py: dashboard_ref path ───────────────────────


class TestToolMigrationEdge:
    """Edge cases for migration tool."""

    @pytest.mark.asyncio
    async def test_execute_migration_with_dashboard_ref(self):
        """Migration with dashboard_ref uses regex path."""
        from src.api.routes.assistant._tool_migration import handle_execute_migration
        from src.schemas.auth import User

        user = User(id="user-1", username="test", email="test@x.com", is_active=True,
                    auth_source="internal", created_at=datetime.now(UTC),
                    last_login=datetime.now(UTC), roles=[])
        task_manager = MagicMock()
        task_manager.create_task = AsyncMock(return_value=MagicMock(id="task-55"))
        config_manager = MagicMock()
        db = MagicMock()

        intent = {
            "entities": {
                "source_env": "dev",
                "target_env": "staging",
                "dashboard_ref": "my-dashboard-slug",
            }
        }

        with patch("src.api.routes.assistant._tool_migration._check_any_permission"), \
             patch("src.api.routes.assistant._tool_migration._resolve_dashboard_id_entity",
                   new=AsyncMock(return_value=None)), \
             patch("src.api.routes.assistant._tool_migration._resolve_env_id",
                   side_effect=lambda t, cm: {"dev": "env-dev", "staging": "env-staging"}.get(t)):

            text, task_id, actions = await handle_execute_migration(
                intent, user, task_manager, config_manager, db
            )
            assert "Миграция запущена" in text
            assert task_id == "task-55"
            # Verify dashboard_regex was used
            call_kwargs = task_manager.create_task.call_args[1]
            assert "dashboard_regex" in call_kwargs["params"]

    @pytest.mark.asyncio
    async def test_execute_migration_missing_both_dashboard_refs(self):
        """Migration without dashboard_id AND dashboard_ref raises 422."""
        from src.api.routes.assistant._tool_migration import handle_execute_migration
        from src.schemas.auth import User

        user = User(id="user-1", username="test", email="test@x.com", is_active=True,
                    auth_source="internal", created_at=datetime.now(UTC),
                    last_login=datetime.now(UTC), roles=[])
        task_manager = MagicMock()
        config_manager = MagicMock()
        db = MagicMock()

        intent = {
            "entities": {
                "source_env": "dev",
                "target_env": "staging",
            }
        }

        with patch("src.api.routes.assistant._tool_migration._check_any_permission"), \
             patch("src.api.routes.assistant._tool_migration._resolve_env_id",
                   return_value="env-dev"):

            with pytest.raises(HTTPException) as exc:
                await handle_execute_migration(intent, user, task_manager, config_manager, db)
            assert exc.value.status_code == 422


# ── _tool_search_dashboards.py: no results and pagination ────────


class TestToolSearchDashboardsEdge:
    """Edge cases for search dashboards tool."""

    @pytest.mark.asyncio
    async def test_search_with_no_results(self):
        """Search with no matching dashboards returns 'not found' message."""
        from src.api.routes.assistant._tool_search_dashboards import handle_search_dashboards

        config_manager = MagicMock()
        env = _make_env("env-dev", "Development")
        config_manager.get_environments = MagicMock(return_value=[env])
        db = MagicMock()

        intent = {"entities": {"environment": "dev", "search": "nonexistent"}}

        with patch("src.api.routes.assistant._tool_search_dashboards._resolve_env_id",
                   return_value="env-dev"), \
             patch("src.api.routes.assistant._tool_search_dashboards.SupersetClient") as mock_cls:

            client = MagicMock()
            mock_cls.return_value = client
            client.get_dashboards_summary = AsyncMock(return_value=[
                {"id": 1, "title": "Dashboard One", "slug": "dash-one"},
            ])

            text, task_id, actions = await handle_search_dashboards(
                intent, MagicMock(id="user-1"), MagicMock(), config_manager, db
            )
            assert "не найдены" in text
            assert task_id is None

    @pytest.mark.asyncio
    async def test_search_empty_env_no_dashboards(self):
        """Environment with no dashboards at all."""
        from src.api.routes.assistant._tool_search_dashboards import handle_search_dashboards

        config_manager = MagicMock()
        env = _make_env("env-dev", "Development")
        config_manager.get_environments = MagicMock(return_value=[env])
        db = MagicMock()

        intent = {"entities": {"environment": "dev"}}

        with patch("src.api.routes.assistant._tool_search_dashboards._resolve_env_id",
                   return_value="env-dev"), \
             patch("src.api.routes.assistant._tool_search_dashboards.SupersetClient") as mock_cls:

            client = MagicMock()
            mock_cls.return_value = client
            client.get_dashboards_summary = AsyncMock(return_value=[])

            text, task_id, actions = await handle_search_dashboards(
                intent, MagicMock(id="user-1"), MagicMock(), config_manager, db
            )
            assert "нет дашбордов" in text

    @pytest.mark.asyncio
    async def test_search_with_pagination_tail(self):
        """Pagination '... and more' indicator."""
        from src.api.routes.assistant._tool_search_dashboards import handle_search_dashboards

        config_manager = MagicMock()
        env = _make_env("env-dev", "Development")
        config_manager.get_environments = MagicMock(return_value=[env])
        db = MagicMock()

        dashboards = [{"id": i, "title": f"Dashboard {i}", "slug": f"dash-{i}"} for i in range(1, 25)]

        intent = {"entities": {"environment": "dev", "page": 1, "page_size": 20}}

        with patch("src.api.routes.assistant._tool_search_dashboards._resolve_env_id",
                   return_value="env-dev"), \
             patch("src.api.routes.assistant._tool_search_dashboards.SupersetClient") as mock_cls:

            client = MagicMock()
            mock_cls.return_value = client
            client.get_dashboards_summary = AsyncMock(return_value=dashboards)

            text, task_id, actions = await handle_search_dashboards(
                intent, MagicMock(id="user-1"), MagicMock(), config_manager, db
            )
            assert "и ещё" in text


# ── _tool_registry.py: get_catalog filtered and defaults ─────────


class TestToolRegistryEdge:
    """Edge cases for tool registry."""

    def _clear_registry(self):
        import src.api.routes.assistant._tool_registry as reg
        reg._tools.clear()

    def test_get_catalog_filters_by_permission(self):
        """get_catalog skips tools user doesn't have permission for."""
        from src.api.routes.assistant._tool_registry import assistant_tool, get_catalog, _tools
        from src.schemas.auth import User

        self._clear_registry()

        @assistant_tool(operation="admin_op", domain="admin", description="Admin only",
                        permission_checks=[("admin", "ACCESS")])
        async def admin_handler(i, u, t, c, d): return ("", None, [])

        @assistant_tool(operation="public_op", domain="public", description="Public",
                        risk_level="safe")
        async def public_handler(i, u, t, c, d): return ("", None, [])

        user = User(id="u1", username="test", email="t@t.com", is_active=True,
                    auth_source="internal", created_at=datetime.now(UTC),
                    last_login=datetime.now(UTC), roles=[])

        with patch("src.api.routes.assistant._tool_registry._has_any_permission",
                   return_value=False):
            catalog = get_catalog(user, MagicMock(), MagicMock())
            ops = [e["operation"] for e in catalog]
            assert "admin_op" not in ops
            assert "public_op" in ops

    def test_get_catalog_with_defaults(self):
        """get_catalog includes defaults when tool has them."""
        from src.api.routes.assistant._tool_registry import assistant_tool, get_catalog, _tools

        self._clear_registry()

        @assistant_tool(operation="with_defaults", domain="t", description="t",
                        defaults={"environment": "dev"})
        async def h(i, u, t, c, d): return ("", None, [])

        catalog = get_catalog(MagicMock(), MagicMock(), MagicMock())
        entry = next(e for e in catalog if e["operation"] == "with_defaults")
        assert "defaults" in entry
        assert entry["defaults"]["environment"] == "dev"

    @pytest.mark.asyncio
    async def test_dispatch_dataset_review_ops(self):
        """dispatch routes dataset_review ops through sub-dispatcher."""
        from src.api.routes.assistant._tool_registry import dispatch

        with patch(
            "src.api.routes.assistant._dataset_review_dispatch._dispatch_dataset_review_intent",
            new=AsyncMock(return_value=("review result", None, [])),
        ) as mock_sub:
            text, tid, acts = await dispatch(
                "dataset_review_answer_context", {}, MagicMock(), MagicMock(), MagicMock(), MagicMock()
            )
            assert text == "review result"
            mock_sub.assert_awaited_once()


# ── _tool_task_status.py: completed task with deep links ─────────


class TestToolTaskStatusEdge:
    """Edge cases for task status tool."""

    @pytest.mark.asyncio
    async def test_get_task_status_by_id_with_deep_links(self):
        """Task status with SUCCESS status adds deep link actions."""
        from src.api.routes.assistant._tool_task_status import handle_get_task_status
        from src.api.routes.assistant._schemas import AssistantAction
        from src.schemas.auth import User

        user = User(id="user-1", username="test", email="test@x.com", is_active=True,
                    auth_source="internal", created_at=datetime.now(UTC),
                    last_login=datetime.now(UTC), roles=[])
        task_manager = MagicMock()
        config_manager = MagicMock()
        db = MagicMock()

        with patch("src.api.routes.assistant._tool_task_status._check_any_permission"), \
             patch("src.api.routes.assistant._tool_task_status._build_task_observability_summary",
                   return_value="Detailed summary"), \
             patch("src.api.routes.assistant._tool_task_status._extract_result_deep_links",
                   return_value=[
                       AssistantAction(type="open_route", label="Open Dashboard", target="/dashboards/42")
                   ]):

            task = MagicMock(id="task-42", status="SUCCESS", user_id="user-1")
            task_manager.get_task.return_value = task

            intent = {"entities": {"task_id": "task-42"}}
            text, task_id, actions = await handle_get_task_status(
                intent, user, task_manager, config_manager, db
            )
            assert "task-42" in text
            assert len(actions) > 1  # open_task + deep links
            assert task_id == "task-42"
# #endregion Test.Assistant.EdgeCases
