# #region Test.SupersetClient.Base [C:3] [TYPE Module] [SEMANTICS test,superset,client,base]
# @BRIEF Unit tests for SupersetClientBase — init, auth, pagination, import/export helpers.
# @RELATION BINDS_TO -> [SupersetClientBase]
# @TEST_EDGE: missing_file -> _do_import raises FileNotFoundError
# @TEST_EDGE: invalid_zip -> _validate_import_file raises SupersetAPIError
# @TEST_EDGE: non_zip_export -> _validate_export_response raises SupersetAPIError
# @TEST_EDGE: empty_export -> _validate_export_response raises SupersetAPIError
# @TEST_EDGE: unknown_resource_type -> get_all_resources returns empty list
import io
import zipfile
from datetime import datetime, timezone
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from src.core.config_models import Environment
from src.core.superset_client._base import SupersetClientBase, DEFAULT_PAGE_SIZE
from src.core.utils.network import SupersetAPIError


def _make_env(**overrides) -> Environment:
    defaults = dict(id="env1", name="TestEnv", url="http://superset.test", username="admin", password="admin")
    defaults.update(overrides)
    return Environment(**defaults)


def _make_client(env=None, mock_client=None) -> SupersetClientBase:
    env = env or _make_env()
    mc = mock_client or MagicMock()
    mc.authenticate = AsyncMock(return_value={"access_token": "tok", "csrf_token": "csrf"})
    mc.get_headers = AsyncMock(return_value={"Authorization": "Bearer tok"})
    mc.fetch_paginated_count = AsyncMock(return_value=42)
    mc.fetch_paginated_data = AsyncMock(return_value=[{"id": 1}, {"id": 2}])
    mc.upload_file = AsyncMock(return_value={"status": "ok"})
    mc.aclose = AsyncMock()
    return SupersetClientBase(env, client=mc)


# #region test_init_stores_env_and_creates_client [C:2] [TYPE Function]
# @BRIEF __init__ stores env, creates AsyncAPIClient when none provided.
def test_init_stores_env_and_creates_client():
    env = _make_env()
    with patch("src.core.superset_client._base.AsyncAPIClient") as MockCls:
        mock_inst = MagicMock()
        MockCls.return_value = mock_inst
        obj = SupersetClientBase(env)
    assert obj.env is env
    assert obj.client is mock_inst
    assert obj.network is mock_inst
    assert obj.delete_before_reimport is False
    MockCls.assert_called_once()
# #endregion test_init_stores_env_and_creates_client

# #region test_init_uses_provided_client [C:2] [TYPE Function]
# @BRIEF __init__ uses injected client instead of creating a new one.
def test_init_uses_provided_client():
    env = _make_env()
    injected = MagicMock()
    obj = SupersetClientBase(env, client=injected)
    assert obj.client is injected
    assert obj.network is injected
# #endregion test_init_uses_provided_client

# #region test_authenticate_delegates_to_client [C:2] [TYPE Function]
# @BRIEF authenticate() delegates to client.authenticate() and returns tokens.
@pytest.mark.asyncio
async def test_authenticate_delegates_to_client():
    obj = _make_client()
    tokens = await obj.authenticate()
    assert tokens == {"access_token": "tok", "csrf_token": "csrf"}
    obj.client.authenticate.assert_awaited_once()
# #endregion test_authenticate_delegates_to_client

# #region test_get_headers_delegates_to_client [C:2] [TYPE Function]
# @BRIEF get_headers() delegates to client.get_headers().
@pytest.mark.asyncio
async def test_get_headers_delegates_to_client():
    obj = _make_client()
    headers = await obj.get_headers()
    assert headers == {"Authorization": "Bearer tok"}
    obj.client.get_headers.assert_awaited_once()
# #endregion test_get_headers_delegates_to_client

# #region test_validate_query_params_defaults [C:2] [TYPE Function]
# @BRIEF _validate_query_params applies default page=0 and page_size=100.
def test_validate_query_params_defaults():
    obj = _make_client()
    result = obj._validate_query_params(None)
    assert result == {"page": 0, "page_size": DEFAULT_PAGE_SIZE}
