# #region Test.DatasourceMetadataService [C:3] [TYPE Module] [SEMANTICS test,datasource,metadata]
# @BRIEF Tests for service_datasource.py — datasource metadata fetching.
# @RELATION BINDS_TO -> [DatasourceMetadataService]
# @TEST_EDGE: empty_backend -> ValueError
# @TEST_EDGE: unsupported_dialect -> ValueError
# @TEST_EDGE: no_env_config -> ValueError

import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent / "src"))

import pytest
from unittest.mock import AsyncMock, MagicMock, patch

from src.plugins.translate.service_datasource import (
    get_dialect_from_database,
    detect_virtual_columns,
    get_datasource_columns,
    fetch_datasource_metadata,
    SUPPORTED_DIALECTS,
)


class TestGetDialectFromDatabase:
    """get_dialect_from_database — extract dialect from Superset DB record."""

    def test_backend_key(self):
        """Uses 'backend' key."""
        result = get_dialect_from_database({"backend": "postgresql"})
        assert result == "postgresql"

    def test_engine_fallback(self):
        """Falls back to 'engine' when 'backend' missing."""
        result = get_dialect_from_database({"engine": "mysql"})
        assert result == "mysql"

    def test_empty_raises(self):
        """Empty backend/engine raises ValueError."""
        with pytest.raises(ValueError, match="Could not determine"):
            get_dialect_from_database({})

    def test_dialect_mapping(self):
        """Dialect map normalizes known backends."""
        assert get_dialect_from_database({"backend": "greenplum"}) == "postgresql"
        assert get_dialect_from_database({"backend": "clickhousedb"}) == "clickhouse"

    def test_unsupported_dialect_raises(self):
        """Unsupported dialect raises ValueError."""
        with pytest.raises(ValueError, match="Unsupported database dialect"):
            get_dialect_from_database({"backend": "mongodb"})

    def test_case_insensitive(self):
        """Backend is case-insensitive."""
        result = get_dialect_from_database({"backend": "PostgreSQL"})
        assert result == "postgresql"


class TestDetectVirtualColumns:
    """detect_virtual_columns — identify computed columns."""

    def test_detects_virtual(self):
        """Returns names of non-physical columns."""
        columns = [
            {"name": "id", "is_physical": True},
            {"name": "full_name", "is_physical": False},
            {"name": "email", "is_physical": True},
        ]
        virtual = detect_virtual_columns(columns)
        assert virtual == ["full_name"]

    def test_all_physical(self):
        """All physical returns empty list."""
        columns = [
            {"name": "id", "is_physical": True},
            {"name": "name", "is_physical": True},
        ]
        assert detect_virtual_columns(columns) == []

    def test_all_virtual(self):
        """All virtual returns all names."""
        columns = [
            {"name": "calc1", "is_physical": False},
            {"name": "calc2", "is_physical": False},
        ]
        virtual = detect_virtual_columns(columns)
        assert virtual == ["calc1", "calc2"]

    def test_default_is_physical(self):
        """Default is_physical is True when missing."""
        columns = [{"name": "id"}]
        assert detect_virtual_columns(columns) == []


