# #region Test.SupersetClient.Databases [C:3] [TYPE Module] [SEMANTICS test,superset,database,crud]
# @BRIEF Unit tests for SupersetDatabasesMixin — list, get, summary, by_uuid, create, delete.
# @RELATION BINDS_TO -> [SupersetDatabasesMixin]
# @TEST_EDGE: databases_empty -> get_databases returns (0, [])
# @TEST_EDGE: database_not_found -> get_database returns empty dict
# @TEST_EDGE: database_by_uuid_not_found -> get_database_by_uuid returns None
# @TEST_EDGE: create_database -> POST request with correct payload
# @TEST_EDGE: delete_database -> DELETE request with correct endpoint
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.fetch_paginated_count = AsyncMock(return_value=0)
    mc.aclose = AsyncMock()
    client.client = mc
    client.network = mc
    return client


# ── get_databases ─────────────────────────────────────────────────────────────


class TestGetDatabases:
    """get_databases — paginated database listing."""

    @pytest.mark.asyncio
    async def test_success(self):
        """Happy path: returns (count, data) from paginated fetch."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[
            {"id": 1, "database_name": "Main", "backend": "postgresql"},
            {"id": 2, "database_name": "Analytics", "backend": "mysql"},
        ])
        count, data = await client.get_databases()
        assert count == 2
        assert len(data) == 2
        assert data[0]["database_name"] == "Main"

    @pytest.mark.asyncio
    async def test_empty(self):
        """Returns (0, []) when no databases exist."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[])
        count, data = await client.get_databases()
        assert count == 0
        assert data == []

    @pytest.mark.asyncio
    async def test_passes_query_params(self):
        """Custom query columns are passed through."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[{"id": 1}])
        await client.get_databases(query={"columns": ["id", "database_name"]})
        call_kwargs = client.client.fetch_paginated_data.call_args
        base_query = call_kwargs.kwargs["pagination_options"]["base_query"]
        assert base_query["columns"] == ["id", "database_name"]

    @pytest.mark.asyncio
    async def test_defaults_columns_to_empty(self):
        """When no columns in query, defaults to empty list."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[{"id": 1}])
        await client.get_databases()
        call_kwargs = client.client.fetch_paginated_data.call_args
        base_query = call_kwargs.kwargs["pagination_options"]["base_query"]
        assert base_query["columns"] == []

    @pytest.mark.asyncio
    async def test_uses_correct_endpoint(self):
        """Calls _fetch_all_pages with /database/ endpoint."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[])
        await client.get_databases()
        call_kwargs = client.client.fetch_paginated_data.call_args
        assert call_kwargs.kwargs["endpoint"] == "/database/"

    @pytest.mark.asyncio
    async def test_results_field(self):
        """Uses 'result' as the results_field."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[])
        await client.get_databases()
        call_kwargs = client.client.fetch_paginated_data.call_args
        assert call_kwargs.kwargs["pagination_options"]["results_field"] == "result"


# ── get_database ──────────────────────────────────────────────────────────────


class TestGetDatabase:
    """get_database — single database by ID."""

    @pytest.mark.asyncio
    async def test_success(self):
        """Returns database dict for valid ID."""
        client = _make_client()
        expected = {"id": 1, "database_name": "Main", "backend": "postgresql"}
        client.client.request = AsyncMock(return_value=expected)
        result = await client.get_database(1)
        assert result == expected
        assert result["database_name"] == "Main"

    @pytest.mark.asyncio
    async def test_uses_correct_endpoint(self):
        """Calls GET /database/<id>."""
        client = _make_client()
        client.client.request = AsyncMock(return_value={})
        await client.get_database(42)
        client.client.request.assert_awaited_once_with(
            method="GET", endpoint="/database/42"
        )

    @pytest.mark.asyncio
    async def test_returns_empty_for_not_found(self):
        """Returns empty dict when database not found (Superset API behavior)."""
        client = _make_client()
        client.client.request = AsyncMock(return_value={})
        result = await client.get_database(999)
        assert result == {}

    @pytest.mark.asyncio
    async def test_handles_non_dict_response(self):
        """Coerces non-dict response via cast (may retain type)."""
        client = _make_client()
        client.client.request = AsyncMock(return_value="not_a_dict")
        result = await client.get_database(1)
        # cast(dict, ...) doesn't convert at runtime; this documents behavior
        assert result == "not_a_dict"


