# #region Test.ProfilePreferenceService [C:3] [TYPE Module] [SEMANTICS test,profile,preference,crud]
# @BRIEF Tests for profile_preference_service.py — get, update, validation, encryption.
# @RELATION BINDS_TO -> [ProfilePreferenceService]

from pathlib import Path
import sys

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

from unittest.mock import MagicMock, patch, ANY
from datetime import UTC, datetime

import pytest


# Set ENCRYPTION_KEY before importing anything that uses EncryptionManager
import os
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
os.environ.setdefault("ENCRYPTION_KEY", "MldUHg5kwSAcPnnYxmhWDS6ASb6e_bWQRV5gtwHrjQ0=")


from src.schemas.profile import ProfilePreferenceUpdateRequest


def make_user(user_id="user-1", **kwargs):
    from src.models.auth import User
    user = MagicMock(spec=User)
    user.id = user_id
    user.roles = []
    user.auth_source = "database"
    for k, v in kwargs.items():
        setattr(user, k, v)
    return user


def make_preference_row(user_id="user-1", **overrides):
    from src.models.profile import UserDashboardPreference
    pref = MagicMock(spec=UserDashboardPreference)
    pref.user_id = user_id
    pref.superset_username = None
    pref.superset_username_normalized = None
    pref.show_only_my_dashboards = False
    pref.show_only_slug_dashboards = True
    pref.git_username = None
    pref.git_email = None
    pref.git_personal_access_token_encrypted = None
    pref.start_page = "dashboards"
    pref.auto_open_task_drawer = True
    pref.dashboards_table_density = "comfortable"
    pref.telegram_id = None
    pref.email_address = None
    pref.notify_on_fail = True
    pref.created_at = datetime.now(UTC)
    pref.updated_at = datetime.now(UTC)

    for k, v in overrides.items():
        setattr(pref, k, v)
    return pref


class TestGetMyPreference:
    """get_my_preference — return user preference or default."""

    @pytest.fixture
    def _make_security_summary(self):
        from src.schemas.profile import ProfileSecuritySummary
        return ProfileSecuritySummary(read_only=True, auth_source="database")

    @pytest.fixture
    def service(self, _make_security_summary):
        from src.services.profile_preference_service import ProfilePreferenceService
        db = MagicMock()
        config_mgr = MagicMock()
        with patch("src.services.profile_preference_service.AuthRepository") as mock_auth_repo_cls, \
             patch("src.services.profile_preference_service.EncryptionManager"), \
             patch("src.services.profile_preference_service.SecurityBadgeService") as mock_badge_cls:
            mock_auth = MagicMock()
            mock_auth_repo_cls.return_value = mock_auth
            mock_badge = MagicMock()
            mock_badge.build_security_summary.return_value = _make_security_summary
            mock_badge_cls.return_value = mock_badge
            svc = ProfilePreferenceService(db, config_mgr)
            svc.auth_repository = mock_auth
            svc.security_badge_service = mock_badge
            yield svc

    def test_no_preference_returns_default(self, service):
        service.auth_repository.get_user_dashboard_preference.return_value = None
        user = make_user()
        result = service.get_my_preference(user)
        assert result.status == "success"
        assert "not configured" in (result.message or "").lower()
        assert result.preference.user_id == "user-1"
        assert result.preference.superset_username is None
        assert result.preference.show_only_my_dashboards is False

    def test_existing_preference(self, service):
        pref = make_preference_row(
            superset_username="admin",
            superset_username_normalized="admin",
            show_only_my_dashboards=True,
        )
        service.auth_repository.get_user_dashboard_preference.return_value = pref
        user = make_user()
        result = service.get_my_preference(user)
        assert result.status == "success"
        assert result.preference.show_only_my_dashboards is True
        assert result.preference.superset_username_normalized == "admin"

    def test_security_summary_included(self, service, _make_security_summary):
        service.auth_repository.get_user_dashboard_preference.return_value = None
        user = make_user()
        result = service.get_my_preference(user)
        assert result.security is not None
        service.security_badge_service.build_security_summary.assert_called_once_with(user)


