# #region TestAsyncRegression [C:3] [TYPE Module] [SEMANTICS test,async,regression,await]
# @BRIEF Regression tests for bugs found during sync→async migration.
#   Ensures that missing `await`, renamed methods, and event-loop issues
#   are caught at test time, not at runtime.
# @RELATION BINDS_TO -> [AsyncAPIClient]
# @RELATION BINDS_TO -> [SupersetClient]
# @RELATION BINDS_TO -> [AsyncSupersetClient]
# @TEST_CONTRACT Every async method call from a sync context raises TypeError
# @TEST_CONTRACT SupersetClient.aclose() exists and is awaitable
# @TEST_CONTRACT get_dashboards_page (not _async) is the canonical method name

import asyncio
import pytest
from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch

from src.core.superset_client import SupersetClient
from src.core.utils.async_network import AsyncAPIClient


# ── 1. SupersetClient.aclose() exists and is awaitable ────────────────────
# Regression: 'SupersetClient' object has no attribute 'aclose'
# Fixed in commit 079b2dd


class TestSupersetClientAclose:

    @pytest.mark.asyncio
    async def test_aclose_is_awaitable(self):
        """SupersetClient must have aclose() to match AsyncSupersetClient interface."""
        env = MagicMock()
        env.id = "test"
        env.url = "http://test.local"
        env.username = "u"
        env.password = "p"
        env.auth = {"username": "u", "password": "p"}
        env.timeout = 30
        env.verify_ssl = False

        client = SupersetClient(env)
        assert hasattr(client, "aclose"), "SupersetClient must have aclose()"
        assert asyncio.iscoroutinefunction(client.aclose), "aclose() must be async"

    @pytest.mark.asyncio
    async def test_aclose_delegates_to_async_client(self):
        """aclose() must delegate to AsyncAPIClient.aclose()."""
        from src.core.superset_client._base import SupersetClientBase

        env = MagicMock()
        env.id = "test"
        env.url = "http://test.local"
        env.username = "u"
        env.password = "p"
        env.auth = {"username": "u", "password": "p"}
        env.timeout = 30
        env.verify_ssl = False

        mock_transport = AsyncMock()
        mock_transport.aclose = AsyncMock()

        client = SupersetClientBase.__new__(SupersetClientBase)
        client.env = env
        client.client = mock_transport
        client.network = mock_transport

        await client.aclose()
        mock_transport.aclose.assert_awaited_once()


# ── 2. get_dashboards_page (not get_dashboards_page_async) ───────────────
# Regression: get_dashboards_page_async was called but only
# get_dashboards_page exists (it's async and renamed).
# Fixed in commit 7115678


class TestDashboardsPageMethod:

    def test_get_dashboards_page_is_async(self):
        """get_dashboards_page must be async (the canonical method name)."""
        from src.core.superset_client._dashboards_list import SupersetDashboardsListMixin

        assert hasattr(SupersetDashboardsListMixin, "get_dashboards_page")
        assert asyncio.iscoroutinefunction(
            SupersetDashboardsListMixin.get_dashboards_page
        ), "get_dashboards_page must be async"

    def test_get_dashboards_page_async_does_not_exist(self):
        """get_dashboards_page_async was removed — only get_dashboards_page exists."""
        from src.core.superset_client._dashboards_list import SupersetDashboardsListMixin

        assert not hasattr(
            SupersetDashboardsListMixin, "get_dashboards_page_async"
        ), "get_dashboards_page_async must not exist — renamed to get_dashboards_page"


# ── 3. Missing await detection — coroutine leak regression ───────────────
# Regression pattern: sync function calls async method without await,
# returns a coroutine that later fails with e.g.:
#   'coroutine' object is not iterable
#   'coroutine' object has no attribute 'get'
#   '>' not supported between instances of 'coroutine' and 'int'


class TestMissingAwaitDetection:

    def test_async_method_returns_coroutine_not_value(self):
        """Calling async def without await returns a coroutine, not the result."""
        async def async_add(a, b):
            return a + b

        # Calling without await → coroutine object, NOT the sum
        result = async_add(1, 2)
        assert asyncio.iscoroutine(result), (
            "Async function called without await must return a coroutine, "
            "not the computed value"
        )
        # The actual value is only obtained via await
        assert asyncio.run(result) == 3

    def test_coroutine_is_not_iterable(self):
        """Iterating over a coroutine raises TypeError (regression guard)."""
        async def async_list():
            return [1, 2, 3]

        coro = async_list()
        assert asyncio.iscoroutine(coro)
        with pytest.raises(TypeError, match="coroutine.*not iterable"):
            for item in coro:
                pass

    def test_coroutine_has_no_dict_attribute(self):
        """Calling .get() on a coroutine raises AttributeError (regression guard)."""
        async def async_dict():
            return {"key": "value"}

        coro = async_dict()
        assert asyncio.iscoroutine(coro)
        with pytest.raises(AttributeError, match="coroutine.*has no attribute"):
            _ = coro.get("key")

    def test_coroutine_cannot_be_compared_to_int(self):
        """Comparing a coroutine to int raises TypeError (regression guard)."""
        async def async_count():
            return 42

        coro = async_count()
        assert asyncio.iscoroutine(coro)
        with pytest.raises(TypeError, match="not supported between instances"):
            _ = coro > 0


