# #region Test.SupersetClient.DashboardsCrud [C:3] [TYPE Module] [SEMANTICS test,superset,dashboard,crud]
# @BRIEF Unit tests for dashboard CRUD mixin — detail, export, import, delete.
# @RELATION BINDS_TO -> [SupersetDashboardsCrudMixin]
# @TEST_EDGE: charts_fetch_fails -> get_dashboard_detail falls back to layout extraction
# @TEST_EDGE: import_fails_no_delete -> import_dashboard re-raises when delete_before_reimport is False
# @TEST_EDGE: import_fails_retry -> import_dashboard deletes then retries when delete_before_reimport is True
import io, json, zipfile
from unittest.mock import AsyncMock, MagicMock
import httpx, 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 _detail_mock(client, charts_resp, datasets_resp):
    async def mock_req(method, endpoint, **kw):
        if "/charts" in endpoint: return charts_resp
        if "/datasets" in endpoint: return datasets_resp
        return {}
    client.client.request = AsyncMock(side_effect=mock_req)

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

# ── get_dashboard_detail ────────────────────────────────────────────────────
# #region test_detail_success [C:2] [TYPE Function]
# @BRIEF Happy path: dashboard with charts and datasets from API responses.
@pytest.mark.asyncio
async def test_detail_success():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {
        "id": 10, "dashboard_title": "My Dash", "slug": "my-dash", "url": "/dash/10",
        "description": "desc", "published": True, "changed_on_utc": "2025-01-01",
        "position_json": "", "json_metadata": "",
    }})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c,
        charts_resp={"result": [{"id": 100, "slice_name": "Chart A",
            "form_data": '{"viz_type":"table","datasource":"42__table"}',
            "datasource_id": None, "changed_on": "2025-01-02", "description": "A chart"}]},
        datasets_resp={"result": [{"id": 42, "table_name": "orders", "schema": "public",
            "database": {"database_name": "pg_main"}, "changed_on": "2025-01-03"}]})
    r = await c.get_dashboard_detail(10)
    assert r["id"] == 10 and r["title"] == "My Dash"
    assert r["chart_count"] == 1 and r["charts"][0]["dataset_id"] == 42
    assert r["dataset_count"] == 1 and r["datasets"][0]["table_name"] == "orders"
# #endregion test_detail_success

# #region test_detail_charts_fail_fallback [C:2] [TYPE Function]
# @BRIEF When charts endpoint fails, falls back to layout-based chart extraction.
@pytest.mark.asyncio
async def test_detail_charts_fail_fallback():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {
        "id": 20, "dashboard_title": "FB", "position_json": json.dumps({"CHART-5": {"chartId": 5}, "CHART-9": {"chartId": 9}}),
        "json_metadata": "", "changed_on_utc": "2025-01-01"}})
    c.get_chart = AsyncMock(side_effect=[
        {"result": {"id": 5, "slice_name": "C5", "viz_type": "bar", "datasource_id": 1, "changed_on": "t", "description": "d"}},
        {"result": {"id": 9, "slice_name": "C9", "viz_type": "line", "datasource_id": 2, "changed_on": "t", "description": "d"}}])
    c.get_dataset = AsyncMock(return_value={"result": {"id": 1, "table_name": "t1", "schema": None, "database": {"database_name": "db"}, "changed_on": "t"}})
    c.client.request = AsyncMock(side_effect=RuntimeError("net err"))
    r = await c.get_dashboard_detail(20)
    assert {ch["id"] for ch in r["charts"]} == {5, 9}
# #endregion test_detail_charts_fail_fallback

# #region test_detail_empty [C:2] [TYPE Function]
# @BRIEF Dashboard with no charts/datasets returns empty lists.
@pytest.mark.asyncio
async def test_detail_empty():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 30, "dashboard_title": "E", "position_json": "", "json_metadata": ""}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    c.client.request = AsyncMock(return_value={"result": []})
    r = await c.get_dashboard_detail(30)
    assert r["charts"] == [] and r["datasets"] == [] and r["chart_count"] == 0
# #endregion test_detail_empty

# #region test_detail_backfill_datasets [C:2] [TYPE Function]
# @BRIEF Datasets referenced by charts but missing from datasets endpoint are backfilled.
@pytest.mark.asyncio
async def test_detail_backfill_datasets():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 40, "dashboard_title": "BF"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock(return_value={"result": {
        "id": 99, "table_name": "backfilled", "schema": "s", "database": {"database_name": "db"}, "changed_on_utc": "t"}})
    _detail_mock(c, charts_resp={"result": [{"id": 1, "slice_name": "C1", "form_data": {"datasource": {"id": 99}}, "datasource_id": 99}]},
        datasets_resp={"result": []})
    r = await c.get_dashboard_detail(40)
    assert any(d["id"] == 99 for d in r["datasets"])
