# #region Test.Api.DashboardProjection [C:3] [TYPE Module] [SEMANTICS test,dashboard,projection]
# @BRIEF Unit tests for dashboard projection/profile helpers.
# @RELATION BINDS_TO -> [DashboardProjection]
# @TEST_EDGE: owner_normalization
# @TEST_EDGE: profile_filter_binding
# @TEST_EDGE: task_matching

import sys
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
    sys.path.insert(0, _src)


# ── _normalize_actor_alias_token ──

class TestNormalizeActorAliasToken:
    def test_none_value(self):
        from src.api.routes.dashboards._projection import _normalize_actor_alias_token
        assert _normalize_actor_alias_token(None) is None

    def test_empty_string(self):
        from src.api.routes.dashboards._projection import _normalize_actor_alias_token
        assert _normalize_actor_alias_token("") is None

    def test_whitespace(self):
        from src.api.routes.dashboards._projection import _normalize_actor_alias_token
        assert _normalize_actor_alias_token("  ") is None

    def test_normalized(self):
        from src.api.routes.dashboards._projection import _normalize_actor_alias_token
        assert _normalize_actor_alias_token("  Alice ") == "alice"


# ── _normalize_owner_display_token ──

class TestNormalizeOwnerDisplayToken:
    def test_none(self):
        from src.api.routes.dashboards._projection import _normalize_owner_display_token
        assert _normalize_owner_display_token(None) is None

    def test_dict_with_username(self):
        from src.api.routes.dashboards._projection import _normalize_owner_display_token
        assert _normalize_owner_display_token({"username": "Alice"}) == "Alice"

    def test_dict_with_full_name(self):
        from src.api.routes.dashboards._projection import _normalize_owner_display_token
        assert _normalize_owner_display_token({"full_name": "Alice Smith"}) == "Alice Smith"

    def test_dict_empty(self):
        from src.api.routes.dashboards._projection import _normalize_owner_display_token
        assert _normalize_owner_display_token({}) is None

    def test_string(self):
        from src.api.routes.dashboards._projection import _normalize_owner_display_token
        assert _normalize_owner_display_token("Alice") == "Alice"

    def test_int(self):
        from src.api.routes.dashboards._projection import _normalize_owner_display_token
        assert _normalize_owner_display_token(42) is None


# ── _normalize_dashboard_owner_values ──

class TestNormalizeDashboardOwnerValues:
    def test_none(self):
        from src.api.routes.dashboards._projection import _normalize_dashboard_owner_values
        assert _normalize_dashboard_owner_values(None) is None

    def test_list_of_dicts(self):
        from src.api.routes.dashboards._projection import _normalize_dashboard_owner_values
        result = _normalize_dashboard_owner_values([
            {"username": "Alice"},
            {"username": "Bob"},
        ])
        assert result == ["Alice", "Bob"]

    def test_single_dict(self):
        from src.api.routes.dashboards._projection import _normalize_dashboard_owner_values
        result = _normalize_dashboard_owner_values({"username": "Charlie"})
        assert result == ["Charlie"]

    def test_duplicates_removed(self):
        from src.api.routes.dashboards._projection import _normalize_dashboard_owner_values
        result = _normalize_dashboard_owner_values([
            {"username": "Alice"},
            {"username": "Alice"},
        ])
        assert result == ["Alice"]


# ── _project_dashboard_response_items ──

class TestProjectDashboardResponseItems:
    def test_owners_normalized(self):
        from src.api.routes.dashboards._projection import _project_dashboard_response_items
        dashboards = [
            {"id": 1, "title": "Main", "owners": [{"username": "Alice"}]},
        ]
        result = _project_dashboard_response_items(dashboards)
        assert result[0]["owners"] == ["Alice"]

    def test_empty_list(self):
        from src.api.routes.dashboards._projection import _project_dashboard_response_items
        assert _project_dashboard_response_items([]) == []


# ── _get_profile_filter_binding ──

class TestGetProfileFilterBinding:
    def test_uses_get_dashboard_filter_binding(self):
        from src.api.routes.dashboards._projection import _get_profile_filter_binding
        profile_service = MagicMock()
        profile_service.get_dashboard_filter_binding.return_value = {
            "superset_username": "alice",
            "superset_username_normalized": "alice",
            "show_only_my_dashboards": True,
            "show_only_slug_dashboards": False,
        }
        result = _get_profile_filter_binding(profile_service, MagicMock())
        assert result["superset_username"] == "alice"
        assert result["show_only_my_dashboards"] is True

    def test_uses_get_my_preference_fallback(self):
        from src.api.routes.dashboards._projection import _get_profile_filter_binding
        profile_service = MagicMock()
        # Remove the attribute so hasattr returns False, triggering fallback to get_my_preference
        del profile_service.get_dashboard_filter_binding
        pref = MagicMock()
        pref.preference.superset_username = "bob"
        pref.preference.superset_username_normalized = "bob"
        pref.preference.show_only_my_dashboards = True
        pref.preference.show_only_slug_dashboards = False
        profile_service.get_my_preference.return_value = pref

        result = _get_profile_filter_binding(profile_service, MagicMock())
        assert result["superset_username"] == "bob"

    def test_no_methods_returns_defaults(self):
        from src.api.routes.dashboards._projection import _get_profile_filter_binding
        profile_service = MagicMock(spec=[])  # no relevant methods
        result = _get_profile_filter_binding(profile_service, MagicMock())
        assert result["superset_username"] is None
        assert result["show_only_my_dashboards"] is False