class TestFetchDatasourceMetadata:
    """fetch_datasource_metadata — fetch columns + dialect."""

    @pytest.mark.asyncio
    async def test_success_with_database_dict(self):
        """Success when database is a dict."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [
                    {"name": "id", "type": "INTEGER", "is_physical": True},
                    {"name": "name", "type": "VARCHAR", "is_physical": True},
                ],
                "database": {"backend": "postgresql"},
            }

            columns, dialect = await fetch_datasource_metadata(1, "env-1", config_manager)
            assert len(columns) == 2
            assert dialect == "postgresql"
            assert columns[0]["name"] == "id"

    @pytest.mark.asyncio
    async def test_no_env_config(self):
        """Non-existent env raises ValueError."""
        config_manager = MagicMock()
        config_manager.get_environments.return_value = []

        with pytest.raises(ValueError, match="not found"):
            await fetch_datasource_metadata(1, "env-1", config_manager)

    @pytest.mark.asyncio
    async def test_columns_with_name_fallback(self):
        """Uses 'column_name' when 'name' is missing."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [
                    {"column_name": "my_col", "type": "VARCHAR"},
                ],
                "database": {"backend": "postgresql"},
            }

            columns, dialect = await fetch_datasource_metadata(1, "env-1", config_manager)
            assert len(columns) == 1
            assert columns[0]["name"] == "my_col"

    @pytest.mark.asyncio
    async def test_skips_columns_without_name(self):
        """Skips columns without name or column_name."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [
                    {"name": "valid_col"},
                    {"extra": "no_name"},
                    {},
                ],
                "database": {"backend": "postgresql"},
            }

            columns, dialect = await fetch_datasource_metadata(1, "env-1", config_manager)
            assert len(columns) == 1

    @pytest.mark.asyncio
    async def test_database_info_as_non_dict(self):
        """When database_info is not a dict (e.g., str), fetches from database_id."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [],
                "database": "not a dict",
                "database_id": 123,
            }
            mock_client.get_database.return_value = {"result": {"backend": "mysql"}}

            columns, dialect = await fetch_datasource_metadata(1, "env-1", config_manager)
            assert dialect == "mysql"

    @pytest.mark.asyncio
    async def test_database_info_not_dict_no_db_id(self):
        """When database_info not a dict and no database_id, raises."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [],
                "database": "not a dict",
            }

            with pytest.raises(ValueError, match="No database information"):
                await fetch_datasource_metadata(1, "env-1", config_manager)

    @pytest.mark.asyncio
    async def test_database_info_not_dict_db_error(self):
        """Exception when fetching database info raises."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [],
                "database": "not a dict",
                "database_id": 123,
            }
            mock_client.get_database.side_effect = Exception("API error")

            with pytest.raises(ValueError, match="Could not determine"):
                await fetch_datasource_metadata(1, "env-1", config_manager)


class TestGetDatasourceColumns:
    """get_datasource_columns — structured column metadata response."""

    @pytest.mark.asyncio
    async def test_success(self):
        """Returns structured response with columns."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [
                    {"name": "id", "type": "INTEGER"},
                ],
                "database": {"backend": "postgresql"},
                "table_name": "my_table",
                "schema": "public",
            }

            response = await get_datasource_columns(1, "env-1", config_manager)
            assert response.datasource_id == 1
            assert response.database_dialect == "postgresql"
            assert response.datasource_name == "my_table"
            assert len(response.columns) == 1
            assert response.columns[0].name == "id"

    @pytest.mark.asyncio
    async def test_no_env_config(self):
        """Non-existent env raises ValueError."""
        config_manager = MagicMock()
        config_manager.get_environments.return_value = []

        with pytest.raises(ValueError, match="not found"):
            await get_datasource_columns(1, "env-1", config_manager)

    @pytest.mark.asyncio
    async def test_dialect_from_database_fallback(self):
        """When dialect from database dict fails, fetches from database_id."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            # get_dialect_from_database will fail on empty backend
            mock_client.get_dataset_detail.return_value = {
                "columns": [],
                "database": {"not_backend": "mysql"},
                "database_id": 456,
            }
            mock_client.get_database.return_value = {"result": {"backend": "mysql"}}

            response = await get_datasource_columns(1, "env-1", config_manager)
            assert response.database_dialect == "mysql"

    @pytest.mark.asyncio
    async def test_no_database_id_raises(self):
        """When dialect fails and no database_id, raises."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [],
                "database": {"not_backend": "xxx"},
            }

            with pytest.raises(ValueError, match="Could not determine"):
                await get_datasource_columns(1, "env-1", config_manager)

    @pytest.mark.asyncio
    async def test_skips_column_without_name(self):
        """Columns without name or column_name are skipped (line 159)."""
        config_manager = MagicMock()
        env_config = MagicMock()
        env_config.id = "env-1"
        config_manager.get_environments.return_value = [env_config]

        with patch("src.plugins.translate.service_datasource.get_superset_client") as mock_get:
            mock_client = AsyncMock()
            mock_get.return_value = mock_client
            mock_client.get_dataset_detail.return_value = {
                "columns": [
                    {"name": "valid_col", "type": "INTEGER"},
                    {"not_name": "should_skip", "type": "VARCHAR"},
                    {},
                    {"column_name": "also_valid", "type": "TEXT"},
                ],
                "database": {"backend": "postgresql"},
                "table_name": "t1",
                "schema": "public",
            }

            response = await get_datasource_columns(1, "env-1", config_manager)
            assert len(response.columns) == 2
            assert response.columns[0].name == "valid_col"
            assert response.columns[1].name == "also_valid"
# #endregion Test.DatasourceMetadataService
