# #region Test.SupersetClient.DashboardsCrud.Edge [C:3] [TYPE Module] [SEMANTICS test,superset,dashboard,crud,edge]
# @BRIEF Additional edge case tests for SupersetDashboardsCrudMixin — untested branches.
# @RELATION BINDS_TO -> [SupersetDashboardsCrudMixin]
# @TEST_EDGE: extract_dataset_id_form_data_none -> extract_dataset_id_from_form_data returns None for None input
# @TEST_EDGE: extract_dataset_id_string_datasource -> str datasource "42__table" extracts id 42
# @TEST_EDGE: extract_dataset_id_string_no_match -> str datasource without pattern returns None
# @TEST_EDGE: extract_dataset_id_string_bad_int -> str datasource with non-int prefix returns None
# @TEST_EDGE: extract_dataset_id_dict_bad_id -> dict datasource with non-int id returns None
# @TEST_EDGE: extract_dataset_id_no_datasource_key -> form_data with neither datasource nor datasource_id returns None
# @TEST_EDGE: export_dashboard_empty_content -> empty export raises SupersetAPIError
# @TEST_EDGE: import_dashboard_invalid_zip -> invalid zip raises SupersetAPIError
# @TEST_EDGE: import_dashboard_missing_file -> missing file raises FileNotFoundError
# @TEST_EDGE: delete_dashboard_non_dict -> delete handles response that is not a dict
from io import BytesIO
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock
import zipfile

import httpx
import pytest

from src.core.config_models import Environment
from src.core.superset_client import SupersetClient
from src.core.utils.network import SupersetAPIError


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


def _zip_with_metadata(tmp_path, name="import.zip"):
    p = tmp_path / name
    buf = BytesIO()
    with zipfile.ZipFile(buf, "w") as zf:
        zf.writestr("dash/metadata.yaml", "title: x")
    p.write_bytes(buf.getvalue())
    return p


class TestExtractDatasetIdFromFormData:
    """Direct tests for the inline extract_dataset_id_from_form_data function inside get_dashboard_detail."""

    @pytest.mark.asyncio
    async def test_form_data_is_none(self):
        """extract_dataset_id_from_form_data returns None when form_data is None."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock()
        # Mock charts endpoint with form_data=None
        c.client.request = AsyncMock(return_value={
            "result": [
                {
                    "id": 10,
                    "slice_name": "C",
                    "form_data": None,
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"][0]["dataset_id"] is None

    @pytest.mark.asyncio
    async def test_datasource_string_with_match(self):
        """String datasource '42__table' extracts id 42."""
        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": "42__table", "viz_type": "table"}',
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"][0]["dataset_id"] == 42

    @pytest.mark.asyncio
    async def test_datasource_string_no_pattern(self):
        """String datasource without N__ pattern returns 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": "table_only", "viz_type": "table"}',
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"][0]["dataset_id"] is None

    @pytest.mark.asyncio
    async def test_datasource_string_bad_int(self):
        """String datasource with non-numeric prefix returns 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": "abc__table", "viz_type": "table"}',
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"][0]["dataset_id"] is None

    @pytest.mark.asyncio
    async def test_datasource_dict_bad_id_type(self):
        """Dict datasource with non-convertible id returns 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": {"id": [1, 2]}, "viz_type": "table"}',
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"][0]["dataset_id"] is None

    @pytest.mark.asyncio
    async def test_no_datasource_key(self):
        """Form_data with neither datasource nor datasource_id returns 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": '{"viz_type": "table"}',
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"][0]["dataset_id"] is None

    @pytest.mark.asyncio
    async def test_datasource_int(self):
        """Integer datasource (not string or dict) returns 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": 42, "viz_type": "table"},
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        # int datasource falls through to datasource_id which is None, so dataset_id is None
        assert detail["charts"][0]["dataset_id"] is None


