# #region Test.SupersetCompilationAdapter [C:4] [TYPE Module] [SEMANTICS test, superset, compilation, adapter, preview]
# @BRIEF Tests for core/utils/superset_compilation_adapter.py — SupersetCompilationAdapter.
# @RELATION BINDS_TO -> [SupersetCompilationAdapter]

from pathlib import Path
import sys

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

from datetime import UTC, datetime
from unittest.mock import AsyncMock, MagicMock, patch

import pytest


@pytest.fixture(autouse=True)
def patch_env_and_models():
    """Prevent Environment/CompiledPreview import errors by patching."""
    with patch("src.core.utils.superset_compilation_adapter.Environment") as mock_env, \
         patch("src.core.utils.superset_compilation_adapter.CompiledPreview") as mock_cp, \
         patch("src.core.utils.superset_compilation_adapter.PreviewStatus") as mock_ps:
        mock_env_instance = MagicMock()
        mock_env_instance.id = "test-env"
        mock_env.return_value = mock_env_instance

        # Make CompiledPreview accept kwargs
        def cp_side_effect(**kwargs):
            obj = MagicMock()
            for k, v in kwargs.items():
                setattr(obj, k, v)
            return obj
        mock_cp.side_effect = cp_side_effect

        mock_ps.READY = "READY"
        mock_ps.FAILED = "FAILED"
        mock_ps.STALE = "STALE"

        yield


@pytest.fixture
def payload():
    from src.core.utils.superset_compilation_adapter import (
        PreviewCompilationPayload,
        SqlLabLaunchPayload,
    )

    return {
        "preview": PreviewCompilationPayload(
            session_id="sess-1",
            dataset_id=1,
            preview_fingerprint="fp123",
            template_params={"schema": "public"},
            effective_filters=[{"col": "status", "val": "active"}],
        ),
        "sqllab": SqlLabLaunchPayload(
            session_id="sess-1",
            dataset_id=1,
            preview_id="prev-1",
            compiled_sql="SELECT * FROM users",
            template_params={"schema": "public"},
        ),
    }


class TestSupersetCompilationAdapterInit:
    """__init__: binds environment and optional client."""

    def test_init_without_client(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )
        from unittest.mock import MagicMock

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        assert adapter.environment is env

    def test_init_with_client(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env, client=client)
        assert adapter.client is client


