# #region Test.SupersetContextParsing [C:3] [TYPE Module] [SEMANTICS test, superset, context, parse, link]
# @BRIEF Tests for _parsing.py — SupersetContextParsingMixin.parse_superset_link full branch coverage.
# @RELATION BINDS_TO -> [SupersetContextParsingMixin]
# @TEST_CONTRACT: [link:str] -> [SupersetParsedContext]
# @TEST_SCENARIO: dataset_link -> resolves dataset_id and dataset_ref
# @TEST_SCENARIO: dashboard_permalink -> resolves via get_dashboard_permalink_state
# @TEST_SCENARIO: dashboard_native_filters -> resolves via get_dashboard_detail + native_filters_key
# @TEST_SCENARIO: chart_link -> partial recovery
# @TEST_SCENARIO: empty_link -> ValueError
# @TEST_EDGE: missing_dataset_detail -> partial_recovery set True
# @TEST_EDGE: permalink_non_dict_state -> logged, no crash
# @TEST_EDGE: unsupported_link_shape -> ValueError

from __future__ import annotations

import sys
from pathlib import Path

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

import os

os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from src.core.utils.superset_context_extractor._base import (
    SupersetContextExtractorBase,
    SupersetParsedContext,
)
from src.core.utils.superset_context_extractor._filters import (
    SupersetContextFiltersExtractMixin,
)
from src.core.utils.superset_context_extractor._parsing import (
    SupersetContextParsingMixin,
)


class _TestExtractor(
    SupersetContextFiltersExtractMixin,
    SupersetContextParsingMixin,
    SupersetContextExtractorBase,
):
    """Concrete test class matching the full extractor MRO for test isolation."""
    pass


@pytest.fixture
def mock_env():
    """Return env mock."""
    env = MagicMock()
    env.id = "test-env"
    env.url = "https://superset.example.com"
    return env


@pytest.fixture
def extractor(mock_env):
    """Return _TestExtractor with client where:
    - Awaited methods use AsyncMock
    - Non-awaited methods use MagicMock
    """
    client = MagicMock()
    # AWAITED in source
    client.get_dashboard_detail = AsyncMock()
    client.get_chart = AsyncMock()
    client.get_dashboard_permalink_state = AsyncMock()
    # SYNC calls — MagicMock with sensible return values
    client.extract_native_filters_from_key = MagicMock()
    # get_dataset_detail runs AFTER dataset_id is resolved — always set a default
    client.get_dataset_detail = AsyncMock(
        return_value={"table_name": "users", "schema": "public"}
    )
    return _TestExtractor(environment=mock_env, client=client)


def _set_dataset_detail(extractor, return_value=None):
    """Helper: set async get_dataset_detail mock."""
    if return_value is None:
        return_value = {"table_name": "users", "schema": "public"}
    extractor.client.get_dataset_detail = AsyncMock(return_value=return_value)


def _set_dashboard_detail(extractor, return_value=None):
    """Helper: set async get_dashboard_detail mock."""
    if return_value is None:
        return_value = {"id": 5, "datasets": [{"id": 10}]}
    extractor.client.get_dashboard_detail = AsyncMock(return_value=return_value)


def _set_permalink_state(extractor, return_value=None):
    """Helper: set async get_dashboard_permalink_state mock."""
    if return_value is None:
        return_value = {"state": {"dashboardId": 42}}
    extractor.client.get_dashboard_permalink_state = AsyncMock(
        return_value=return_value
    )


def _set_chart(extractor, return_value=None):
    """Helper: set async get_chart mock."""
    if return_value is None:
        return_value = {"result": {"datasource_id": 15}}
    extractor.client.get_chart = AsyncMock(return_value=return_value)


def _set_native_filters_key(extractor, return_value=None):
    """Helper: set sync extract_native_filters_from_key mock."""
    if return_value is None:
        return_value = {"dataMask": {"NATIVE-1": {"filterState": {"value": "US"}}}}
    extractor.client.extract_native_filters_from_key = MagicMock(
        return_value=return_value
    )


# ──────────────────────────────────────────────
# Input validation
# ──────────────────────────────────────────────