# ── get_databases_summary ─────────────────────────────────────────────────────


class TestGetDatabasesSummary:
    """get_databases_summary — summary with renamed engine field."""

    @pytest.mark.asyncio
    async def test_success(self):
        """Renames 'backend' to 'engine' and returns list."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[
            {"id": 1, "uuid": "abc-123", "database_name": "Main", "backend": "postgresql"},
        ])
        result = await client.get_databases_summary()
        assert len(result) == 1
        assert result[0]["engine"] == "postgresql"
        assert "backend" not in result[0]

    @pytest.mark.asyncio
    async def test_empty_list(self):
        """Returns empty list when no databases."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[])
        result = await client.get_databases_summary()
        assert result == []

    @pytest.mark.asyncio
    async def test_missing_backend_field(self):
        """Handles databases without 'backend' key gracefully."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[
            {"id": 1, "uuid": "abc-123", "database_name": "Main"},
        ])
        result = await client.get_databases_summary()
        assert len(result) == 1
        # pop("backend", None) returns None, so engine is None
        assert result[0].get("engine") is None

    @pytest.mark.asyncio
    async def test_passes_correct_columns(self):
        """Passes id, uuid, database_name, backend as columns."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[])
        await client.get_databases_summary()
        call_kwargs = client.client.fetch_paginated_data.call_args
        base_query = call_kwargs.kwargs["pagination_options"]["base_query"]
        assert base_query["columns"] == ["id", "uuid", "database_name", "backend"]


# ── get_database_by_uuid ──────────────────────────────────────────────────────


