# #region Test.SupersetClient.DashboardsCrud.Edge2 [C:3] [TYPE Module] [SEMANTICS test,superset,dashboard,crud,edge,fallback]
# @BRIEF Edge tests for SupersetDashboardsCrudMixin — get_dashboard_detail branches, fallback chart/dataset resolution, parse errors.
# @RELATION BINDS_TO -> [SupersetDashboardsCrudMixin]
import json
from unittest.mock import AsyncMock, MagicMock

import pytest

from src.core.config_models import Environment
from src.core.superset_client import SupersetClient


def _make_client() -> SupersetClient:
    env = Environment(id="env1", name="T", url="http://test.local", username="u", password="p")
    client = SupersetClient(env)
    mc = MagicMock()
    mc.request = AsyncMock(return_value={})
    mc.fetch_paginated_data = AsyncMock(return_value=[])
    mc.upload_file = AsyncMock(return_value={"status": "ok"})
    client.client = mc
    client.network = mc
    return client


# ── get_dashboard_detail: form_data / charts / datasets extra branches ───


class TestGetDashboardDetailDetailBranches:
    """Extra branches in get_dashboard_detail — form_data, charts, datasets."""

    @pytest.mark.asyncio
    async def test_form_data_invalid_json(self):
        """Invalid JSON form_data falls back to chart_obj.datasource_id."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={"result": {"id": 1, "dashboard_title": "D"}})
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": [{"id": 10, "slice_name": "C", "form_data": "{invalid json!!!}", "datasource_id": 42}]})
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"][0]["dataset_id"] == 42

    @pytest.mark.asyncio
    async def test_charts_endpoint_raises_exception(self):
        """Charts endpoint exception leaves charts list empty."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={"result": {"id": 1, "dashboard_title": "D"}})
        async def mock_req(method, endpoint, **kw):
            if "/charts" in endpoint:
                raise Exception("Network error")
            if "/datasets" in endpoint:
                return {"result": []}
            return {}
        c.client.request = AsyncMock(side_effect=mock_req)
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"] == []

    @pytest.mark.asyncio
    async def test_datasets_endpoint_raises_exception(self):
        """Datasets endpoint exception leaves datasets list empty."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={"result": {"id": 1, "dashboard_title": "D"}})
        async def mock_req(method, endpoint, **kw):
            if "/charts" in endpoint:
                return {"result": []}
            if "/datasets" in endpoint:
                raise Exception("Network error")
            return {}
        c.client.request = AsyncMock(side_effect=mock_req)
        detail = await c.get_dashboard_detail(1)
        assert detail["datasets"] == []

    @pytest.mark.asyncio
    async def test_non_dict_chart_in_payload(self):
        """Non-dict chart in API payload skipped via continue."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={"result": {"id": 1, "dashboard_title": "D"}})
        c.client.request = AsyncMock(return_value={"result": ["not_a_dict", {"id": 10, "slice_name": "C", "datasource_id": 99}]})
        detail = await c.get_dashboard_detail(1)
        assert len(detail["charts"]) == 1 and detail["charts"][0]["id"] == 10

    @pytest.mark.asyncio
    async def test_chart_without_id(self):
        """Chart without 'id' key skipped via continue."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={"result": {"id": 1, "dashboard_title": "D"}})
        c.client.request = AsyncMock(return_value={"result": [{"slice_name": "NoID"}, {"id": 10, "slice_name": "C", "datasource_id": 99}]})
        detail = await c.get_dashboard_detail(1)
        assert len(detail["charts"]) == 1 and detail["charts"][0]["id"] == 10

    @pytest.mark.asyncio
    async def test_non_dict_dataset_in_payload(self):
        """Non-dict dataset in API payload skipped."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={"result": {"id": 1, "dashboard_title": "D"}})
        async def mock_req(method, endpoint, **kw):
            if "/datasets" in endpoint:
                return {"result": ["not_a_dict", {"id": 42, "table_name": "orders"}]}
            return {"result": []}
        c.client.request = AsyncMock(side_effect=mock_req)
        detail = await c.get_dashboard_detail(1)
        assert len(detail["datasets"]) == 1 and detail["datasets"][0]["id"] == 42

    @pytest.mark.asyncio
    async def test_dataset_without_id(self):
        """Dataset without 'id' key skipped."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={"result": {"id": 1, "dashboard_title": "D"}})
        async def mock_req(method, endpoint, **kw):
            if "/datasets" in endpoint:
                return {"result": [{"table_name": "no_id"}, {"id": 42, "table_name": "orders"}]}
            return {"result": []}
        c.client.request = AsyncMock(side_effect=mock_req)
        detail = await c.get_dashboard_detail(1)
        assert len(detail["datasets"]) == 1 and detail["datasets"][0]["id"] == 42


# ── get_dashboard_detail: fallback chart extraction from position_json/json_metadata ───


class TestGetDashboardDetailFallback:
    """Fallback chart extraction when /charts endpoint returns nothing."""

    @pytest.mark.asyncio
    async def test_position_json_as_dict(self):
        """position_json as dict is parsed for chart IDs (elif isinstance(dict) branch)."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_json": {
                    "DASHBOARD_VERSION_KEY": "v2",
                    "CHART-42": {
                        "id": "CHART-42",
                        "meta": {"chartId": 42},
                        "type": "CHART",
                    },
                },
                "json_metadata": "",
                "changed_on_utc": "2025-01-01",
            }
        })
        c.get_chart = AsyncMock(return_value={
            "result": {
                "id": 42,
                "slice_name": "Fallback Chart",
                "viz_type": "table",
                "datasource_id": 99,
                "changed_on": "2025-01-01",
            }
        })
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": []})
        detail = await c.get_dashboard_detail(1)
        assert len(detail["charts"]) >= 1
        assert detail["charts"][0]["id"] == 42
        assert detail["charts"][0]["title"] == "Fallback Chart"

    @pytest.mark.asyncio
    async def test_json_metadata_with_chart_ids(self):
        """json_metadata as string with non-recognized keys yields no fallback charts."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_json": "",
                "json_metadata": '{"chartIds": [77, 88]}',
                "changed_on_utc": "2025-01-01",
            }
        })

        async def mock_get_chart(chart_id):
            return {"result": {
                "id": chart_id,
                "slice_name": f"Chart {chart_id}",
                "viz_type": "line",
                "datasource_id": 100 + chart_id,
                "changed_on": "2025-01-01",
            }}

        c.get_chart = AsyncMock(side_effect=mock_get_chart)
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": []})
        detail = await c.get_dashboard_detail(1)
        # "chartIds" key is NOT in recognized keys (chartId/chart_id/slice_id/sliceId)
        assert detail["charts"] == []

    @pytest.mark.asyncio
    async def test_fallback_from_json_metadata_with_chart_objects(self):
        """json_metadata with recognized chartId objects triggers fallback."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_json": "",
                "json_metadata": json.dumps({
                    "native_filter": {},
                    "chart_configs": [
                        {"chartId": 42, "title": "Sales"},
                        {"chartId": 99, "title": "Revenue"},
                    ],
                }),
                "changed_on_utc": "2025-01-01",
            }
        })

        async def mock_get_chart(chart_id):
            return {"result": {
                "id": chart_id,
                "slice_name": f"Chart {chart_id}",
                "viz_type": "bar",
                "datasource_id": 100 + chart_id,
                "changed_on": "2025-01-01",
            }}

        c.get_chart = AsyncMock(side_effect=mock_get_chart)
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": []})
        detail = await c.get_dashboard_detail(1)
        assert len(detail["charts"]) >= 1

    @pytest.mark.asyncio
    async def test_fallback_get_chart_error(self):
        """Fallback get_chart failure is handled gracefully."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_json": {
                    "CHART-42": {
                        "id": "CHART-42",
                        "meta": {"chartId": 42},
                        "type": "CHART",
                    },
                    "CHART-99": {
                        "id": "CHART-99",
                        "meta": {"chartId": 99},
                        "type": "CHART",
                    },
                },
                "json_metadata": "",
                "changed_on_utc": "2025-01-01",
            }
        })

        call_count = 0

        async def mock_get_chart(chart_id):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise Exception("Chart fetch failure")
            return {"result": {
                "id": 99,
                "slice_name": "Chart 99",
                "viz_type": "line",
                "datasource_id": 199,
                "changed_on": "2025-01-01",
            }}

        c.get_chart = AsyncMock(side_effect=mock_get_chart)
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": []})
        detail = await c.get_dashboard_detail(1)
        assert len(detail["charts"]) >= 1
        assert detail["charts"][0]["id"] == 99

    @pytest.mark.asyncio
    async def test_fallback_json_metadata_as_dict(self):
        """json_metadata as dict is parsed directly (not via json.loads)."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_json": "",
                "json_metadata": {"native_filter": {}, "chartId": 42},
                "changed_on_utc": "2025-01-01",
            }
        })

        async def mock_get_chart(chart_id):
            return {"result": {
                "id": chart_id,
                "slice_name": f"Chart {chart_id}",
                "viz_type": "line",
                "datasource_id": 100 + chart_id,
                "changed_on": "2025-01-01",
            }}

        c.get_chart = AsyncMock(side_effect=mock_get_chart)
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": []})
        detail = await c.get_dashboard_detail(1)
        assert len(detail["charts"]) >= 1
        assert detail["charts"][0]["id"] == 42

    @pytest.mark.asyncio
    async def test_both_fallbacks_dedup(self):
        """chart IDs from position_json and json_metadata are deduplicated."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_json": json.dumps({
                    "CHART-42": {
                        "id": "CHART-42",
                        "meta": {"chartId": 42},
                        "type": "CHART",
                    },
                }),
                "json_metadata": json.dumps({
                    "chartId": 42,
                }),
                "changed_on_utc": "2025-01-01",
            }
        })

        call_count = 0

        async def mock_get_chart(chart_id):
            nonlocal call_count
            call_count += 1
            return {"result": {
                "id": chart_id,
                "slice_name": f"Chart {chart_id}",
                "viz_type": "table",
                "datasource_id": 99,
                "changed_on": "2025-01-01",
            }}

        c.get_chart = AsyncMock(side_effect=mock_get_chart)
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": []})
        detail = await c.get_dashboard_detail(1)
        assert len(detail["charts"]) == 1
        assert detail["charts"][0]["id"] == 42
        assert call_count == 1


# ── get_dashboard_detail: backfill missing datasets from charts ───


class TestGetDashboardDetailBackfill:
    """Backfill datasets referenced by chart datasource IDs."""

    @pytest.mark.asyncio
    async def test_backfill_datasets(self):
        """Missing dataset IDs from chart references are backfilled."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock(return_value={
            "result": {
                "id": 99,
                "table_name": "orders",
                "schema": "public",
                "database": {"database_name": "MainDB"},
                "changed_on_utc": "2025-01-01",
            }
        })

        async def mock_req(method, endpoint, **kw):
            if "/charts" in endpoint:
                return {"result": [
                    {
                        "id": 10,
                        "slice_name": "C",
                        "form_data": json.dumps({"datasource": "99__table", "viz_type": "line"}),
                        "datasource_id": 99,
                    }
                ]}
            if "/datasets" in endpoint:
                return {"result": []}
            return {}

        c.client.request = AsyncMock(side_effect=mock_req)
        detail = await c.get_dashboard_detail(1)
        assert len(detail["datasets"]) >= 1
        assert detail["datasets"][0]["id"] == 99
        assert detail["datasets"][0]["table_name"] == "orders"

    @pytest.mark.asyncio
    async def test_backfill_no_dup(self):
        """Datasets already present are not backfilled again."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock()

        async def mock_req(method, endpoint, **kw):
            if "/charts" in endpoint:
                return {"result": [{
                    "id": 10, "slice_name": "C",
                    "form_data": json.dumps({"datasource": "99__table"}),
                    "datasource_id": 99,
                }]}
            if "/datasets" in endpoint:
                return {"result": [{"id": 99, "table_name": "orders", "database": {"database_name": "DB"}}]}
            return {}

        c.client.request = AsyncMock(side_effect=mock_req)
        detail = await c.get_dashboard_detail(1)
        assert len(detail["datasets"]) == 1
        c.get_dataset.assert_not_called()

    @pytest.mark.asyncio
    async def test_backfill_exception_skipped(self):
        """Backfill dataset fetch exception does not break the whole result."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()

        call_count = 0

        async def mock_get_dataset(dataset_id):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                raise Exception("Dataset fetch error")
            return {
                "result": {
                    "id": 100,
                    "table_name": "products",
                    "database": {"database_name": "Analytics"},
                    "changed_on_utc": "2025-01-01T00:00:00",
                }
            }

        c.get_dataset = AsyncMock(side_effect=mock_get_dataset)

        async def mock_req(method, endpoint, **kw):
            if "/charts" in endpoint:
                return {"result": [
                    {"id": 10, "slice_name": "C", "form_data": json.dumps({"datasource": "99__table"}), "datasource_id": 99},
                    {"id": 11, "slice_name": "C2", "form_data": json.dumps({"datasource": "100__table"}), "datasource_id": 100},
                ]}
            if "/datasets" in endpoint:
                return {"result": []}
            return {}

        c.client.request = AsyncMock(side_effect=mock_req)
        detail = await c.get_dashboard_detail(1)
        assert len(detail["datasets"]) == 1
        assert detail["datasets"][0]["id"] == 100
        assert detail["datasets"][0]["database"] == "Analytics"

    @pytest.mark.asyncio
    async def test_backfill_dataset_without_db_name(self):
        """Backfilled dataset without database dict gets 'Unknown'."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock(return_value={
            "result": {
                "id": 99,
                "table_name": "orders",
                "schema": None,
                "changed_on_utc": "2025-01-01",
            }
        })

        async def mock_req(method, endpoint, **kw):
            if "/charts" in endpoint:
                return {"result": [{
                    "id": 10, "slice_name": "C",
                    "form_data": json.dumps({"datasource": "99__table"}),
                    "datasource_id": 99,
                }]}
            if "/datasets" in endpoint:
                return {"result": []}
            return {}

        c.client.request = AsyncMock(side_effect=mock_req)
        detail = await c.get_dashboard_detail(1)
        assert detail["datasets"][0]["database"] == "Unknown"


# ── extract_dataset_id_from_form_data: remaining branches ───


class TestExtractDatasetIdBranches:
    """Remaining branches in extract_dataset_id_from_form_data."""

    @pytest.mark.asyncio
    async def test_datasource_id_non_int(self):
        """datasource_id that is not int-convertable falls back to None."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={
            "result": [
                {
                    "id": 10,
                    "slice_name": "C",
                    "form_data": {"datasource": 999, "datasource_id": [1, 2, 3]},
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        # datasource is int (non-str/dict), datasource_id in form_data is list
        assert detail["charts"][0]["dataset_id"] is None

    @pytest.mark.asyncio
    async def test_datasource_id_non_int_str_form_data(self):
        """datasource_id as non-numeric string raises ValueError in fallback."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={
            "result": [
                {
                    "id": 10,
                    "slice_name": "C",
                    "form_data": {"datasource_id": "not_a_number"},
                    "datasource_id": 99,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        # Falls through to chart_obj.datasource_id which is 99
        assert detail["charts"][0]["dataset_id"] == 99


# ── get_dashboard_detail: position_json/json_metadata parse errors ───


class TestGetDashboardDetailParseErrors:
    """Parse error fallbacks in get_dashboard_detail fallback code."""

    @pytest.mark.asyncio
    async def test_position_json_invalid_string(self):
        """position_json as invalid JSON string triggers except handler."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_json": "{not valid json!!!",
                "json_metadata": "",
                "changed_on_utc": "2025-01-01",
            }
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": []})
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"] == []

    @pytest.mark.asyncio
    async def test_json_metadata_invalid_string(self):
        """json_metadata as invalid JSON string triggers except handler."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_json": "",
                "json_metadata": "{not valid json!!!",
                "changed_on_utc": "2025-01-01",
            }
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock()
        c.client.request = AsyncMock(return_value={"result": []})
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"] == []
# #endregion Test.SupersetClient.DashboardsCrud.Edge2