class TestParseSupersetLinkValidation:
    """parse_superset_link input guards."""

    @pytest.mark.asyncio
    async def test_empty_link_raises_value_error(self, extractor):
        """Empty link string raises ValueError."""
        with pytest.raises(ValueError, match="must be non-empty"):
            await extractor.parse_superset_link("")

    @pytest.mark.asyncio
    async def test_none_link_raises_value_error(self, extractor):
        """None link raises ValueError."""
        with pytest.raises(ValueError, match="must be non-empty"):
            await extractor.parse_superset_link(None)

    @pytest.mark.asyncio
    async def test_whitespace_link_raises_value_error(self, extractor):
        """Whitespace-only link raises ValueError."""
        with pytest.raises(ValueError, match="must be non-empty"):
            await extractor.parse_superset_link("   ")

    @pytest.mark.asyncio
    async def test_non_http_scheme_raises_value_error(self, extractor):
        """Non-http(s) scheme raises ValueError."""
        with pytest.raises(ValueError, match="absolute http"):
            await extractor.parse_superset_link("ftp://superset.example.com/dataset/5")

    @pytest.mark.asyncio
    async def test_missing_netloc_raises_value_error(self, extractor):
        """URL without netloc raises ValueError."""
        with pytest.raises(ValueError, match="absolute http"):
            await extractor.parse_superset_link("http:///dataset/5")


# ──────────────────────────────────────────────
# Direct dataset link
# ──────────────────────────────────────────────


class TestParseSupersetLinkDataset:
    """parse_superset_link — direct /dataset/N/ path."""

    DATASET_URL = "https://superset.example.com/superset/dataset/5/"

    @pytest.mark.asyncio
    async def test_direct_dataset_link(self, extractor):
        """dataset/5/ resolves dataset_id=5 and resource_type='dataset'."""
        _set_dataset_detail(extractor)
        result = await extractor.parse_superset_link(self.DATASET_URL)
        assert result.dataset_id == 5
        assert result.resource_type == "dataset"
        assert result.dataset_ref == "public.users"
        assert result.chart_id is None
        assert result.dashboard_id is None
        assert result.partial_recovery is False

    @pytest.mark.asyncio
    async def test_dataset_link_without_schema(self, extractor):
        """dataset detail without schema uses just table_name as ref."""
        _set_dataset_detail(extractor, {"table_name": "users", "schema": ""})
        result = await extractor.parse_superset_link(self.DATASET_URL)
        assert result.dataset_ref == "users"

    @pytest.mark.asyncio
    async def test_dataset_detail_fetch_fails_partial_recovery(self, extractor):
        """Dataset detail fetch failure sets partial_recovery=True."""
        extractor.client.get_dataset_detail = AsyncMock(
            side_effect=Exception("Superset API timeout")
        )
        result = await extractor.parse_superset_link(self.DATASET_URL)
        assert result.dataset_id == 5
        assert result.partial_recovery is True
        assert "dataset_detail_lookup_failed" in result.unresolved_references


# ──────────────────────────────────────────────
# Dashboard permalink
# ──────────────────────────────────────────────


class TestParseSupersetLinkDashboardPermalink:
    """parse_superset_link — /dashboard/p/<key>/ path"""

    PERMALINK_URL = "https://superset.example.com/superset/dashboard/p/abc123/"

    @pytest.mark.asyncio
    async def test_permalink_resolves_dashboard_with_data_mask(self, extractor):
        """Permalink state with dataMask + dashboardId resolves fully."""
        _set_permalink_state(extractor, {
            "state": {
                "dashboardId": 42,
                "dataMask": {"NATIVE-1": {"filterState": {"value": "US"}}},
            }
        })
        _set_dashboard_detail(extractor)
        result = await extractor.parse_superset_link(self.PERMALINK_URL)
        assert result.dashboard_id == 42
        assert result.dataset_id == 10
        assert result.resource_type == "dashboard"
        assert "dataMask" in result.query_state

    @pytest.mark.asyncio
    async def test_permalink_resolves_chart_recovery(self, extractor):
        """Permalink without dashboardId but with sliceId recovers dataset from chart."""
        _set_permalink_state(extractor, {
            "state": {
                "slice_id": 77,
                "dataMask": {},
            }
        })
        _set_chart(extractor)
        result = await extractor.parse_superset_link(self.PERMALINK_URL)
        assert result.chart_id == 77
        assert result.dataset_id == 15
        # dataset_ref gets canonicalized by get_dataset_detail result
        assert result.dataset_ref == "public.users"

    @pytest.mark.asyncio
    async def test_permalink_chart_missing_datasource_id(self, extractor):
        """Chart without datasource_id adds unresolvable marker."""
        _set_permalink_state(extractor, {
            "state": {
                "slice_id": 77,
                "dataMask": {},
            }
        })
        _set_chart(extractor, {"result": {}})
        result = await extractor.parse_superset_link(self.PERMALINK_URL)
        assert result.dataset_id is None
        assert "chart_dataset_binding_unresolved" in result.unresolved_references

    @pytest.mark.asyncio
    async def test_permalink_chart_fetch_raises(self, extractor):
        """Chart fetch failure adds unresolvable marker."""
        _set_permalink_state(extractor, {
            "state": {
                "slice_id": 77,
                "dataMask": {},
            }
        })
        extractor.client.get_chart = AsyncMock(side_effect=Exception("Chart API error"))
        result = await extractor.parse_superset_link(self.PERMALINK_URL)
        assert "chart_dataset_binding_unresolved" in result.unresolved_references

    @pytest.mark.asyncio
    async def test_permalink_state_not_a_dict(self, extractor):
        """Permalink state value is not a dict — logged, does not crash."""
        _set_permalink_state(extractor, {"state": "not_a_dict"})
        extractor.client.get_dashboard_detail = AsyncMock(
            return_value={"id": 42, "datasets": []}
        )
        result = await extractor.parse_superset_link(self.PERMALINK_URL)
        assert result.resource_type == "dashboard"
        assert result.partial_recovery is True

    @pytest.mark.asyncio
    async def test_permalink_no_ids_found(self, extractor):
        """Permalink state without dashboardId or sliceId results in partial."""
        _set_permalink_state(extractor, {
            "state": {
                "dataMask": {},
            }
        })
        result = await extractor.parse_superset_link(self.PERMALINK_URL)
        assert result.partial_recovery is True
        assert result.dataset_id is None