# #endregion test_detail_backfill_datasets

# ── extract_dataset_id edge cases ───────────────────────────────────────────
# #region test_detail_form_data_not_dict [C:2] [TYPE Function]
# @BRIEF Non-dict form_data yields dataset_id=None.
@pytest.mark.asyncio
async def test_detail_form_data_not_dict():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 50, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": [{"id": 10, "slice_name": "C", "form_data": None, "datasource_id": None}]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(50))["charts"][0]["dataset_id"] is None
# #endregion test_detail_form_data_not_dict

# #region test_detail_form_data_dict_datasource [C:2] [TYPE Function]
# @BRIEF Dict datasource extracts id correctly.
@pytest.mark.asyncio
async def test_detail_form_data_dict_datasource():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 51, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": [{"id": 11, "slice_name": "C", "form_data": {"datasource": {"id": 77}}, "datasource_id": None}]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(51))["charts"][0]["dataset_id"] == 77
# #endregion test_detail_form_data_dict_datasource

# #region test_detail_form_data_datasource_id_fallback [C:2] [TYPE Function]
# @BRIEF datasource_id field (no datasource key) extracts id.
@pytest.mark.asyncio
async def test_detail_form_data_datasource_id_fallback():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 52, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": [{"id": 12, "slice_name": "C", "form_data": {"datasource_id": 88}, "datasource_id": None}]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(52))["charts"][0]["dataset_id"] == 88
# #endregion test_detail_form_data_datasource_id_fallback

# #region test_detail_chart_obj_not_dict [C:2] [TYPE Function]
# @BRIEF Non-dict chart entries are skipped.
@pytest.mark.asyncio
async def test_detail_chart_obj_not_dict():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 53, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": ["not_a_dict", 42, None]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(53))["charts"] == []
# #endregion test_detail_chart_obj_not_dict

# #region test_detail_chart_id_none [C:2] [TYPE Function]
# @BRIEF Chart entries with no id field are skipped.
@pytest.mark.asyncio
async def test_detail_chart_id_none():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 54, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": [{"slice_name": "NoID"}]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(54))["charts"] == []
# #endregion test_detail_chart_id_none

# #region test_detail_form_data_bad_json [C:2] [TYPE Function]
# @BRIEF Invalid JSON form_data falls back to empty dict.
@pytest.mark.asyncio
async def test_detail_form_data_bad_json():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 55, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": [{"id": 15, "slice_name": "C", "form_data": "{bad", "datasource_id": 5}]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(55))["charts"][0]["dataset_id"] == 5
# #endregion test_detail_form_data_bad_json

# #region test_detail_dataset_obj_not_dict [C:2] [TYPE Function]
# @BRIEF Non-dict dataset entries are skipped.
@pytest.mark.asyncio
async def test_detail_dataset_obj_not_dict():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 56, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    c.client.request = AsyncMock(return_value={"result": ["not_dict"]})
    assert (await c.get_dashboard_detail(56))["datasets"] == []
# #endregion test_detail_dataset_obj_not_dict

# #region test_detail_dataset_id_none [C:2] [TYPE Function]
# @BRIEF Dataset entries with no id field are skipped.
@pytest.mark.asyncio
async def test_detail_dataset_id_none():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 57, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    c.client.request = AsyncMock(return_value={"result": [{"table_name": "no_id"}]})
    assert (await c.get_dashboard_detail(57))["datasets"] == []
# #endregion test_detail_dataset_id_none

# ── Layout fallback edge cases ──────────────────────────────────────────────
# #region test_detail_position_json_dict [C:2] [TYPE Function]
# @BRIEF Fallback extracts chart IDs when position_json is a dict.
@pytest.mark.asyncio
async def test_detail_position_json_dict():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 58, "dashboard_title": "D", "position_json": {"CHART-3": {"chartId": 3}}, "json_metadata": ""}})
    c.get_chart = AsyncMock(return_value={"result": {"id": 3, "slice_name": "C3", "viz_type": "t", "datasource_id": None, "changed_on": "t", "description": "d"}})
    c.get_dataset = AsyncMock(); c.client.request = AsyncMock(return_value={"result": []})
    assert any(ch["id"] == 3 for ch in (await c.get_dashboard_detail(58))["charts"])
# #endregion test_detail_position_json_dict

# #region test_detail_json_metadata_str [C:2] [TYPE Function]
# @BRIEF Fallback extracts chart IDs from json_metadata string.
@pytest.mark.asyncio
async def test_detail_json_metadata_str():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 59, "dashboard_title": "D", "position_json": "", "json_metadata": '{"charts": [{"chartId": 8}]}'}})
    c.get_chart = AsyncMock(return_value={"result": {"id": 8, "slice_name": "C8", "viz_type": "b", "datasource_id": None, "changed_on": "t", "description": "d"}})
    c.get_dataset = AsyncMock(); c.client.request = AsyncMock(return_value={"result": []})
    assert any(ch["id"] == 8 for ch in (await c.get_dashboard_detail(59))["charts"])