# ── _resolve_profile_actor_aliases ──

class TestResolveProfileActorAliases:
    @pytest.mark.asyncio
    async def test_empty_username(self):
        from src.api.routes.dashboards._projection import _resolve_profile_actor_aliases
        result = await _resolve_profile_actor_aliases(MagicMock(), "")
        assert result == []

    @pytest.mark.asyncio
    async def test_lookup_success(self):
        from src.api.routes.dashboards._projection import _resolve_profile_actor_aliases

        mock_adapter = AsyncMock()
        mock_adapter.get_users_page.return_value = {
            "items": [
                {"username": "alice", "display_name": "Alice Smith"},
            ]
        }

        mock_env = MagicMock()
        mock_env.id = "env-1"

        with patch("src.api.routes.dashboards._projection.AsyncSupersetClient") as MockClient, \
             patch("src.api.routes.dashboards._projection.SupersetAccountLookupAdapter") as MockAdapter:

            MockAdapter.return_value = mock_adapter

            result = await _resolve_profile_actor_aliases(mock_env, "alice")
            assert "alice" in result
            assert "alice smith" in result

    @pytest.mark.asyncio
    async def test_lookup_exception(self):
        from src.api.routes.dashboards._projection import _resolve_profile_actor_aliases

        mock_env = MagicMock()
        mock_env.id = "env-1"

        with patch("src.api.routes.dashboards._projection.AsyncSupersetClient") as MockClient, \
             patch("src.api.routes.dashboards._projection.SupersetAccountLookupAdapter") as MockAdapter:

            MockAdapter.side_effect = Exception("Connection error")

            result = await _resolve_profile_actor_aliases(mock_env, "alice")
            assert result == ["alice"]


# ── _matches_dashboard_actor_aliases ──

class TestMatchesDashboardActorAliases:
    def test_matches(self):
        from src.api.routes.dashboards._projection import _matches_dashboard_actor_aliases
        profile_service = MagicMock()
        profile_service.matches_dashboard_actor.return_value = True
        assert _matches_dashboard_actor_aliases(profile_service, ["alice"], [], "bob") is True

    def test_no_match(self):
        from src.api.routes.dashboards._projection import _matches_dashboard_actor_aliases
        profile_service = MagicMock()
        profile_service.matches_dashboard_actor.return_value = False
        assert _matches_dashboard_actor_aliases(profile_service, ["alice"], [], None) is False

    def test_first_match_short_circuits(self):
        from src.api.routes.dashboards._projection import _matches_dashboard_actor_aliases
        profile_service = MagicMock()
        profile_service.matches_dashboard_actor.side_effect = [False, True]
        assert _matches_dashboard_actor_aliases(profile_service, ["alice", "bob"], [], None) is True
        assert profile_service.matches_dashboard_actor.call_count == 2


# ── _task_matches_dashboard ──

class TestTaskMatchesDashboard:
    def test_llm_validation_matches(self):
        from src.api.routes.dashboards._projection import _task_matches_dashboard
        task = MagicMock()
        task.plugin_id = "llm_dashboard_validation"
        task.params = {"dashboard_id": 42, "environment_id": "env-1"}
        assert _task_matches_dashboard(task, 42, "env-1") is True

    def test_llm_validation_wrong_dashboard(self):
        from src.api.routes.dashboards._projection import _task_matches_dashboard
        task = MagicMock()
        task.plugin_id = "llm_dashboard_validation"
        task.params = {"dashboard_id": 99}
        assert _task_matches_dashboard(task, 42, None) is False

    def test_llm_validation_no_env(self):
        from src.api.routes.dashboards._projection import _task_matches_dashboard
        task = MagicMock()
        task.plugin_id = "llm_dashboard_validation"
        task.params = {"dashboard_id": 42}
        assert _task_matches_dashboard(task, 42, None) is True

    def test_backup_matches(self):
        from src.api.routes.dashboards._projection import _task_matches_dashboard
        task = MagicMock()
        task.plugin_id = "superset-backup"
        task.params = {"dashboard_ids": [1, 42, 3], "environment_id": "env-1"}
        assert _task_matches_dashboard(task, 42, "env-1") is True

    def test_backup_dashboards_key(self):
        from src.api.routes.dashboards._projection import _task_matches_dashboard
        task = MagicMock()
        task.plugin_id = "superset-backup"
        task.params = {"dashboards": [42], "env": "env-1"}
        assert _task_matches_dashboard(task, 42, "env-1") is True

    def test_unrelated_plugin(self):
        from src.api.routes.dashboards._projection import _task_matches_dashboard
        task = MagicMock()
        task.plugin_id = "some-other-plugin"
        assert _task_matches_dashboard(task, 42, "env-1") is False

    def test_backup_wrong_dashboard(self):
        from src.api.routes.dashboards._projection import _task_matches_dashboard
        task = MagicMock()
        task.plugin_id = "superset-backup"
        task.params = {"dashboard_ids": [1, 2], "environment_id": "env-1"}
        assert _task_matches_dashboard(task, 42, "env-1") is False

    def test_backup_wrong_env(self):
        from src.api.routes.dashboards._projection import _task_matches_dashboard
        task = MagicMock()
        task.plugin_id = "superset-backup"
        task.params = {"dashboard_ids": [42], "environment_id": "env-1"}
        assert _task_matches_dashboard(task, 42, "env-2") is False
# #endregion Test.Api.DashboardProjection