# ── 4. Async route handler event loop regression ─────────────────────────
# Regression: run_translation was sync def but used asyncio.create_task
# inside, which requires a running event loop.
# Fixed in commit 71000db


class TestAsyncRouteHandlerEventLoop:

    @pytest.mark.asyncio
    async def test_async_route_handler_has_running_loop(self):
        """Async route handlers run inside a running event loop."""
        loop = asyncio.get_running_loop()
        assert loop is not None
        assert not loop.is_closed()

    @pytest.mark.asyncio
    async def test_create_task_works_in_async_context(self):
        """asyncio.create_task must work when called from an async function."""
        results = []

        async def background_work():
            await asyncio.sleep(0.001)
            results.append("done")

        task = asyncio.create_task(background_work())
        await task
        assert results == ["done"]

    def test_create_task_raises_in_sync_context(self):
        """asyncio.create_task WITHOUT running loop raises RuntimeError."""
        import threading

        # This must be tested in a thread with no event loop
        # (sync route handlers run in thread pool)
        errors = []

        def sync_function():
            try:
                async def dummy():
                    pass
                asyncio.create_task(dummy())
            except RuntimeError as e:
                errors.append(str(e))

        t = threading.Thread(target=sync_function)
        t.start()
        t.join()
        assert len(errors) > 0
        assert "no running event loop" in errors[0]


# ── 5. Sync function calling async method without await ──────────────────
# Regression: many sync functions called async SupersetClient methods.
# The pattern can be detected statically (see final scan) but we also
# verify that the runtime error is predictable.


class TestSyncCallsAsyncWithoutAwait:

    def test_awaiting_magicmock_raises_typeerror(self):
        """await on a regular MagicMock raises TypeError."""
        m = MagicMock()
        m.async_method = MagicMock(return_value=42)

        async def caller():
            return await m.async_method()

        with pytest.raises(TypeError, match="can't be used in 'await' expression"):
            asyncio.run(caller())

    @pytest.mark.asyncio
    async def test_asyncmock_await_returns_value(self):
        """await on an AsyncMock returns the configured return_value."""
        m = MagicMock()
        m.async_method = AsyncMock(return_value=42)

        result = await m.async_method()
        assert result == 42

    @pytest.mark.asyncio
    async def test_mock_for_async_method_must_be_asyncmock(self):
        """Tests that mock async methods must use AsyncMock, not MagicMock.

        If a test uses MagicMock.return_value for an async method, the
        production code will receive a coroutine instead of the expected value.
        This test guards against that pattern.
        """
        # ❌ WRONG: MagicMock with return_value — await fails
        wrong = MagicMock()
        wrong.async_method = MagicMock(return_value={"status": "ok"})
        with pytest.raises(TypeError):
            await wrong.async_method()

        # ✅ CORRECT: AsyncMock with return_value — await succeeds
        correct = MagicMock()
        correct.async_method = AsyncMock(return_value={"status": "ok"})
        result = await correct.async_method()
        assert result["status"] == "ok"


# ── 6. SupersetClient method name consistency ────────────────────────────
# All async methods on SupersetClient should exist and be awaitable.