# #endregion test_validate_query_params_defaults

# #region test_validate_query_params_merge [C:2] [TYPE Function]
# @BRIEF _validate_query_params merges caller overrides over defaults.
def test_validate_query_params_merge():
    obj = _make_client()
    result = obj._validate_query_params({"page": 3, "columns": ["id"]})
    assert result["page"] == 3
    assert result["page_size"] == DEFAULT_PAGE_SIZE
    assert result["columns"] == ["id"]
# #endregion test_validate_query_params_merge

# #region test_fetch_total_object_count [C:2] [TYPE Function]
# @BRIEF _fetch_total_object_count delegates to client.fetch_paginated_count.
@pytest.mark.asyncio
async def test_fetch_total_object_count():
    obj = _make_client()
    count = await obj._fetch_total_object_count("/dashboard/")
    assert count == 42
    obj.client.fetch_paginated_count.assert_awaited_once_with(
        endpoint="/dashboard/",
        query_params={"page": 0, "page_size": 1},
        count_field="count",
    )
# #endregion test_fetch_total_object_count

# #region test_fetch_all_pages [C:2] [TYPE Function]
# @BRIEF _fetch_all_pages delegates to client.fetch_paginated_data.
@pytest.mark.asyncio
async def test_fetch_all_pages():
    obj = _make_client()
    opts = {"base_query": {"page": 0}, "results_field": "result"}
    data = await obj._fetch_all_pages("/chart/", opts)
    assert data == [{"id": 1}, {"id": 2}]
    obj.client.fetch_paginated_data.assert_awaited_once_with(
        endpoint="/chart/", pagination_options=opts
    )
# #endregion test_fetch_all_pages

# #region test_do_import_success [C:2] [TYPE Function]
# @BRIEF _do_import uploads an existing file via client.upload_file.
@pytest.mark.asyncio
async def test_do_import_success(tmp_path):
    obj = _make_client()
    fake_file = tmp_path / "export.zip"
    fake_file.write_bytes(b"PK\x03\x04fake")
    result = await obj._do_import(str(fake_file))
    assert result == {"status": "ok"}
    obj.client.upload_file.assert_awaited_once()
# #endregion test_do_import_success

# #region test_do_import_file_not_found [C:2] [TYPE Function]
# @BRIEF _do_import raises FileNotFoundError when file does not exist.
@pytest.mark.asyncio
async def test_do_import_file_not_found():
    obj = _make_client()
    with pytest.raises(FileNotFoundError, match="does_not_exist.zip"):
        await obj._do_import("/tmp/does_not_exist.zip")
# #endregion test_do_import_file_not_found

# #region test_validate_export_response_success [C:2] [TYPE Function]
# @BRIEF _validate_export_response passes for valid ZIP content-type with body.
def test_validate_export_response_success():
    obj = _make_client()
    resp = MagicMock(spec=httpx.Response)
    resp.headers = {"Content-Type": "application/zip"}
    resp.content = b"PK\x03\x04data"
    obj._validate_export_response(resp, 1)  # no exception
# #endregion test_validate_export_response_success

# #region test_validate_export_response_not_zip [C:2] [TYPE Function]
# @BRIEF _validate_export_response raises SupersetAPIError for non-ZIP content-type.
def test_validate_export_response_not_zip():
    obj = _make_client()
    resp = MagicMock(spec=httpx.Response)
    resp.headers = {"Content-Type": "application/json"}
    resp.content = b'{"error": true}'
    with pytest.raises(SupersetAPIError, match="не ZIP"):
        obj._validate_export_response(resp, 1)
# #endregion test_validate_export_response_not_zip