class TestGetDashboardDetailExtraEdge:
    """Additional edge cases for get_dashboard_detail."""

    @pytest.mark.asyncio
    async def test_flat_response_no_result_key(self):
        """get_dashboard returns flat data without 'result' key."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "id": 1,
            "dashboard_title": "Flat Dashboard",
            "slug": "flat",
            "description": "desc",
            "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["title"] == "Flat Dashboard"
        assert detail["charts"] == []
        assert detail["datasets"] == []

    @pytest.mark.asyncio
    async def test_charts_response_not_dict(self):
        """Charts payload that is not a dict is handled."""
        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="not_a_dict")
        detail = await c.get_dashboard_detail(1)
        assert detail["charts"] == []

    @pytest.mark.asyncio
    async def test_datasets_response_not_dict(self):
        """Datasets payload that is not a dict is handled."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock()
        # First call returns charts, second returns non-dict datasets
        call_count = 0

        async def mock_req(method, endpoint, **kw):
            nonlocal call_count
            call_count += 1
            if call_count == 1:
                return {"result": [{"id": 10, "slice_name": "C", "datasource_id": 99}]}
            return "not_a_dict"

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

    @pytest.mark.asyncio
    async def test_datasets_response_no_result_key(self):
        """Datasets response without 'result' key falls back to empty list."""
        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": []}
            if "/datasets" in endpoint:
                return {"status": "ok"}  # no "result" key
            return {}

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

    @pytest.mark.asyncio
    async def test_fallback_datasets_no_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": "t1", "schema": None,
                "changed_on": "2025-01-01",
            }
        })
        c.client.request = AsyncMock(return_value={
            "result": [
                {
                    "id": 10,
                    "slice_name": "C",
                    "form_data": '{"datasource": "99__table", "viz_type": "line"}',
                    "datasource_id": None,
                }
            ]
        })
        detail = await c.get_dashboard_detail(1)
        assert len(detail["datasets"]) > 0
        assert detail["datasets"][0]["database"] == "Unknown"

    @pytest.mark.asyncio
    async def test_detail_dataset_backfill_error(self):
        """When backfill dataset fetch fails, it is skipped."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {"id": 1, "dashboard_title": "D"}
        })
        c.get_chart = AsyncMock()
        c.get_dataset = AsyncMock(side_effect=Exception("net error"))

        # Endpoint-aware mock: charts returns chart data, datasets returns empty
        async def mock_req(method, endpoint, **kw):
            if "/charts" in endpoint:
                return {
                    "result": [
                        {
                            "id": 10,
                            "slice_name": "C",
                            "form_data": '{"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)
        # Dataset 99 fetch failed, so datasets remains empty
        assert detail["datasets"] == []
        assert detail["charts"][0]["dataset_id"] == 99

    @pytest.mark.asyncio
    async def test_empty_position_json_and_json_metadata(self):
        """Empty position_json and json_metadata string results in no fallback charts."""
        c = _make_client()
        c.get_dashboard = AsyncMock(return_value={
            "result": {
                "id": 1,
                "dashboard_title": "D",
                "position_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"] == []


class TestExportDashboardEdge:
    """Edge cases for export_dashboard."""

    @pytest.mark.asyncio
    async def test_empty_content_raises(self):
        """Export with empty content raises SupersetAPIError."""
        c = _make_client()
        resp = MagicMock(spec=httpx.Response)
        resp.headers = {"Content-Type": "application/zip"}
        resp.content = b""
        c.client.request = AsyncMock(return_value=resp)
        with pytest.raises(SupersetAPIError, match="пустые данные"):
            await c.export_dashboard(5)

    @pytest.mark.asyncio
    async def test_response_as_dict_raises_type_error(self):
        """When export returns dict instead of httpx.Response, type error may occur."""
        c = _make_client()
        c.client.request = AsyncMock(return_value={"result": "not a response"})
        with pytest.raises((TypeError, AttributeError, SupersetAPIError)):
            await c.export_dashboard(5)


class TestImportDashboardEdge:
    """Edge cases for import_dashboard."""

    @pytest.mark.asyncio
    async def test_missing_file_raises(self):
        """import_dashboard with non-existent file raises FileNotFoundError."""
        c = _make_client()
        with pytest.raises(FileNotFoundError):
            await c.import_dashboard("/tmp/nonexistent_import.zip")

    @pytest.mark.asyncio
    async def test_not_a_zip_raises(self, tmp_path):
        """import_dashboard with non-zip file raises SupersetAPIError."""
        c = _make_client()
        p = tmp_path / "not_a_zip.txt"
        p.write_text("not a zip file")
        with pytest.raises(SupersetAPIError, match="не является ZIP"):
            await c.import_dashboard(str(p))

    @pytest.mark.asyncio
    async def test_zip_without_metadata_raises(self, tmp_path):
        """Zip without metadata.yaml raises SupersetAPIError."""
        c = _make_client()
        p = tmp_path / "no_meta.zip"
        buf = BytesIO()
        with zipfile.ZipFile(buf, "w") as zf:
            zf.writestr("random_file.txt", "content")
        p.write_bytes(buf.getvalue())
        with pytest.raises(SupersetAPIError, match="не содержит"):
            await c.import_dashboard(str(p))


class TestDeleteDashboardEdge:
    """Edge cases for delete_dashboard."""

    @pytest.mark.asyncio
    async def test_response_not_dict(self):
        """delete_dashboard raises AttributeError on non-dict response (production assumption)."""
        c = _make_client()
        c.client.request = AsyncMock(return_value=["not_a_dict"])
        with pytest.raises(AttributeError):
            await c.delete_dashboard(42)

    @pytest.mark.asyncio
    async def test_response_without_result_key(self):
        """delete_dashboard handles response without 'result' key."""
        c = _make_client()
        c.client.request = AsyncMock(return_value={"status": "ok"})
        # Should not raise
        await c.delete_dashboard(42)
# #endregion Test.SupersetClient.DashboardsCrud.Edge