class TestSupersetClientMethodConsistency:

    """Verify key SupersetClient async methods exist and are awaitable."""

    ASYNC_METHODS = [
        "get_dashboards_summary",
        "get_dashboards_page",
        "get_datasets_summary",
        "get_dataset_detail",
        "get_database",
        "get_databases",
        "get_dataset",
        "update_dataset",
        "get_dashboards",
        "get_charts",
        "get_chart",
        "get_dashboard_detail",
        "export_dashboard",
        "get_all_resources",
        "get_database_by_uuid",
        "get_dashboards_summary_page",
        "get_dataset_linked_dashboard_count",
        "aclose",
    ]

    def test_all_async_methods_exist_on_client(self):
        """All canonical async methods must exist on SupersetClient."""
        from src.core.superset_client import SupersetClient

        for method_name in self.ASYNC_METHODS:
            assert hasattr(SupersetClient, method_name), (
                f"SupersetClient must have method '{method_name}'"
            )

    def test_all_async_methods_are_coroutines(self):
        """All canonical async methods must be awaitable (async def)."""
        from src.core.superset_client import SupersetClient

        for method_name in self.ASYNC_METHODS:
            method = getattr(SupersetClient, method_name)
            assert asyncio.iscoroutinefunction(method), (
                f"SupersetClient.{method_name} must be async def"
            )

    def test_no_legacy_sync_names_remain(self):
        """Legacy _async suffixed names must not exist."""
        from src.core.superset_client import SupersetClient

        legacy_names = [
            "get_dashboards_page_async",
            "get_dashboards_summary_async",
            "get_dashboard_detail_async",
            "get_datasets_summary_async",
        ]
        for name in legacy_names:
            assert not hasattr(SupersetClient, name), (
                f"Legacy name '{name}' must not exist on SupersetClient"
            )


# ── 7. AsyncAPIClient semaphore / pool timeout regression ────────────────


class TestAsyncAPIClientSemaphore:

    @pytest.mark.asyncio
    async def test_semaphore_limits_concurrent_requests(self):
        """Semaphore should cap concurrent requests."""
        config = {"base_url": "http://test.local", "auth": {"username": "x", "password": "y"}}

        semaphore = asyncio.Semaphore(2)
        client = AsyncAPIClient(config=config, timeout=30, semaphore=semaphore)
        assert client._semaphore is semaphore
        await client.aclose()

    @pytest.mark.asyncio
    async def test_client_aclose_is_idempotent(self):
        """Calling aclose() multiple times should not raise."""
        config = {"base_url": "http://test.local", "auth": {"username": "x", "password": "y"}}
        client = AsyncAPIClient(config=config, timeout=30)

        # First close
        await client.aclose()
        # Second close — should be safe
        await client.aclose()


# ── 8. run_blocking executor regression ──────────────────────────────────


class TestRunBlocking:

    @pytest.mark.asyncio
    async def test_run_blocking_executes_sync_function(self):
        """run_blocking must execute a sync function and return its result."""
        from src.core.utils.executors import run_blocking

        def sync_add(a, b):
            return a + b

        result = await run_blocking("db", sync_add, 1, 2)
        assert result == 3

    @pytest.mark.asyncio
    async def test_run_blocking_raises_on_exception(self):
        """run_blocking must propagate exceptions from the sync function."""
        from src.core.utils.executors import run_blocking

        def sync_fail():
            raise ValueError("sync error")

        with pytest.raises(ValueError, match="sync error"):
            await run_blocking("file", sync_fail)



# ── 9. asyncio.run() in running event loop regression ───────────────────
# Regression: _test_postgresql(), _execute_pg(), _fetch_pg() used asyncio.run()
# which raises RuntimeError when called from within a running event loop
# (e.g., FastAPI async def endpoint). Fixed: all methods converted to
# async def; asyncio.run() removed.
# Files: src/core/connection_service.py, src/core/db_executor.py