# ──────────────────────────────────────────────
# Dashboard slug / numeric reference
# ──────────────────────────────────────────────


class TestParseSupersetLinkDashboardRef:
    """parse_superset_link — /dashboard/<id-or-slug>/ path"""

    DASH_URL = "https://superset.example.com/superset/dashboard/my-dash/"

    @pytest.mark.asyncio
    async def test_dashboard_slug_resolves_datasets(self, extractor):
        """Dashboard slug resolves dashboard detail and first dataset."""
        _set_dashboard_detail(extractor, {
            "id": 5,
            "datasets": [{"id": 10, "table_name": "users"}],
        })
        result = await extractor.parse_superset_link(self.DASH_URL)
        assert result.dashboard_id == 5
        assert result.dataset_id == 10
        # dataset_ref gets canonicalized by get_dataset_detail result
        assert result.dataset_ref == "public.users"

    @pytest.mark.asyncio
    async def test_dashboard_multiple_datasets_partial(self, extractor):
        """Dashboard with multiple datasets sets partial_recovery."""
        _set_dashboard_detail(extractor, {
            "id": 5,
            "datasets": [
                {"id": 10, "table_name": "users"},
                {"id": 11, "table_name": "orders"},
            ],
        })
        result = await extractor.parse_superset_link(self.DASH_URL)
        assert result.partial_recovery is True
        assert "multiple_dashboard_datasets" in result.unresolved_references

    @pytest.mark.asyncio
    async def test_dashboard_no_datasets_partial(self, extractor):
        """Dashboard with no datasets sets partial_recovery."""
        _set_dashboard_detail(extractor, {
            "id": 5,
            "datasets": [],
        })
        result = await extractor.parse_superset_link(self.DASH_URL)
        assert result.partial_recovery is True
        assert "dashboard_dataset_binding_missing" in result.unresolved_references

    @pytest.mark.asyncio
    async def test_dashboard_dataset_missing_id_partial(self, extractor):
        """Dashboard dataset entry without id sets partial."""
        _set_dashboard_detail(extractor, {
            "id": 5,
            "datasets": [{"table_name": "users"}],
        })
        result = await extractor.parse_superset_link(self.DASH_URL)
        assert result.partial_recovery is True
        assert "dashboard_dataset_id_missing" in result.unresolved_references


# ──────────────────────────────────────────────
# Dashboard with native_filters_key
# ──────────────────────────────────────────────


class TestParseSupersetLinkNativeFiltersKey:
    """parse_superset_link — dashboard link with native_filters_key query param."""

    @pytest.mark.asyncio
    async def test_native_filters_key_success(self, extractor):
        """native_filters_key fetches filter state and merges dataMask."""
        url = "https://superset.example.com/superset/dashboard/5?native_filters_key=key123"
        _set_dashboard_detail(extractor, {
            "id": 5,
            "datasets": [{"id": 10}],
        })
        _set_native_filters_key(extractor)
        result = await extractor.parse_superset_link(url)
        assert result.dashboard_id == 5
        assert "native_filter_state" in result.query_state
        extractor.client.extract_native_filters_from_key.assert_called_once_with(5, "key123")

    @pytest.mark.asyncio
    async def test_native_filters_key_empty_datamask(self, extractor):
        """native_filters_key with empty dataMask is logged, not merged."""
        url = "https://superset.example.com/superset/dashboard/5?native_filters_key=key123"
        _set_dashboard_detail(extractor, {
            "id": 5,
            "datasets": [{"id": 10}],
        })
        _set_native_filters_key(extractor, {"dataMask": {}})
        result = await extractor.parse_superset_link(url)
        assert "native_filter_state" not in result.query_state

    @pytest.mark.asyncio
    async def test_native_filters_key_exception(self, extractor):
        """native_filters_key failure is logged, does not crash."""
        url = "https://superset.example.com/superset/dashboard/5?native_filters_key=key123"
        _set_dashboard_detail(extractor, {
            "id": 5,
            "datasets": [{"id": 10}],
        })
        extractor.client.extract_native_filters_from_key = MagicMock(
            side_effect=Exception("API error")
        )
        result = await extractor.parse_superset_link(url)
        assert result.dashboard_id == 5
        assert result.dataset_id == 10