class TestGetDatabaseByUuid:
    """get_database_by_uuid — find database by UUID."""

    @pytest.mark.asyncio
    async def test_found(self):
        """Returns database dict when UUID matches."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[
            {"id": 1, "uuid": "abc-123", "database_name": "Main"},
        ])
        result = await client.get_database_by_uuid("abc-123")
        assert result is not None
        assert result["id"] == 1

    @pytest.mark.asyncio
    async def test_not_found(self):
        """Returns None when UUID does not match any database."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[])
        result = await client.get_database_by_uuid("nonexistent-uuid")
        assert result is None

    @pytest.mark.asyncio
    async def test_multiple_results_returns_first(self):
        """Returns first result when multiple databases match."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[
            {"id": 1, "uuid": "dup-uuid", "database_name": "First"},
            {"id": 2, "uuid": "dup-uuid", "database_name": "Second"},
        ])
        result = await client.get_database_by_uuid("dup-uuid")
        assert result is not None
        assert result["id"] == 1
        assert result["database_name"] == "First"

    @pytest.mark.asyncio
    async def test_passes_uuid_filter(self):
        """Passes correct UUID filter to get_databases."""
        client = _make_client()
        client.client.fetch_paginated_data = AsyncMock(return_value=[])
        await client.get_database_by_uuid("some-uuid-123")
        call_kwargs = client.client.fetch_paginated_data.call_args
        base_query = call_kwargs.kwargs["pagination_options"]["base_query"]
        assert base_query["filters"] == [
            {"col": "uuid", "op": "eq", "value": "some-uuid-123"}
        ]


# ── create_database ───────────────────────────────────────────────────────────


class TestCreateDatabase:
    """create_database — register new database in Superset."""

    @pytest.mark.asyncio
    async def test_success(self):
        """POST to /database/ with correct payload returns created database."""
        client = _make_client()
        expected = {"id": 100, "database_name": "TestDB"}
        client.client.request = AsyncMock(return_value=expected)
        result = await client.create_database(
            database_name="TestDB",
            sqlalchemy_uri="postgresql://localhost:5432/test",
            expose_in_sqllab=True,
            allow_dml=False,
        )
        assert result["id"] == 100
        assert result["database_name"] == "TestDB"

    @pytest.mark.asyncio
    async def test_default_params(self):
        """Defaults expose_in_sqllab=True, allow_dml=False."""
        client = _make_client()
        client.client.request = AsyncMock(return_value={"id": 1})
        await client.create_database(
            database_name="DefaultDB",
            sqlalchemy_uri="postgresql://localhost:5432/default",
        )
        client.client.request.assert_awaited_once()
        call_kwargs = client.client.request.call_args
        data = call_kwargs.kwargs["data"]
        assert data["expose_in_sqllab"] is True
        assert data["allow_dml"] is False

    @pytest.mark.asyncio
    async def test_correct_endpoint(self):
        """Uses POST to /database/."""
        client = _make_client()
        client.client.request = AsyncMock(return_value={"id": 1})
        await client.create_database(
            database_name="Test",
            sqlalchemy_uri="postgresql://localhost/test",
        )
        client.client.request.assert_awaited_once_with(
            method="POST",
            endpoint="/database/",
            data={
                "database_name": "Test",
                "sqlalchemy_uri": "postgresql://localhost/test",
                "expose_in_sqllab": True,
                "allow_dml": False,
            },
        )

    @pytest.mark.asyncio
    async def test_with_dml_enabled(self):
        """Can create database with DML allowed."""
        client = _make_client()
        client.client.request = AsyncMock(return_value={"id": 1})
        await client.create_database(
            database_name="DMLDB",
            sqlalchemy_uri="postgresql://localhost/dml",
            allow_dml=True,
        )
        call_kwargs = client.client.request.call_args
        assert call_kwargs.kwargs["data"]["allow_dml"] is True

    @pytest.mark.asyncio
    async def test_not_exposed_in_sqllab(self):
        """Can create database not exposed in SQL Lab."""
        client = _make_client()
        client.client.request = AsyncMock(return_value={"id": 1})
        await client.create_database(
            database_name="NoSQL",
            sqlalchemy_uri="postgresql://localhost/nosql",
            expose_in_sqllab=False,
        )
        call_kwargs = client.client.request.call_args
        assert call_kwargs.kwargs["data"]["expose_in_sqllab"] is False


# ── delete_database ───────────────────────────────────────────────────────────


class TestDeleteDatabase:
    """delete_database — remove database from Superset."""

    @pytest.mark.asyncio
    async def test_success(self):
        """DELETE to /database/<id> returns response."""
        client = _make_client()
        expected = {"message": "Deleted successfully"}
        client.client.request = AsyncMock(return_value=expected)
        result = await client.delete_database(42)
        assert result == expected

    @pytest.mark.asyncio
    async def test_correct_endpoint(self):
        """Uses DELETE to /database/<id>."""
        client = _make_client()
        client.client.request = AsyncMock(return_value={})
        await client.delete_database(99)
        client.client.request.assert_awaited_once_with(
            method="DELETE",
            endpoint="/database/99",
        )

    @pytest.mark.asyncio
    async def test_successful_delete_returns_dict(self):
        """Returns dict response."""
        client = _make_client()
        client.client.request = AsyncMock(return_value={"result": True})
        result = await client.delete_database(1)
        assert isinstance(result, dict)
        assert result.get("result") is True


# ── Integration-style: combined operations ────────────────────────────────────


class TestDatabaseWorkflow:
    """Combined workflow: create, list, find by uuid, delete."""

    @pytest.mark.asyncio
    async def test_create_then_list(self):
        """After creating, the new database appears in listing."""
        client = _make_client()

        # Mock create
        client.client.request = AsyncMock(return_value={
            "id": 100,
            "database_name": "NewDB",
            "uuid": "new-uuid-456",
            "backend": "postgresql",
        })

        # Mock list
        client.client.fetch_paginated_data = AsyncMock(return_value=[
            {"id": 1, "database_name": "Existing"},
            {"id": 100, "database_name": "NewDB"},
        ])

        created = await client.create_database(
            database_name="NewDB",
            sqlalchemy_uri="postgresql://localhost/newdb",
        )
        assert created["id"] == 100

        count, databases = await client.get_databases()
        assert count == 2
        names = [d["database_name"] for d in databases]
        assert "NewDB" in names

    @pytest.mark.asyncio
    async def test_create_delete_not_listed(self):
        """After creating and deleting, database is gone."""
        client = _make_client()
        call_log = []

        async def mock_fetch_paginated_data(**kw):
            call_log.append(("fetch", kw.get("endpoint", "")))
            if len(call_log) == 1:
                return [{"id": 100, "database_name": "Temp"}]
            return []

        client.client.fetch_paginated_data = mock_fetch_paginated_data

        # Step 1: list shows the database
        count1, _ = await client.get_databases()
        assert count1 == 1

        # Step 2: delete
        client.client.request = AsyncMock(return_value={"message": "ok"})
        await client.delete_database(100)

        # Step 3: list again shows empty
        count2, _ = await client.get_databases()
        assert count2 == 0
# #endregion Test.SupersetClient.Databases