class TestGetDashboardFilterBinding:
    """get_dashboard_filter_binding — dashboard filter fields only."""

    @pytest.fixture
    def service(self):
        from src.services.profile_preference_service import ProfilePreferenceService
        db = MagicMock()
        config_mgr = MagicMock()
        with patch("src.services.profile_preference_service.AuthRepository") as mock_auth_repo_cls, \
             patch("src.services.profile_preference_service.EncryptionManager"), \
             patch("src.services.profile_preference_service.SecurityBadgeService"):
            mock_auth = MagicMock()
            mock_auth_repo_cls.return_value = mock_auth
            svc = ProfilePreferenceService(db, config_mgr)
            svc.auth_repository = mock_auth
            yield svc

    def test_no_preference(self, service):
        service.auth_repository.get_user_dashboard_preference.return_value = None
        user = make_user()
        result = service.get_dashboard_filter_binding(user)
        assert result["superset_username"] is None
        assert result["superset_username_normalized"] is None
        assert result["show_only_my_dashboards"] is False
        assert result["show_only_slug_dashboards"] is True

    def test_with_preference(self, service):
        pref = make_preference_row(
            superset_username="  Admin  ",
            show_only_my_dashboards=True,
            show_only_slug_dashboards=False,
        )
        service.auth_repository.get_user_dashboard_preference.return_value = pref
        user = make_user()
        result = service.get_dashboard_filter_binding(user)
        assert result["superset_username"] == "Admin"
        assert result["superset_username_normalized"] == "admin"
        assert result["show_only_my_dashboards"] is True
        assert result["show_only_slug_dashboards"] is False