# ──────────────────────────────────────────────
# Chart-only link
# ──────────────────────────────────────────────


class TestParseSupersetLinkChartOnly:
    """parse_superset_link — /chart/N/ path without dashboard context."""

    CHART_URL = "https://superset.example.com/superset/chart/42/"

    @pytest.mark.asyncio
    async def test_chart_only_partial_recovery(self, extractor):
        """Chart-only link returns partial_recovery=True."""
        result = await extractor.parse_superset_link(self.CHART_URL)
        assert result.chart_id == 42
        assert result.resource_type == "chart"
        assert result.partial_recovery is True
        assert "chart_dataset_binding_unresolved" in result.unresolved_references
        assert result.dataset_ref == "chart:42"


# ──────────────────────────────────────────────
# Unsupported link shape
# ──────────────────────────────────────────────


class TestParseSupersetLinkUnsupported:
    """parse_superset_link — unrecognized path shapes."""

    @pytest.mark.asyncio
    async def test_unsupported_path_raises_value_error(self, extractor):
        """Link without dataset/dashboard/chart raises ValueError."""
        with pytest.raises(ValueError, match="Unsupported Superset link"):
            await extractor.parse_superset_link(
                "https://superset.example.com/superset/other/5/"
            )

    @pytest.mark.asyncio
    async def test_explore_path_without_known_prefix(self, extractor):
        """/explore/ path without known prefixes raises ValueError."""
        with pytest.raises(ValueError, match="Unsupported Superset link"):
            await extractor.parse_superset_link(
                "https://superset.example.com/superset/explore/"
            )


# ──────────────────────────────────────────────
# _recover_dataset_binding_from_dashboard
# ──────────────────────────────────────────────


class TestRecoverDatasetBindingFromDashboard:
    """_recover_dataset_binding_from_dashboard — recover dataset from dashboard detail."""

    @pytest.mark.asyncio
    async def test_recovers_from_single_dataset(self, extractor):
        """Single dataset returns resolved id."""
        _set_dashboard_detail(extractor, {"datasets": [{"id": 10}]})
        ds_id, refs = await extractor._recover_dataset_binding_from_dashboard(
            dashboard_id=5, dataset_ref=None, unresolved_references=[]
        )
        assert ds_id == 10

    @pytest.mark.asyncio
    async def test_recovers_from_multiple_datasets(self, extractor):
        """Multiple datasets adds multiple_dashboard_datasets marker."""
        _set_dashboard_detail(extractor, {"datasets": [{"id": 10}, {"id": 11}]})
        ds_id, refs = await extractor._recover_dataset_binding_from_dashboard(
            dashboard_id=5, dataset_ref=None, unresolved_references=[]
        )
        assert ds_id == 10
        assert "multiple_dashboard_datasets" in refs

    @pytest.mark.asyncio
    async def test_no_datasets_returns_none(self, extractor):
        """No datasets returns None with marker."""
        _set_dashboard_detail(extractor, {"datasets": []})
        ds_id, refs = await extractor._recover_dataset_binding_from_dashboard(
            dashboard_id=5, dataset_ref=None, unresolved_references=[]
        )
        assert ds_id is None
        assert "dashboard_dataset_binding_missing" in refs

    @pytest.mark.asyncio
    async def test_dataset_missing_id_returns_none(self, extractor):
        """Dataset without id returns None."""
        _set_dashboard_detail(extractor, {"datasets": [{"table_name": "users"}]})
        ds_id, refs = await extractor._recover_dataset_binding_from_dashboard(
            dashboard_id=5, dataset_ref=None, unresolved_references=[]
        )
        assert ds_id is None
        assert "dashboard_dataset_id_missing" in refs

    @pytest.mark.asyncio
    async def test_existing_markers_not_duplicated(self, extractor):
        """Existing markers are not duplicated."""
        _set_dashboard_detail(extractor, {"datasets": []})
        _, refs = await extractor._recover_dataset_binding_from_dashboard(
            dashboard_id=5,
            dataset_ref=None,
            unresolved_references=["dashboard_dataset_binding_missing"],
        )
        matches = [r for r in refs if r == "dashboard_dataset_binding_missing"]
        assert len(matches) == 1
# #endregion Test.SupersetContextParsing