# #endregion test_detail_json_metadata_str

# #region test_detail_json_metadata_dict [C:2] [TYPE Function]
# @BRIEF json_metadata as dict is traversed for chart IDs.
@pytest.mark.asyncio
async def test_detail_json_metadata_dict():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 62, "dashboard_title": "D", "position_json": "", "json_metadata": {"charts": [{"chartId": 11}]}}})
    c.get_chart = AsyncMock(return_value={"result": {"id": 11, "slice_name": "C11", "viz_type": "x", "datasource_id": None, "changed_on": "t", "description": "d"}})
    c.get_dataset = AsyncMock(); c.client.request = AsyncMock(return_value={"result": []})
    assert any(ch["id"] == 11 for ch in (await c.get_dashboard_detail(62))["charts"])
# #endregion test_detail_json_metadata_dict

# #region test_detail_position_json_bad_string [C:2] [TYPE Function]
# @BRIEF Invalid position_json string handled gracefully.
@pytest.mark.asyncio
async def test_detail_position_json_bad_string():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 61, "dashboard_title": "D", "position_json": "{bad!!", "json_metadata": ""}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock(); c.client.request = AsyncMock(return_value={"result": []})
    assert (await c.get_dashboard_detail(61))["charts"] == []
# #endregion test_detail_position_json_bad_string

# #region test_detail_json_metadata_bad_string [C:2] [TYPE Function]
# @BRIEF Invalid json_metadata string handled gracefully.
@pytest.mark.asyncio
async def test_detail_json_metadata_bad_string():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 63, "dashboard_title": "D", "position_json": "", "json_metadata": "not json{{{"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock(); c.client.request = AsyncMock(return_value={"result": []})
    assert (await c.get_dashboard_detail(63))["charts"] == []
# #endregion test_detail_json_metadata_bad_string

# #region test_detail_fallback_chart_error [C:2] [TYPE Function]
# @BRIEF Fallback chart resolution continues on error.
@pytest.mark.asyncio
async def test_detail_fallback_chart_error():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 60, "dashboard_title": "D", "position_json": {"CHART-99": {"chartId": 99}}, "json_metadata": ""}})
    c.get_chart = AsyncMock(side_effect=RuntimeError("fail")); c.get_dataset = AsyncMock()
    c.client.request = AsyncMock(return_value={"result": []})
    assert (await c.get_dashboard_detail(60))["charts"] == []
# #endregion test_detail_fallback_chart_error

# ── Type error edge cases ───────────────────────────────────────────────────
# #region test_detail_dict_datasource_type_error [C:2] [TYPE Function]
# @BRIEF Dict datasource with non-convertible id → dataset_id=None.
@pytest.mark.asyncio
async def test_detail_dict_datasource_type_error():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 64, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": [{"id": 20, "slice_name": "C", "form_data": {"datasource": {"id": [1, 2]}}, "datasource_id": None}]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(64))["charts"][0]["dataset_id"] is None
# #endregion test_detail_dict_datasource_type_error

# #region test_detail_datasource_id_type_error [C:2] [TYPE Function]
# @BRIEF datasource_id that can't convert to int → dataset_id=None.
@pytest.mark.asyncio
async def test_detail_datasource_id_type_error():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 65, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": [{"id": 21, "slice_name": "C", "form_data": {"datasource_id": [1]}, "datasource_id": None}]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(65))["charts"][0]["dataset_id"] is None
# #endregion test_detail_datasource_id_type_error

# #region test_detail_backfill_dataset_type_error [C:2] [TYPE Function]
# @BRIEF Non-int dataset_id from charts skipped in backfill.
@pytest.mark.asyncio
async def test_detail_backfill_dataset_type_error():
    c = _make_client()
    c.get_dashboard = AsyncMock(return_value={"result": {"id": 66, "dashboard_title": "D"}})
    c.get_chart = AsyncMock(); c.get_dataset = AsyncMock()
    _detail_mock(c, charts_resp={"result": [{"id": 22, "slice_name": "C", "form_data": {}, "datasource_id": "not_int"}]}, datasets_resp={"result": []})
    assert (await c.get_dashboard_detail(66))["datasets"] == []
# #endregion test_detail_backfill_dataset_type_error

# ── export_dashboard ────────────────────────────────────────────────────────
# #region test_export_dashboard_success [C:2] [TYPE Function]
# @BRIEF export_dashboard returns (content, filename) for valid ZIP.
@pytest.mark.asyncio
async def test_export_dashboard_success():
    c = _make_client()
    resp = MagicMock(spec=httpx.Response)
    resp.headers = {"Content-Type": "application/zip", "Content-Disposition": 'attachment; filename="d.zip"'}
    resp.content = b"PK\x03\x04data"
    c.client.request = AsyncMock(return_value=resp)
    content, fname = await c.export_dashboard(5)
    assert content == b"PK\x03\x04data" and fname == "d.zip"
