# #region Test.SupersetSqlLabExecutor [C:3] [TYPE Module] [SEMANTICS test, superset, sqllab, executor, poll]
# @BRIEF Verify SupersetSqlLabExecutor contracts — _get_client, resolve_database_id, execute_sql,
#         poll_execution_status, execute_and_poll, get_query_results.
# @RELATION BINDS_TO -> [SupersetSqlLabExecutor]
# @TEST_EDGE: env_not_found -> Raises ValueError
# @TEST_EDGE: target_database_id_resolution -> Resolves via target_database_id
# @TEST_EDGE: database_name_resolution -> Resolves by database_name
# @TEST_EDGE: databases_empty -> Raises ValueError
# @TEST_EDGE: database_name_not_found -> Raises ValueError
# @TEST_EDGE: execute_sql_no_database_id -> Falls back to resolve_database_id
# @TEST_EDGE: execute_sql_missing_query_id -> Returns result with None query_id
# @TEST_EDGE: polling_success -> Returns success status
# @TEST_EDGE: polling_failed -> Returns failed status
# @TEST_EDGE: polling_timeout -> Returns timeout status
# @TEST_EDGE: polling_error_retry -> Retries on network error
# @TEST_EDGE: execute_and_poll_sync_success -> Returns directly from sync mode
# @TEST_EDGE: execute_and_poll_no_query_id -> Returns failed
# @TEST_EDGE: get_query_results_success -> Returns results
# @TEST_EDGE: get_query_results_failure -> Returns error
import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from src.core.config_manager import ConfigManager
from src.plugins.translate.superset_executor import SupersetSqlLabExecutor


def _make_config_manager(environments=None):
    """Create a mock ConfigManager with the given environments."""
    config = MagicMock(spec=ConfigManager)
    if environments is not None:
        config.get_environments.return_value = environments
        for env in environments:
            config.get_environment.return_value = env
    else:
        config.get_environments.return_value = []
        config.get_environment.return_value = None
    return config


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


class TestInit:
    """Verify initialization."""

    def test_init(self):
        config = _make_config_manager()
        executor = SupersetSqlLabExecutor(config, "env-1")
        assert executor.config_manager is config
        assert executor.env_id == "env-1"
        assert executor._client is None
        assert executor._database_id is None