# #region test_validate_export_response_empty [C:2] [TYPE Function]
# @BRIEF _validate_export_response raises SupersetAPIError for empty body.
def test_validate_export_response_empty():
    obj = _make_client()
    resp = MagicMock(spec=httpx.Response)
    resp.headers = {"Content-Type": "application/zip"}
    resp.content = b""
    with pytest.raises(SupersetAPIError, match="пустые"):
        obj._validate_export_response(resp, 1)
# #endregion test_validate_export_response_empty

# #region test_resolve_export_filename_from_header [C:2] [TYPE Function]
# @BRIEF _resolve_export_filename extracts filename from Content-Disposition header.
def test_resolve_export_filename_from_header():
    obj = _make_client()
    resp = MagicMock(spec=httpx.Response)
    resp.headers = {"Content-Disposition": 'attachment; filename="my_dash.zip"'}
    fname = obj._resolve_export_filename(resp, 99)
    assert fname == "my_dash.zip"
# #endregion test_resolve_export_filename_from_header

# #region test_resolve_export_filename_fallback [C:2] [TYPE Function]
# @BRIEF _resolve_export_filename generates timestamped name when header is absent.
def test_resolve_export_filename_fallback():
    obj = _make_client()
    resp = MagicMock(spec=httpx.Response)
    resp.headers = {}
    fname = obj._resolve_export_filename(resp, 42)
    assert fname.startswith("dashboard_export_42_")
    assert fname.endswith(".zip")
# #endregion test_resolve_export_filename_fallback

# #region test_validate_import_file_valid [C:2] [TYPE Function]
# @BRIEF _validate_import_file passes for a valid ZIP containing metadata.yaml.
def test_validate_import_file_valid(tmp_path):
    obj = _make_client()
    zip_path = tmp_path / "valid.zip"
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w") as zf:
        zf.writestr("dashboard_1/metadata.yaml", "title: test")
    zip_path.write_bytes(buf.getvalue())
    obj._validate_import_file(str(zip_path))  # no exception
# #endregion test_validate_import_file_valid

# #region test_validate_import_file_not_zip [C:2] [TYPE Function]
# @BRIEF _validate_import_file raises SupersetAPIError for non-ZIP file.
def test_validate_import_file_not_zip(tmp_path):
    obj = _make_client()
    bad = tmp_path / "bad.zip"
    bad.write_bytes(b"not a zip file")
    with pytest.raises(SupersetAPIError, match="не является ZIP"):
        obj._validate_import_file(str(bad))
# #endregion test_validate_import_file_not_zip

# #region test_validate_import_file_missing_metadata [C:2] [TYPE Function]
# @BRIEF _validate_import_file raises SupersetAPIError when metadata.yaml is absent.
def test_validate_import_file_missing_metadata(tmp_path):
    obj = _make_client()
    zip_path = tmp_path / "no_meta.zip"
    buf = io.BytesIO()
    with zipfile.ZipFile(buf, "w") as zf:
        zf.writestr("dashboard_1/charts.yaml", "data: x")
    zip_path.write_bytes(buf.getvalue())
    with pytest.raises(SupersetAPIError, match="metadata.yaml"):
        obj._validate_import_file(str(zip_path))
# #endregion test_validate_import_file_missing_metadata

# #region test_validate_import_file_not_found [C:2] [TYPE Function]
# @BRIEF _validate_import_file raises FileNotFoundError for missing path.
def test_validate_import_file_not_found():
    obj = _make_client()
    with pytest.raises(FileNotFoundError):
        obj._validate_import_file("/tmp/nonexistent_file.zip")
# #endregion test_validate_import_file_not_found

# #region test_resolve_target_id_by_direct_id [C:2] [TYPE Function]
# @BRIEF _resolve_target_id_for_delete returns dash_id directly when provided.
@pytest.mark.asyncio
async def test_resolve_target_id_by_direct_id():
    obj = _make_client()
    result = await obj._resolve_target_id_for_delete(dash_id=10, dash_slug=None)
    assert result == 10
# #endregion test_resolve_target_id_by_direct_id