# #endregion test_export_dashboard_success

# #region test_export_dashboard_invalid [C:2] [TYPE Function]
# @BRIEF export_dashboard raises SupersetAPIError for non-ZIP.
@pytest.mark.asyncio
async def test_export_dashboard_invalid():
    c = _make_client()
    resp = MagicMock(spec=httpx.Response)
    resp.headers = {"Content-Type": "text/html"}; resp.content = b"<html>"
    c.client.request = AsyncMock(return_value=resp)
    with pytest.raises(SupersetAPIError):
        await c.export_dashboard(5)
# #endregion test_export_dashboard_invalid

# ── import_dashboard ────────────────────────────────────────────────────────
# #region test_import_dashboard_success [C:2] [TYPE Function]
# @BRIEF import_dashboard validates file and delegates to _do_import.
@pytest.mark.asyncio
async def test_import_dashboard_success(tmp_path):
    c = _make_client()
    zp = _zip_with_metadata(tmp_path)
    c.client.upload_file = AsyncMock(return_value={"status": "ok"})
    assert await c.import_dashboard(str(zp)) == {"status": "ok"}
# #endregion test_import_dashboard_success

# #region test_import_dashboard_none [C:2] [TYPE Function]
# @BRIEF import_dashboard raises ValueError when file_name is None.
@pytest.mark.asyncio
async def test_import_dashboard_none():
    c = _make_client()
    with pytest.raises(ValueError, match="file_name cannot be None"):
        await c.import_dashboard(None)
# #endregion test_import_dashboard_none

# #region test_import_retry_with_delete [C:2] [TYPE Function]
# @BRIEF import_dashboard deletes and retries when delete_before_reimport=True.
@pytest.mark.asyncio
async def test_import_retry_with_delete(tmp_path):
    c = _make_client(); c.delete_before_reimport = True
    zp = _zip_with_metadata(tmp_path, "retry.zip")
    c.client.upload_file = AsyncMock(side_effect=[RuntimeError("conflict"), {"status": "ok"}])
    c.delete_dashboard = AsyncMock(); c.get_dashboards = AsyncMock(return_value=(1, [{"id": 55}]))
    assert await c.import_dashboard(str(zp), dash_slug="s") == {"status": "ok"}
    c.delete_dashboard.assert_awaited_once_with(55)
# #endregion test_import_retry_with_delete

# #region test_import_no_retry [C:2] [TYPE Function]
# @BRIEF import_dashboard re-raises when delete_before_reimport=False.
@pytest.mark.asyncio
async def test_import_no_retry(tmp_path):
    c = _make_client(); c.delete_before_reimport = False
    zp = _zip_with_metadata(tmp_path, "nr.zip")
    c.client.upload_file = AsyncMock(side_effect=RuntimeError("fail"))
    with pytest.raises(RuntimeError, match="fail"):
        await c.import_dashboard(str(zp))
# #endregion test_import_no_retry

# #region test_import_retry_no_target [C:2] [TYPE Function]
# @BRIEF import_dashboard re-raises when retry enabled but no target ID found.
@pytest.mark.asyncio
async def test_import_retry_no_target(tmp_path):
    c = _make_client(); c.delete_before_reimport = True
    zp = _zip_with_metadata(tmp_path, "nt.zip")
    c.client.upload_file = AsyncMock(side_effect=RuntimeError("conflict"))
    c.get_dashboards = AsyncMock(return_value=(0, []))
    with pytest.raises(RuntimeError, match="conflict"):
        await c.import_dashboard(str(zp), dash_slug="missing")
# #endregion test_import_retry_no_target

# ── delete_dashboard ────────────────────────────────────────────────────────
# #region test_delete_dashboard_success [C:2] [TYPE Function]
# @BRIEF delete_dashboard sends DELETE for given ID.
@pytest.mark.asyncio
async def test_delete_dashboard_success():
    c = _make_client()
    c.client.request = AsyncMock(return_value={"result": True})
    await c.delete_dashboard(42)
    c.client.request.assert_awaited_once_with(method="DELETE", endpoint="/dashboard/42")
# #endregion test_delete_dashboard_success

# #region test_delete_dashboard_unexpected [C:2] [TYPE Function]
# @BRIEF delete_dashboard handles result=False without raising.
@pytest.mark.asyncio
async def test_delete_dashboard_unexpected():
    c = _make_client()
    c.client.request = AsyncMock(return_value={"result": False})
    await c.delete_dashboard(99)  # no exception
# #endregion test_delete_dashboard_unexpected

# #endregion Test.SupersetClient.DashboardsCrud