class TestGetClient:
    """Verify _get_client."""

    @pytest.mark.asyncio
    async def test_lazy_initialization(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        with patch('src.plugins.translate.superset_executor.get_superset_client',
                   new=AsyncMock(return_value=MagicMock())) as mock_get:
            client = await executor._get_client()
            assert client is not None
            assert executor._client is not None
            # Second call returns cached client
            client2 = await executor._get_client()
            assert client2 is client
            assert mock_get.call_count == 1

    @pytest.mark.asyncio
    async def test_env_not_found(self):
        config = _make_config_manager([])  # no environments
        executor = SupersetSqlLabExecutor(config, "env-1")

        with pytest.raises(ValueError, match="not found"):
            await executor._get_client()

    @pytest.mark.asyncio
    async def test_env_not_found_empty_list(self):
        config = _make_config_manager()
        config.get_environments.return_value = []
        executor = SupersetSqlLabExecutor(config, "nonexistent")
        with pytest.raises(ValueError, match="not found"):
            await executor._get_client()


class TestResolveDatabaseId:
    """Verify resolve_database_id."""

    @pytest.mark.asyncio
    async def test_resolve_by_target_database_id(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        mock_client = AsyncMock()
        mock_client.get_database.return_value = {
            "result": {"id": 42, "backend": "postgresql", "database_name": "Analytics DB"},
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            db_id = await executor.resolve_database_id(target_database_id="42")
            assert db_id == 42
            assert executor._database_backend == "postgresql"
            assert executor._database_name == "Analytics DB"

    @pytest.mark.asyncio
    async def test_resolve_by_target_database_id_fallback(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        mock_client = AsyncMock()
        # get_database raises error, fallback to database list
        mock_client.get_database.side_effect = ValueError("API error")
        mock_client.get_databases.return_value = (None, [
            {"id": 1, "database_name": "Main DB", "backend": "postgresql"},
        ])

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            db_id = await executor.resolve_database_id(target_database_id="99")
            assert db_id == 1
            assert executor._database_backend == "postgresql"

    @pytest.mark.asyncio
    async def test_resolve_by_database_name(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (None, [
            {"id": 1, "database_name": "Main DB", "backend": "postgresql"},
            {"id": 2, "database_name": "Analytics DB", "backend": "clickhouse"},
        ])

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            db_id = await executor.resolve_database_id(database_name="Analytics DB")
            assert db_id == 2
            assert executor._database_backend == "clickhouse"

    @pytest.mark.asyncio
    async def test_resolve_by_database_name_case_insensitive(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (None, [
            {"id": 1, "database_name": "Main DB", "backend": "postgresql"},
        ])

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            db_id = await executor.resolve_database_id(database_name="main db")
            assert db_id == 1

    @pytest.mark.asyncio
    async def test_resolve_database_name_not_found(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (None, [
            {"id": 1, "database_name": "Main DB", "backend": "postgresql"},
        ])

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            with pytest.raises(ValueError, match="not found"):
                await executor.resolve_database_id(database_name="Nonexistent DB")

    @pytest.mark.asyncio
    async def test_resolve_default_first_database(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (None, [
            {"id": 42, "database_name": "Default DB", "backend": "mysql"},
        ])

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            db_id = await executor.resolve_database_id()
            assert db_id == 42
            assert executor._database_backend == "mysql"

    @pytest.mark.asyncio
    async def test_resolve_no_databases_raises(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        mock_client = AsyncMock()
        mock_client.get_databases.return_value = (None, [])

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            with pytest.raises(ValueError, match="No databases found"):
                await executor.resolve_database_id()


class TestGetDatabaseBackend:
    """Verify get_database_backend."""

    def test_cached_value(self):
        config = _make_config_manager()
        executor = SupersetSqlLabExecutor(config, "env-1")
        assert executor.get_database_backend() is None
        executor._database_backend = "postgresql"
        assert executor.get_database_backend() == "postgresql"


class TestGetDatabaseName:
    """Verify get_database_name."""

    def test_cached_value(self):
        config = _make_config_manager()
        executor = SupersetSqlLabExecutor(config, "env-1")
        assert executor.get_database_name() is None
        executor._database_name = "Main DB"
        assert executor.get_database_name() == "Main DB"


class TestExecuteSql:
    """Verify execute_sql."""

    @pytest.mark.asyncio
    async def test_execute_with_database_id(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        executor._database_id = 42

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {
            "query_id": "q-123",
            "status": "success",
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.execute_sql("SELECT 1", database_id=42)
            assert result["query_id"] == "q-123"
            assert result["status"] == "success"
            assert result["database_id"] == 42

    @pytest.mark.asyncio
    async def test_execute_falls_back_to_resolve(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {
            "query_id": "q-456",
            "status": "success",
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            with patch.object(executor, 'resolve_database_id',
                              new=AsyncMock(return_value=99)):
                result = await executor.execute_sql("SELECT 1")
                assert result["query_id"] == "q-456"
                assert result["database_id"] == 99

    @pytest.mark.asyncio
    async def test_execute_api_error(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        executor._database_id = 42

        mock_client = AsyncMock()
        mock_client.network.request.side_effect = ValueError("Connection refused")

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            with pytest.raises(ValueError, match="Superset SQL Lab execute failed"):
                await executor.execute_sql("SELECT 1")

    @pytest.mark.asyncio
    async def test_execute_missing_query_id(self):
        """Response missing query_id — still returns result with None query_id."""
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        executor._database_id = 42

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {"status": "pending"}

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.execute_sql("SELECT 1")
            assert result["query_id"] is None
            assert result["status"] == "pending"

    @pytest.mark.asyncio
    async def test_execute_query_id_in_result(self):
        """Query ID nested in result block."""
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        executor._database_id = 42

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {
            "result": {"query_id": "q-nested"},
            "status": "success",
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.execute_sql("SELECT 1")
            assert result["query_id"] == "q-nested"

    @pytest.mark.asyncio
    async def test_execute_query_id_in_query_block(self):
        """Query ID nested in query block."""
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")
        executor._database_id = 42

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {
            "query": {"id": "q-from-query"},
            "status": "pending",
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.execute_sql("SELECT 1")
            assert result["query_id"] == "q-from-query"


class TestPollExecutionStatus:
    """Verify poll_execution_status."""

    @pytest.mark.asyncio
    async def test_poll_success(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {
            "result": {"status": "success", "state": "success", "rows": 10},
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.poll_execution_status("q-1", max_polls=1)
            assert result["status"] == "success"
            assert result["rows_affected"] == 10

    @pytest.mark.asyncio
    async def test_poll_failed(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {
            "result": {"status": "failed", "state": "failed", "error_message": "Query error"},
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.poll_execution_status("q-1", max_polls=1)
            assert result["status"] == "failed"
            assert "Query error" in result["error_message"]

    @pytest.mark.asyncio
    async def test_poll_timeout(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        # Always return pending
        mock_client.network.request.return_value = {
            "result": {"status": "pending", "state": "pending"},
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.poll_execution_status("q-1", max_polls=1, poll_interval_seconds=0.01)
            assert result["status"] == "timeout"

    @pytest.mark.asyncio
    async def test_poll_network_error_retry(self):
        """Network error should be retried."""
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        # First call fails, second succeeds
        mock_client.network.request = AsyncMock(side_effect=[
            ValueError("Network error"),
            {"result": {"status": "success", "state": "success", "rows": 5}},
        ])

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.poll_execution_status("q-1", max_polls=5, poll_interval_seconds=0.01)
            assert result["status"] == "success"

    @pytest.mark.asyncio
    async def test_poll_unknown_state_continues(self):
        """Unknown state should continue polling."""
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request = AsyncMock(side_effect=[
            {"result": {"status": "weird_state", "state": "weird_state"}},
            {"result": {"status": "success", "state": "success", "rows": 3}},
        ])

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.poll_execution_status("q-1", max_polls=5, poll_interval_seconds=0.01)
            assert result["status"] == "success"

    @pytest.mark.asyncio
    async def test_poll_uses_raw_result_when_no_result_key(self):
        """When response has no 'result' key, use raw dict."""
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {
            "status": "success",
            "state": "success",
            "rows": 7,
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.poll_execution_status("q-1", max_polls=1)
            assert result["status"] == "success"

    @pytest.mark.asyncio
    async def test_poll_not_dict_response(self):
        """When response is not a dict, use empty dict."""
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request.return_value = "not a dict"

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.poll_execution_status("q-1", max_polls=5, poll_interval_seconds=0.01)
            assert result["status"] == "timeout"


class TestExecuteAndPoll:
    """Verify execute_and_poll."""

    @pytest.mark.asyncio
    async def test_sync_mode_success(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        with patch.object(executor, 'execute_sql',
                          new=AsyncMock(return_value={
                              "query_id": "q-1",
                              "status": "success",
                              "raw_response": {
                                  "query": {"rows": 5, "endDttm": "2024-01-01"},
                                  "data": [{"col": "val"}],
                              },
                          })):
            result = await executor.execute_and_poll("SELECT 1")
            assert result["status"] == "success"
            assert result["rows_affected"] == 5

    @pytest.mark.asyncio
    async def test_async_mode_poll(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        with patch.object(executor, 'execute_sql',
                          new=AsyncMock(return_value={
                              "query_id": "q-1",
                              "status": "pending",
                              "raw_response": {},
                          })):
            with patch.object(executor, 'poll_execution_status',
                              new=AsyncMock(return_value={
                                  "query_id": "q-1",
                                  "status": "success",
                                  "state": "success",
                              })):
                result = await executor.execute_and_poll("SELECT 1")
                assert result["status"] == "success"

    @pytest.mark.asyncio
    async def test_no_query_id_returns_failed(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        with patch.object(executor, 'execute_sql',
                          new=AsyncMock(return_value={
                              "query_id": None,
                              "status": "pending",
                              "raw_response": {},
                          })):
            result = await executor.execute_and_poll("SELECT 1")
            assert result["status"] == "failed"
            assert "No query_id" in result["error_message"]


class TestGetQueryResults:
    """Verify get_query_results."""

    @pytest.mark.asyncio
    async def test_success(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {
            "results": [{"col": "val"}],
        }

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.get_query_results("q-1")
            assert result["status"] == "success"
            assert result["results"] == [{"col": "val"}]

    @pytest.mark.asyncio
    async def test_no_results(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request.return_value = {}

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.get_query_results("q-1")
            assert result["status"] == "no_results"

    @pytest.mark.asyncio
    async def test_api_error(self):
        env = _make_env()
        config = _make_config_manager([env])
        executor = SupersetSqlLabExecutor(config, "env-1")

        mock_client = AsyncMock()
        mock_client.network.request.side_effect = ValueError("Not found")

        with patch.object(executor, '_get_client', new=AsyncMock(return_value=mock_client)):
            result = await executor.get_query_results("q-1")
            assert result["status"] == "failed"
            assert "Not found" in result["error_message"]
# #endregion Test.SupersetSqlLabExecutor