class TestAsyncioRunInRunningLoop:
    """Verify that migrated async methods work when called from a running event loop
    (the scenario that previously crashed with 'asyncio.run() cannot be called from
    a running event loop')."""

    # ── 9a. Static structure: all migrated methods must be async def ──────────

    def test_connection_service_test_connection_is_async(self):
        """test_connection must be a coroutine function (regression: was sync)."""
        from src.core.connection_service import ConnectionService
        assert asyncio.iscoroutinefunction(
            ConnectionService.test_connection
        ), "test_connection must be async def — regression guard against asyncio.run()"

    def test_connection_service_test_postgresql_is_async(self):
        """_test_postgresql must be a coroutine function (regression: used asyncio.run())."""
        from src.core.connection_service import ConnectionService
        assert asyncio.iscoroutinefunction(
            ConnectionService._test_postgresql
        ), "_test_postgresql must be async def — regression guard against asyncio.run()"

    def test_db_executor_execute_sql_is_async(self):
        """execute_sql must be a coroutine function (regression: was sync)."""
        from src.core.db_executor import DbExecutor
        assert asyncio.iscoroutinefunction(
            DbExecutor.execute_sql
        ), "execute_sql must be async def — regression guard against asyncio.run()"

    def test_db_executor_fetch_schema_is_async(self):
        """fetch_schema must be a coroutine function (regression: was sync)."""
        from src.core.db_executor import DbExecutor
        assert asyncio.iscoroutinefunction(
            DbExecutor.fetch_schema
        ), "fetch_schema must be async def — regression guard against asyncio.run()"

    def test_db_executor_execute_pg_is_async(self):
        """_execute_pg must be a coroutine function (regression: used asyncio.run())."""
        from src.core.db_executor import DbExecutor
        assert asyncio.iscoroutinefunction(
            DbExecutor._execute_pg
        ), "_execute_pg must be async def — regression guard against asyncio.run()"

    def test_db_executor_fetch_pg_is_async(self):
        """_fetch_pg must be a coroutine function (regression: used asyncio.run())."""
        from src.core.db_executor import DbExecutor
        assert asyncio.iscoroutinefunction(
            DbExecutor._fetch_pg
        ), "_fetch_pg must be async def — regression guard against asyncio.run()"

    # ── 9b. Functional: calling from running event loop doesn't crash ────────

    @pytest.mark.asyncio
    async def test_test_postgresql_works_in_running_loop(self):
        """_test_postgresql must not crash with 'asyncio.run() cannot be called'
        when awaited from a running event loop. Tests the full asyncpg path."""
        import sys
        from src.core.config_models import DatabaseConnection
        from src.core.connection_service import ConnectionService

        cm = MagicMock()
        cm.config.settings.connections = []
        service = ConnectionService(cm)

        conn = DatabaseConnection(
            id="reg-asyncpg-1", name="Regression PG", host="localhost",
            port=5432, database="test", username="u", password="p",
            dialect="postgresql", pool_size=5,
        )

        # Mock asyncpg driver at EXT boundary
        mock_pg_conn = AsyncMock()
        mock_pg_conn.fetchrow = AsyncMock(return_value=["PostgreSQL 16.3"])

        mock_asyncpg = MagicMock()
        mock_asyncpg.connect = AsyncMock(return_value=mock_pg_conn)

        with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
            # Before fix: RuntimeError: asyncio.run() cannot be called from a running event loop
            result = await service._test_postgresql(conn)

        assert result == "PostgreSQL 16.3"
        mock_asyncpg.connect.assert_awaited_once_with(
            host="localhost", port=5432, user="u", password="p",
            database="test", timeout=10,
        )
        mock_pg_conn.fetchrow.assert_awaited_once_with("SELECT version()")
        mock_pg_conn.close.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_execute_pg_works_in_running_loop(self):
        """_execute_pg must not crash with 'asyncio.run() cannot be called'
        when awaited from a running event loop. Tests the full asyncpg pool path."""
        import sys
        from src.core.db_executor import DbExecutor

        conn = MagicMock()
        conn.id = "reg-execpg-1"
        conn.dialect = "postgresql"
        conn.host = "localhost"
        conn.port = 5432
        conn.database = "test"
        conn.username = "u"
        conn.password = "p"
        conn.pool_size = 5
        conn.updated_at = "2025-01-01"

        svc = MagicMock()
        svc.get_connection.return_value = conn
        executor = DbExecutor(svc)

        # Mock asyncpg pool at EXT boundary
        mock_pg_conn = AsyncMock()
        mock_pg_conn.execute = AsyncMock(return_value="INSERT 0 5")

        mock_pool_ctx = AsyncMock()
        mock_pool_ctx.__aenter__ = AsyncMock(return_value=mock_pg_conn)
        mock_pool_ctx.__aexit__ = AsyncMock(return_value=False)

        mock_pool = MagicMock()
        mock_pool.acquire.return_value = mock_pool_ctx

        mock_asyncpg = MagicMock()
        mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)

        with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
            # Before fix: RuntimeError: asyncio.run() cannot be called from a running event loop
            result = await executor._execute_pg(conn, "INSERT INTO t VALUES (1)", 0.0)

        assert result.success is True
        assert result.rows_affected == 5
        mock_asyncpg.create_pool.assert_awaited_once()
        mock_pg_conn.execute.assert_awaited_once_with("INSERT INTO t VALUES (1)")

    @pytest.mark.asyncio
    async def test_fetch_pg_works_in_running_loop(self):
        """_fetch_pg must not crash with 'asyncio.run() cannot be called'
        when awaited from a running event loop. Tests the full asyncpg pool path."""
        import sys
        from src.core.db_executor import DbExecutor, DbSchemaColumn

        conn = MagicMock()
        conn.id = "reg-fetchpg-1"
        conn.dialect = "postgresql"
        conn.host = "localhost"
        conn.port = 5432
        conn.database = "test"
        conn.username = "u"
        conn.password = "p"
        conn.pool_size = 5
        conn.updated_at = "2025-01-01"

        svc = MagicMock()
        svc.get_connection.return_value = conn
        executor = DbExecutor(svc)

        # Mock asyncpg pool at EXT boundary
        fake_rows = [
            {"name": "id", "type": "integer"},
            {"name": "email", "type": "varchar"},
        ]
        mock_pg_conn = AsyncMock()
        mock_pg_conn.fetch = AsyncMock(return_value=fake_rows)

        mock_pool_ctx = AsyncMock()
        mock_pool_ctx.__aenter__ = AsyncMock(return_value=mock_pg_conn)
        mock_pool_ctx.__aexit__ = AsyncMock(return_value=False)

        mock_pool = MagicMock()
        mock_pool.acquire.return_value = mock_pool_ctx

        mock_asyncpg = MagicMock()
        mock_asyncpg.create_pool = AsyncMock(return_value=mock_pool)

        with patch.dict(sys.modules, {"asyncpg": mock_asyncpg}):
            # Before fix: RuntimeError: asyncio.run() cannot be called from a running event loop
            result = await executor._fetch_pg(
                conn, "SELECT column_name, data_type FROM information_schema.columns"
            )

        assert result is not None
        assert len(result) == 2
        assert result[0].name == "id"
        assert result[0].data_type == "integer"
        assert result[1].name == "email"
        assert result[1].data_type == "varchar"
        mock_asyncpg.create_pool.assert_awaited_once()
        mock_pg_conn.fetch.assert_awaited_once()

    @pytest.mark.asyncio
    async def test_connection_service_test_connection_routes_to_async_pg(self):
        """test_connection must route to async _test_postgresql when dialect=postgresql
        and work from a running event loop."""
        from src.core.config_models import DatabaseConnection
        from src.core.connection_service import ConnectionService

        cm = MagicMock()
        cm.config.settings.connections = []
        service = ConnectionService(cm)

        conn = DatabaseConnection(
            id="reg-route-1", name="PG Route", host="localhost",
            port=5432, database="test", username="u", password="p",
            dialect="postgresql", pool_size=5,
        )
        cm.config.settings.connections.append(conn)

        # Mock decryption to pass through
        with patch.object(service, '_decrypt_password', return_value="p"), \
             patch.object(service, '_test_postgresql', return_value="PostgreSQL 17"):

            result = await service.test_connection("reg-route-1")

        assert result["success"] is True
        assert result["db_version"] == "PostgreSQL 17"
        assert "latency_ms" in result

    @pytest.mark.asyncio
    async def test_execute_sql_pg_routes_to_async_execute_pg(self):
        """execute_sql must route to async _execute_pg when dialect=postgresql
        and work from a running event loop."""
        from unittest.mock import AsyncMock
        from src.core.db_executor import DbExecutor, DbExecutionResult

        conn = MagicMock()
        conn.id = "reg-route-pg-1"
        conn.dialect = "postgresql"
        conn.host = "localhost"
        conn.port = 5432
        conn.database = "test"
        conn.username = "u"
        conn.password = "p"
        conn.pool_size = 5
        conn.updated_at = "2025-01-01"

        svc = MagicMock()
        svc.get_connection.return_value = conn
        executor = DbExecutor(svc)

        expected = DbExecutionResult(success=True, rows_affected=10, execution_time_ms=50)
        mock_pg = AsyncMock(return_value=expected)
        with patch.object(executor, '_execute_pg', mock_pg):
            result = await executor.execute_sql("reg-route-pg-1", "INSERT INTO t VALUES (1)")

        assert result.success is True
        assert result.rows_affected == 10

    # ── 9c. Rejected path: asyncio.run() on coroutine raises TypeError ───────
    # Ensures that if someone mistakenly calls execute_sql() without await,
    # the error is immediate and informative.

    @pytest.mark.asyncio
    async def test_async_method_without_await_returns_coroutine(self):
        """Calling execute_sql() without await returns a coroutine, not a result.
        This guards the contract: the method IS async and must be awaited."""
        from src.core.db_executor import DbExecutor

        conn = MagicMock()
        conn.id = "reg-coro-1"
        conn.dialect = "clickhouse"
        conn.updated_at = "2025-01-01"
        svc = MagicMock()
        svc.get_connection.return_value = conn
        executor = DbExecutor(svc)

        # Call without await → should return coroutine, NOT DbExecutionResult
        coro = executor.execute_sql("reg-coro-1", "SELECT 1")
        assert asyncio.iscoroutine(coro), (
            "execute_sql() called without await must return a coroutine, "
            "not a result — guards the contract that it's async def"
        )


# #endregion TestAsyncRegression