class TestUpdateMyPreference:
    """update_my_preference — validate and persist preference."""

    @pytest.fixture
    def _make_security_summary(self):
        from src.schemas.profile import ProfileSecuritySummary
        return ProfileSecuritySummary(read_only=True, auth_source="database")

    @pytest.fixture
    def service(self, _make_security_summary):
        from src.services.profile_preference_service import ProfilePreferenceService
        db = MagicMock()
        config_mgr = MagicMock()
        with patch("src.services.profile_preference_service.AuthRepository") as mock_auth_repo_cls, \
             patch("src.services.profile_preference_service.EncryptionManager") as mock_enc_cls, \
             patch("src.services.profile_preference_service.SecurityBadgeService") as mock_badge_cls:
            mock_auth = MagicMock()
            mock_auth_repo_cls.return_value = mock_auth
            mock_enc = MagicMock()
            mock_enc.encrypt.return_value = "encrypted-token"
            mock_enc.decrypt.return_value = "decrypted-token"
            mock_enc_cls.return_value = mock_enc
            mock_badge = MagicMock()
            mock_badge.build_security_summary.return_value = _make_security_summary
            mock_badge_cls.return_value = mock_badge
            svc = ProfilePreferenceService(db, config_mgr)
            svc.auth_repository = mock_auth
            svc.encryption = mock_enc
            svc.security_badge_service = mock_badge
            yield svc

    def test_cross_user_blocked(self, service):
        from src.services.profile_utils import ProfileAuthorizationError
        user = make_user("user-1")
        payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
        payload.superset_username = None
        payload.show_only_my_dashboards = None
        with pytest.raises(ProfileAuthorizationError):
            service.update_my_preference(user, payload, target_user_id="user-other")

    def test_validation_error(self, service):
        from src.services.profile_utils import ProfileValidationError
        service.auth_repository.get_user_dashboard_preference.return_value = None
        user = make_user("user-1")
        payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
        payload.superset_username = "admin user"  # has space
        payload.show_only_my_dashboards = None
        payload.show_only_slug_dashboards = None
        payload.git_username = None
        payload.git_email = None
        payload.git_personal_access_token = None
        payload.start_page = None
        payload.auto_open_task_drawer = None
        payload.dashboards_table_density = "spacious"  # invalid
        payload.telegram_id = None
        payload.email_address = None
        payload.notify_on_fail = None
        payload.model_fields_set = {"superset_username", "dashboards_table_density"}

        with pytest.raises(ProfileValidationError) as exc:
            service.update_my_preference(user, payload)
        assert len(exc.value.errors) >= 1

    def test_successful_update_new_preference(self, service):
        service.auth_repository.get_user_dashboard_preference.return_value = None
        saved_pref = make_preference_row(
            superset_username="admin",
            superset_username_normalized="admin",
            show_only_my_dashboards=True,
            git_personal_access_token_encrypted="encrypted-token",
        )
        service.auth_repository.save_user_dashboard_preference.return_value = saved_pref

        user = make_user("user-1")
        payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
        payload.superset_username = "admin"
        payload.show_only_my_dashboards = True
        payload.show_only_slug_dashboards = None
        payload.git_username = None
        payload.git_email = None
        payload.git_personal_access_token = "new-token"
        payload.start_page = None
        payload.auto_open_task_drawer = None
        payload.dashboards_table_density = None
        payload.telegram_id = None
        payload.email_address = None
        payload.notify_on_fail = None
        payload.model_fields_set = {"superset_username", "show_only_my_dashboards", "git_personal_access_token"}

        result = service.update_my_preference(user, payload)
        assert result.status == "success"
        assert result.preference.show_only_my_dashboards is True
        assert result.preference.has_git_personal_access_token is True
        service.encryption.encrypt.assert_called_once_with("new-token")

    def test_clear_git_token(self, service):
        # When git_personal_access_token is empty string, clear existing token
        existing = make_preference_row(
            git_personal_access_token_encrypted="existing-encrypted",
        )
        service.auth_repository.get_user_dashboard_preference.return_value = existing
        saved_pref = make_preference_row(
            git_personal_access_token_encrypted=None,
        )
        service.auth_repository.save_user_dashboard_preference.return_value = saved_pref

        user = make_user("user-1")
        payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
        payload.superset_username = None
        payload.show_only_my_dashboards = None
        payload.show_only_slug_dashboards = None
        payload.git_username = None
        payload.git_email = None
        payload.git_personal_access_token = ""
        payload.start_page = None
        payload.auto_open_task_drawer = None
        payload.dashboards_table_density = None
        payload.telegram_id = None
        payload.email_address = None
        payload.notify_on_fail = None
        payload.model_fields_set = {"git_personal_access_token"}

        result = service.update_my_preference(user, payload)
        assert result.status == "success"

    def test_update_existing_fields(self, service):
        existing = make_preference_row(
            superset_username="old_user",
            superset_username_normalized="old_user",
            start_page="reports",
            dashboards_table_density="compact",
            telegram_id="@old",
            email_address="old@x.com",
            notify_on_fail=False,
        )
        service.auth_repository.get_user_dashboard_preference.return_value = existing
        saved_pref = make_preference_row(
            superset_username="new_user",
            superset_username_normalized="new_user",
        )
        service.auth_repository.save_user_dashboard_preference.return_value = saved_pref

        user = make_user("user-1")
        payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
        payload.superset_username = "new_user"
        payload.show_only_my_dashboards = None
        payload.show_only_slug_dashboards = None
        payload.git_username = None
        payload.git_email = None
        payload.git_personal_access_token = None
        payload.start_page = None
        payload.auto_open_task_drawer = None
        payload.dashboards_table_density = None
        payload.telegram_id = "@new_telegram"
        payload.email_address = None
        payload.notify_on_fail = None
        payload.model_fields_set = {"superset_username", "telegram_id"}

        result = service.update_my_preference(user, payload)
        assert result.status == "success"

    def test_all_fields_update(self, service):
        """Cover all provided_fields branches: show_only_slug, git_username, git_email,
        start_page, auto_open_task_drawer, email_address, notify_on_fail."""
        existing = make_preference_row(
            superset_username="old", superset_username_normalized="old",
            show_only_my_dashboards=False, show_only_slug_dashboards=False,
            git_username="old_git", git_email="old@x.com",
            start_page="reports", auto_open_task_drawer=False,
            dashboards_table_density="compact",
            telegram_id="@old", email_address="old@x.com", notify_on_fail=False,
        )
        service.auth_repository.get_user_dashboard_preference.return_value = existing
        saved_pref = make_preference_row(
            superset_username="new", superset_username_normalized="new",
        )
        service.auth_repository.save_user_dashboard_preference.return_value = saved_pref

        user = make_user("user-1")
        payload = MagicMock(spec=ProfilePreferenceUpdateRequest)
        payload.superset_username = None
        payload.show_only_my_dashboards = True
        payload.show_only_slug_dashboards = True
        payload.git_username = "new_git"
        payload.git_email = "new@x.com"
        payload.git_personal_access_token = None
        payload.start_page = "datasets"
        payload.auto_open_task_drawer = True
        payload.dashboards_table_density = "compact"
        payload.telegram_id = None
        payload.email_address = "new_email@x.com"
        payload.notify_on_fail = True
        payload.model_fields_set = {
            "show_only_my_dashboards", "show_only_slug_dashboards",
            "git_username", "git_email", "start_page",
            "auto_open_task_drawer", "email_address", "notify_on_fail",
        }

        result = service.update_my_preference(user, payload)
        assert result.status == "success"