# #region test_resolve_target_id_by_slug [C:2] [TYPE Function]
# @BRIEF _resolve_target_id_for_delete resolves slug via get_dashboards.
@pytest.mark.asyncio
async def test_resolve_target_id_by_slug():
    obj = _make_client()
    obj.get_dashboards = AsyncMock(return_value=(1, [{"id": 77}]))
    result = await obj._resolve_target_id_for_delete(dash_id=None, dash_slug="my-slug")
    assert result == 77
# #endregion test_resolve_target_id_by_slug

# #region test_resolve_target_id_slug_not_found [C:2] [TYPE Function]
# @BRIEF _resolve_target_id_for_delete returns None when slug resolves to nothing.
@pytest.mark.asyncio
async def test_resolve_target_id_slug_not_found():
    obj = _make_client()
    obj.get_dashboards = AsyncMock(return_value=(0, []))
    result = await obj._resolve_target_id_for_delete(dash_id=None, dash_slug="nope")
    assert result is None
# #endregion test_resolve_target_id_slug_not_found

# #region test_resolve_target_id_slug_exception [C:2] [TYPE Function]
# @BRIEF _resolve_target_id_for_delete returns None when slug lookup raises.
@pytest.mark.asyncio
async def test_resolve_target_id_slug_exception():
    obj = _make_client()
    obj.get_dashboards = AsyncMock(side_effect=RuntimeError("network down"))
    result = await obj._resolve_target_id_for_delete(dash_id=None, dash_slug="oops")
    assert result is None
# #endregion test_resolve_target_id_slug_exception

# #region test_resolve_target_id_both_none [C:2] [TYPE Function]
# @BRIEF _resolve_target_id_for_delete returns None when both args are None.
@pytest.mark.asyncio
async def test_resolve_target_id_both_none():
    obj = _make_client()
    result = await obj._resolve_target_id_for_delete(dash_id=None, dash_slug=None)
    assert result is None
# #endregion test_resolve_target_id_both_none

# #region test_get_all_resources_chart [C:2] [TYPE Function]
# @BRIEF get_all_resources fetches chart resources with correct columns.
@pytest.mark.asyncio
async def test_get_all_resources_chart():
    obj = _make_client()
    data = await obj.get_all_resources("chart")
    assert len(data) == 2
    call_kwargs = obj.client.fetch_paginated_data.call_args
    assert call_kwargs.kwargs["endpoint"] == "/chart/"
    base_query = call_kwargs.kwargs["pagination_options"]["base_query"]
    assert "id" in base_query["columns"]
    assert "uuid" in base_query["columns"]
# #endregion test_get_all_resources_chart

# #region test_get_all_resources_unknown_type [C:2] [TYPE Function]
# @BRIEF get_all_resources returns empty list for unknown resource type.
@pytest.mark.asyncio
async def test_get_all_resources_unknown_type():
    obj = _make_client()
    data = await obj.get_all_resources("unknown_type")
    assert data == []
# #endregion test_get_all_resources_unknown_type

# #region test_get_all_resources_with_since [C:2] [TYPE Function]
# @BRIEF get_all_resources adds timestamp filter when since_dttm is provided.
@pytest.mark.asyncio
async def test_get_all_resources_with_since():
    obj = _make_client()
    since = datetime(2025, 1, 1, tzinfo=timezone.utc)
    await obj.get_all_resources("dashboard", since_dttm=since)
    call_kwargs = obj.client.fetch_paginated_data.call_args
    base_query = call_kwargs.kwargs["pagination_options"]["base_query"]
    assert "filters" in base_query
    assert base_query["filters"][0]["col"] == "changed_on_dttm"
# #endregion test_get_all_resources_with_since

# #region test_aclose_delegates [C:2] [TYPE Function]
# @BRIEF aclose() delegates to client.aclose().
@pytest.mark.asyncio
async def test_aclose_delegates():
    obj = _make_client()
    await obj.aclose()
    obj.client.aclose.assert_awaited_once()
# #endregion test_aclose_delegates

# #endregion Test.SupersetClient.Base