class TestCompilePreview:
    """compile_preview: compiles preview through _request_superset_preview."""

    @pytest.mark.asyncio
    async def test_successful_compile(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        adapter._request_superset_preview = AsyncMock(
            return_value={"compiled_sql": "SELECT * FROM users"}
        )

        preview = await adapter.compile_preview(payload["preview"])
        assert preview.preview_status == "READY"
        assert preview.compiled_sql == "SELECT * FROM users"
        assert preview.compiled_by == "superset"

    @pytest.mark.asyncio
    async def test_invalid_dataset_id_raises(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )
        from src.core.utils.superset_compilation_adapter import (
            PreviewCompilationPayload,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        bad_payload = PreviewCompilationPayload(
            session_id="sess-1",
            dataset_id=0,
            preview_fingerprint="fp",
            template_params={},
            effective_filters=[],
        )

        with pytest.raises(ValueError, match="dataset_id must be a positive integer"):
            await adapter.compile_preview(bad_payload)

    @pytest.mark.asyncio
    async def test_request_exception_returns_failed_preview(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        adapter._request_superset_preview = AsyncMock(
            side_effect=RuntimeError("Upstream failed")
        )

        preview = await adapter.compile_preview(payload["preview"])
        assert preview.preview_status == "FAILED"
        assert preview.error_code == "superset_preview_failed"
        assert "Upstream failed" in preview.error_details

    @pytest.mark.asyncio
    async def test_empty_compiled_sql_returns_failed_preview(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        adapter._request_superset_preview = AsyncMock(return_value={"compiled_sql": ""})

        preview = await adapter.compile_preview(payload["preview"])
        assert preview.preview_status == "FAILED"
        assert preview.error_code == "superset_preview_empty"

    @pytest.mark.asyncio
    async def test_missing_compiled_sql_key(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        adapter._request_superset_preview = AsyncMock(return_value={"other": "data"})

        preview = await adapter.compile_preview(payload["preview"])
        assert preview.preview_status == "FAILED"
        assert preview.error_code == "superset_preview_empty"


class TestMarkPreviewStale:
    """mark_preview_stale: sets status to STALE."""

    def test_marks_as_stale(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        mock_preview = MagicMock()
        mock_preview.preview_status = "READY"

        result = adapter.mark_preview_stale(mock_preview)
        assert result.preview_status == "STALE"


class TestCreateSqlLabSession:
    """create_sql_lab_session: creates audited execution session."""

    @pytest.mark.asyncio
    async def test_successful_creation(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        adapter._request_sql_lab_session = AsyncMock(
            return_value={"sql_lab_session_ref": "query-42"}
        )

        ref = await adapter.create_sql_lab_session(payload["sqllab"])
        assert ref == "query-42"

    @pytest.mark.asyncio
    async def test_empty_compiled_sql_raises(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
            SqlLabLaunchPayload,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        bad_payload = SqlLabLaunchPayload(
            session_id="sess-1",
            dataset_id=1,
            preview_id="prev-1",
            compiled_sql="",
            template_params={},
        )

        with pytest.raises(ValueError, match="compiled_sql must be non-empty"):
            await adapter.create_sql_lab_session(bad_payload)

    @pytest.mark.asyncio
    async def test_missing_session_ref_raises(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        adapter._request_sql_lab_session = AsyncMock(return_value={})

        with pytest.raises(RuntimeError, match="did not include a session reference"):
            await adapter.create_sql_lab_session(payload["sqllab"])

    @pytest.mark.asyncio
    async def test_session_ref_in_result_id(self, payload):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        adapter._request_sql_lab_session = AsyncMock(
            return_value={"result": {"id": "query-99"}}
        )

        ref = await adapter.create_sql_lab_session(payload["sqllab"])
        assert ref == "query-99"


class TestRequestSupersetPreview:
    """_request_superset_preview: client method resolution."""

    @pytest.mark.asyncio
    async def test_uses_direct_compile_preview_method(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        client.compile_preview = AsyncMock(return_value={"compiled_sql": "SELECT 1"})

        adapter = SupersetCompilationAdapter(environment=env, client=client)
        adapter._supports_client_method = MagicMock(return_value=True)
        adapter._normalize_preview_response = MagicMock(
            return_value={"compiled_sql": "SELECT 1"}
        )

        payload = MagicMock()
        payload.dataset_id = 1
        payload.session_id = "sess-1"
        payload.template_params = {}
        payload.effective_filters = []

        result = await adapter._request_superset_preview(payload)
        assert result["compiled_sql"] == "SELECT 1"

    @pytest.mark.asyncio
    async def test_compile_preview_type_error_fallback(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        # compile_preview raises TypeError first time (wrong signature),
        # succeeds on second call with re-arranged args.
        # The source code catches TypeError, retries with positional args,
        # but due to try/except/else semantics the result from the retry
        # is NOT returned — it falls through to compile_dataset_preview.
        # So we must mock compile_dataset_preview too.
        client.compile_preview = AsyncMock(
            side_effect=[TypeError("wrong args"), {"compiled_sql": "SELECT 2"}]
        )
        client.compile_dataset_preview = AsyncMock(
            return_value={"compiled_sql": "SELECT 2"}
        )

        adapter = SupersetCompilationAdapter(environment=env, client=client)
        adapter._supports_client_method = MagicMock(return_value=True)
        adapter._normalize_preview_response = MagicMock(
            return_value={"compiled_sql": "SELECT 2"}
        )

        payload = MagicMock()
        payload.dataset_id = 1
        payload.template_params = {}
        payload.effective_filters = []

        result = await adapter._request_superset_preview(payload)
        assert result["compiled_sql"] == "SELECT 2"

    @pytest.mark.asyncio
    async def test_compile_preview_exception_falls_to_dataset_preview(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        # compile_preview raises ValueError
        client.compile_preview = AsyncMock(side_effect=ValueError("bad call"))

        adapter = SupersetCompilationAdapter(environment=env, client=client)
        adapter._supports_client_method = MagicMock(return_value=True)
        # Must return non-None so the normalized-check passes
        adapter._normalize_preview_response = MagicMock(
            return_value={"compiled_sql": "SELECT 3"}
        )

        # Fallback: compile_dataset_preview succeeds
        client.compile_dataset_preview = AsyncMock(
            return_value={"compiled_sql": "SELECT 3"}
        )

        payload = MagicMock()
        payload.dataset_id = 1
        payload.template_params = {}
        payload.effective_filters = []

        result = await adapter._request_superset_preview(payload)
        assert result["compiled_sql"] == "SELECT 3"

    @pytest.mark.asyncio
    async def test_both_methods_fail_raises_error(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        client.compile_preview = AsyncMock(side_effect=ValueError("fail"))
        client.compile_dataset_preview = AsyncMock(
            side_effect=RuntimeError("also fail")
        )

        adapter = SupersetCompilationAdapter(environment=env, client=client)
        adapter._supports_client_method = MagicMock(side_effect=[True, True])
        adapter._normalize_preview_response = MagicMock(return_value=None)

        payload = MagicMock()
        payload.dataset_id = 1
        payload.template_params = {}
        payload.effective_filters = []

        with pytest.raises(RuntimeError, match="also fail"):
            await adapter._request_superset_preview(payload)

    @pytest.mark.asyncio
    async def test_fallback_to_raw_endpoint(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        # Neither compile_preview nor compile_dataset_preview exist
        # But _supports_client_method returns False for both
        client.network = MagicMock()
        client.network.request = AsyncMock(
            return_value={"sql": "SELECT raw"}
        )

        adapter = SupersetCompilationAdapter(environment=env, client=client)
        adapter._supports_client_method = MagicMock(return_value=False)
        adapter._dump_json = MagicMock(return_value='{"key": "val"}')
        adapter._normalize_preview_response = MagicMock(
            side_effect=lambda r: {"compiled_sql": "SELECT raw"} if isinstance(r, dict) else None
        )

        payload = MagicMock()
        payload.dataset_id = 1
        payload.template_params = {}
        payload.effective_filters = []

        result = await adapter._request_superset_preview(payload)
        assert result["compiled_sql"] == "SELECT raw"
        # Should have tried both fallback endpoints
        assert client.network.request.await_count >= 1

    @pytest.mark.asyncio
    async def test_all_fallback_endpoints_fail(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        client.network = MagicMock()
        client.network.request = AsyncMock(
            side_effect=[
                Exception("first failed"),
                Exception("second failed"),
            ]
        )

        adapter = SupersetCompilationAdapter(environment=env, client=client)
        adapter._supports_client_method = MagicMock(return_value=False)
        adapter._dump_json = MagicMock(return_value="{}")
        adapter._normalize_preview_response = MagicMock(return_value=None)

        payload = MagicMock()
        payload.dataset_id = 1
        payload.template_params = {}
        payload.effective_filters = []

        with pytest.raises(RuntimeError, match="first failed"):
            await adapter._request_superset_preview(payload)


class TestRequestSqlLabSession:
    """_request_sql_lab_session: SQL Lab session creation."""

    @pytest.mark.asyncio
    async def test_successful_first_candidate(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        client.get_dataset = AsyncMock(
            return_value={
                "result": {
                    "database": {"id": 5},
                    "schema": "public",
                }
            }
        )
        client.network = MagicMock()
        client.network.request = AsyncMock(return_value={"query_id": 42})

        adapter = SupersetCompilationAdapter(environment=env, client=client)

        payload = MagicMock()
        payload.compiled_sql = "SELECT 1"
        payload.template_params = {}
        payload.preview_id = "prev-1"

        result = await adapter._request_sql_lab_session(payload)
        assert result == {"query_id": 42}

    @pytest.mark.asyncio
    async def test_fallback_to_second_candidate(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        client.get_dataset = AsyncMock(
            return_value={
                "result": {
                    "database_id": 10,
                    "schema": "public",
                }
            }
        )
        client.network = MagicMock()
        client.network.request = AsyncMock(
            side_effect=[
                Exception("first failed"),
                {"id": 99},
            ]
        )

        adapter = SupersetCompilationAdapter(environment=env, client=client)

        payload = MagicMock()
        payload.compiled_sql = "SELECT *"
        payload.template_params = {}
        payload.preview_id = "prev-2"

        result = await adapter._request_sql_lab_session(payload)
        assert result == {"id": 99}

    @pytest.mark.asyncio
    async def test_all_candidates_fail(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        client.get_dataset = AsyncMock(
            return_value={
                "result": {
                    "database": {"id": 5},
                }
            }
        )
        client.network = MagicMock()
        client.network.request = AsyncMock(
            side_effect=[
                Exception("first failed"),
                Exception("second failed"),
            ]
        )

        adapter = SupersetCompilationAdapter(environment=env, client=client)

        payload = MagicMock()
        payload.compiled_sql = "SELECT *"
        payload.template_params = {}
        payload.preview_id = "prev-3"

        with pytest.raises(RuntimeError, match="first failed"):
            await adapter._request_sql_lab_session(payload)

    @pytest.mark.asyncio
    async def test_missing_database_id_raises(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        client = MagicMock()
        client.get_dataset = AsyncMock(return_value={"result": {}})

        adapter = SupersetCompilationAdapter(environment=env, client=client)

        payload = MagicMock()
        payload.compiled_sql = "SELECT *"

        with pytest.raises(RuntimeError, match="does not expose a database identifier"):
            await adapter._request_sql_lab_session(payload)


class TestNormalizePreviewResponse:
    """_normalize_preview_response: extracts compiled SQL from various shapes."""

    def test_response_with_compiled_sql(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()
        adapter = SupersetCompilationAdapter(environment=env)
        result = adapter._normalize_preview_response({"compiled_sql": "SELECT 1"})
        assert result == {"compiled_sql": "SELECT 1", "raw_response": {"compiled_sql": "SELECT 1"}}

    def test_response_with_sql_key(self):
        adapter = _make_adapter()
        result = adapter._normalize_preview_response({"sql": "SELECT * FROM t"})
        assert result["compiled_sql"] == "SELECT * FROM t"

    def test_response_with_query_key(self):
        adapter = _make_adapter()
        result = adapter._normalize_preview_response({"query": "SELECT count(*)"})
        assert result["compiled_sql"] == "SELECT count(*)"

    def test_response_with_nested_result(self):
        adapter = _make_adapter()
        result = adapter._normalize_preview_response(
            {"result": {"compiled_sql": "SELECT nested"}}
        )
        assert result["compiled_sql"] == "SELECT nested"

    def test_non_dict_returns_none(self):
        adapter = _make_adapter()
        assert adapter._normalize_preview_response("not_a_dict") is None

    def test_all_null_or_empty_returns_none(self):
        adapter = _make_adapter()
        result = adapter._normalize_preview_response({"compiled_sql": None, "result": {}})
        assert result is None


def _make_adapter():
    from src.core.utils.superset_compilation_adapter import (
        SupersetCompilationAdapter,
    )

    return SupersetCompilationAdapter(environment=MagicMock())


class TestDumpJson:
    """_dump_json: deterministic JSON serialization."""

    def test_sorted_keys(self):
        adapter = _make_adapter()
        result = adapter._dump_json({"b": 2, "a": 1})
        assert result == '{"a": 1, "b": 2}'

    def test_default_str_for_non_serializable(self):
        adapter = _make_adapter()
        result = adapter._dump_json({"date": datetime(2024, 1, 1, tzinfo=UTC)})
        assert "2024-01-01" in result


class TestSupportsClientMethod:
    """_supports_client_method: method detection."""

    def test_method_in_instance_dict(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()

        class FakeClient:
            pass

        client = FakeClient()
        client.my_method = lambda: None

        adapter = SupersetCompilationAdapter(environment=env, client=client)
        assert adapter._supports_client_method("my_method") is True

    def test_method_not_found(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()

        class FakeClient:
            pass

        client = FakeClient()

        adapter = SupersetCompilationAdapter(environment=env, client=client)
        assert adapter._supports_client_method("nonexistent") is False

    def test_method_on_class(self):
        from src.core.utils.superset_compilation_adapter import (
            SupersetCompilationAdapter,
        )

        env = MagicMock()

        class FakeClient:
            def compile_preview(self):
                pass

        client = FakeClient()
        adapter = SupersetCompilationAdapter(environment=env, client=client)
        assert adapter._supports_client_method("compile_preview") is True
# #endregion Test.SupersetCompilationAdapter