class TestToPreferencePayload:
    """_to_preference_payload — ORM row to API DTO."""

    @pytest.fixture
    def service(self):
        from src.services.profile_preference_service import ProfilePreferenceService
        db = MagicMock()
        config_mgr = MagicMock()
        with patch("src.services.profile_preference_service.AuthRepository"), \
             patch("src.services.profile_preference_service.EncryptionManager") as mock_enc_cls, \
             patch("src.services.profile_preference_service.SecurityBadgeService"):
            mock_enc = MagicMock()
            mock_enc.decrypt.return_value = "my-secret-token"
            mock_enc_cls.return_value = mock_enc
            svc = ProfilePreferenceService(db, config_mgr)
            svc.encryption = mock_enc
            yield svc

    def test_maps_all_fields(self, service):
        pref = make_preference_row(
            superset_username="admin",
            superset_username_normalized="admin",
            git_username="jdoe",
            git_email="j@x.com",
            git_personal_access_token_encrypted="encrypted-val",
            start_page="datasets",
            telegram_id="@admin",
            email_address="admin@x.com",
        )
        result = service._to_preference_payload(pref, "user-1")
        assert result.user_id == "user-1"
        assert result.superset_username == "admin"
        assert result.git_username == "jdoe"
        assert result.git_email == "j@x.com"
        assert result.has_git_personal_access_token is True
        assert result.start_page == "datasets"
        assert result.telegram_id == "@admin"
        assert result.email_address == "admin@x.com"

    def test_no_token(self, service):
        pref = make_preference_row(
            git_personal_access_token_encrypted=None,
        )
        result = service._to_preference_payload(pref, "user-1")
        assert result.has_git_personal_access_token is False
        assert result.git_personal_access_token_masked is None

    def test_decryption_failure_fallback(self, service):
        service.encryption.decrypt.side_effect = Exception("Decrypt failed")
        pref = make_preference_row(
            git_personal_access_token_encrypted="corrupted",
        )
        result = service._to_preference_payload(pref, "user-1")
        assert result.has_git_personal_access_token is True
        assert result.git_personal_access_token_masked == "***"


class TestGetOrCreatePreferenceRow:
    """_get_or_create_preference_row — returns existing or new."""

    @pytest.fixture
    def service(self):
        from src.services.profile_preference_service import ProfilePreferenceService
        db = MagicMock()
        config_mgr = MagicMock()
        with patch("src.services.profile_preference_service.AuthRepository") as mock_auth_repo_cls, \
             patch("src.services.profile_preference_service.EncryptionManager"), \
             patch("src.services.profile_preference_service.SecurityBadgeService"):
            mock_auth = MagicMock()
            mock_auth_repo_cls.return_value = mock_auth
            svc = ProfilePreferenceService(db, config_mgr)
            svc.auth_repository = mock_auth
            yield svc

    def test_existing_returns_row(self, service):
        pref = make_preference_row()
        service.auth_repository.get_user_dashboard_preference.return_value = pref
        result = service._get_or_create_preference_row("user-1")
        assert result is pref

    def test_missing_creates_new(self, service):
        from src.models.profile import UserDashboardPreference
        service.auth_repository.get_user_dashboard_preference.return_value = None
        result = service._get_or_create_preference_row("user-1")
        assert isinstance(result, UserDashboardPreference)
        assert result.user_id == "user-1"


class TestBuildDefaultPreference:
    """_build_default_preference — delegates to profile_utils."""

    def test_delegates_correctly(self):
        from src.services.profile_preference_service import ProfilePreferenceService
        db = MagicMock()
        config_mgr = MagicMock()
        with patch("src.services.profile_preference_service.AuthRepository"), \
             patch("src.services.profile_preference_service.EncryptionManager"), \
             patch("src.services.profile_preference_service.SecurityBadgeService"), \
             patch("src.services.profile_preference_service.build_default_preference") as mock_build:
            mock_build.return_value = MagicMock()
            svc = ProfilePreferenceService(db, config_mgr)
            result = svc._build_default_preference("user-1")
            assert result is mock_build.return_value
            mock_build.assert_called_once_with("user-1")
# #endregion Test.ProfilePreferenceService
