# #region Test.ProfileUtils [C:3] [TYPE Module] [SEMANTICS test,profile,utils,pure-functions]
# @BRIEF Tests for profile_utils.py — sanitize, normalize, mask, validate, default preference, owner tokens, exceptions.
# @RELATION BINDS_TO -> [ProfileUtils]
# @TEST_EDGE: missing_field -> None/empty inputs handled gracefully
# @TEST_EDGE: invalid_type -> Invalid values produce validation errors
# @TEST_EDGE: external_fail -> N/A (pure functions, no external deps)

from pathlib import Path
import sys

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

import pytest

from src.services.profile_utils import (
    EnvironmentNotFoundError,
    ProfileAuthorizationError,
    ProfileValidationError,
    build_default_preference,
    mask_secret_value,
    normalize_density,
    normalize_owner_tokens,
    normalize_start_page,
    normalize_username,
    sanitize_secret,
    sanitize_text,
    sanitize_username,
    validate_update_payload,
)


def _validate(**overrides):
    defaults = dict(
        superset_username="admin",
        show_only_my_dashboards=False,
        git_email=None,
        start_page="dashboards",
        dashboards_table_density="comfortable",
    )
    defaults.update(overrides)
    return validate_update_payload(**defaults)


# --- sanitize_text ---

# #region test_sanitize_text [C:2] [TYPE Function]
# @BRIEF Verify sanitize_text with None, empty, whitespace, and valid inputs.
@pytest.mark.parametrize("value, expected", [
    (None, None), ("", None), ("   ", None),
    ("  hello  ", "hello"), ("world", "world"),
])
def test_sanitize_text(value, expected):
    assert sanitize_text(value) == expected
# #endregion test_sanitize_text

# --- sanitize_secret ---

# #region test_sanitize_secret [C:2] [TYPE Function]
# @BRIEF Verify sanitize_secret with None, empty, whitespace, and valid secrets.
@pytest.mark.parametrize("value, expected", [
    (None, None), ("", None), ("   ", None),
    ("  my-token-abc  ", "my-token-abc"),
])
def test_sanitize_secret(value, expected):
    assert sanitize_secret(value) == expected
# #endregion test_sanitize_secret

# --- sanitize_username ---

# #region test_sanitize_username [C:2] [TYPE Function]
# @BRIEF Verify sanitize_username delegates to sanitize_text.
@pytest.mark.parametrize("value, expected", [
    (None, None), ("  admin  ", "admin"),
])
def test_sanitize_username(value, expected):
    assert sanitize_username(value) == expected
# #endregion test_sanitize_username

# --- normalize_username ---

# #region test_normalize_username [C:2] [TYPE Function]
# @BRIEF Verify trim+lower normalization for actor matching.
@pytest.mark.parametrize("value, expected", [
    (None, None), ("", None),
    ("  AdminUser  ", "adminuser"), ("guest", "guest"),
])
def test_normalize_username(value, expected):
    assert normalize_username(value) == expected
# #endregion test_normalize_username

# --- normalize_start_page ---

# #region test_normalize_start_page [C:2] [TYPE Function]
# @BRIEF Verify start page alias mapping to canonical values.
@pytest.mark.parametrize("value, expected", [
    ("reports-logs", "reports"), ("dashboards", "dashboards"),
    ("datasets", "datasets"), ("reports", "reports"),
    ("settings", "dashboards"), (None, "dashboards"),
    ("DATASETS", "datasets"),
])
def test_normalize_start_page(value, expected):
    assert normalize_start_page(value) == expected
# #endregion test_normalize_start_page

# --- normalize_density ---

# #region test_normalize_density [C:2] [TYPE Function]
# @BRIEF Verify density alias mapping to canonical values.
@pytest.mark.parametrize("value, expected", [
    ("free", "comfortable"), ("compact", "compact"),
    ("comfortable", "comfortable"), ("spacious", "comfortable"),
    (None, "comfortable"),
])
def test_normalize_density(value, expected):
    assert normalize_density(value) == expected
# #endregion test_normalize_density

# --- mask_secret_value ---

# #region test_mask_secret_value [C:2] [TYPE Function]
# @BRIEF Verify safe display masking for secrets of various lengths.
@pytest.mark.parametrize("value, expected", [
    (None, None), ("", None),
    ("ab", "***"), ("abcd", "***"),
    ("my-token-value", "my***ue"), ("  ab  ", "***"),
])
def test_mask_secret_value(value, expected):
    assert mask_secret_value(value) == expected
# #endregion test_mask_secret_value

# --- validate_update_payload ---

# #region test_validate_valid_payload [C:2] [TYPE Function]
# @BRIEF All-valid input returns empty error list.
def test_validate_valid_payload():
    assert _validate() == []
# #endregion test_validate_valid_payload

# #region test_validate_username_with_space [C:2] [TYPE Function]
# @BRIEF Username containing space produces validation error.
def test_validate_username_with_space():
    errors = _validate(superset_username="admin user")
    assert any("space" in e.lower() for e in errors)
# #endregion test_validate_username_with_space

# #region test_validate_show_only_requires_username [C:2] [TYPE Function]
# @BRIEF show_only_my_dashboards=True without username produces error.
def test_validate_show_only_requires_username():
    errors = _validate(superset_username=None, show_only_my_dashboards=True)
    assert any("username" in e.lower() and "required" in e.lower() for e in errors)
# #endregion test_validate_show_only_requires_username

# #region test_validate_git_email_missing_at [C:2] [TYPE Function]
# @BRIEF Git email without @ produces validation error.
def test_validate_git_email_missing_at():
    errors = _validate(git_email="not-an-email")
    assert any("git email" in e.lower() for e in errors)
# #endregion test_validate_git_email_missing_at

# #region test_validate_git_email_with_space [C:2] [TYPE Function]
# @BRIEF Git email containing space produces validation error.
def test_validate_git_email_with_space():
    errors = _validate(git_email="admin @x.com")
    assert any("git email" in e.lower() for e in errors)
# #endregion test_validate_git_email_with_space

# #region test_validate_git_email_starts_with_at [C:2] [TYPE Function]
# @BRIEF Git email starting with @ produces validation error.
def test_validate_git_email_starts_with_at():
    errors = _validate(git_email="@example.com")
    assert any("git email" in e.lower() for e in errors)
# #endregion test_validate_git_email_starts_with_at

# #region test_validate_git_email_ends_with_at [C:2] [TYPE Function]
# @BRIEF Git email ending with @ produces validation error.
def test_validate_git_email_ends_with_at():
    errors = _validate(git_email="admin@")
    assert any("git email" in e.lower() for e in errors)
# #endregion test_validate_git_email_ends_with_at

# #region test_validate_invalid_start_page [C:2] [TYPE Function]
# @BRIEF Unsupported start_page value produces validation error.
def test_validate_invalid_start_page():
    errors = _validate(start_page="settings")
    assert any("start page" in e.lower() for e in errors)
# #endregion test_validate_invalid_start_page

# #region test_validate_invalid_density [C:2] [TYPE Function]
# @BRIEF Unsupported density value produces validation error.
def test_validate_invalid_density():
    errors = _validate(dashboards_table_density="spacious")
    assert any("density" in e.lower() for e in errors)
# #endregion test_validate_invalid_density

# #region test_validate_invalid_notification_email [C:2] [TYPE Function]
# @BRIEF Invalid notification email variants all produce validation error.
@pytest.mark.parametrize("bad_email", ["bad-email", "@x.com", "a@", "a b@c.com"])
def test_validate_invalid_notification_email(bad_email):
    errors = _validate(email_address=bad_email)
    assert any("notification email" in e.lower() for e in errors)
# #endregion test_validate_invalid_notification_email

# #region test_validate_valid_notification_email [C:2] [TYPE Function]
# @BRIEF Valid notification email produces no notification error.
def test_validate_valid_notification_email():
    errors = _validate(email_address="user@example.com")
    assert not any("notification email" in e.lower() for e in errors)
# #endregion test_validate_valid_notification_email

# #region test_validate_multiple_errors [C:2] [TYPE Function]
# @BRIEF Multiple invalid fields produce multiple accumulated errors.
def test_validate_multiple_errors():
    errors = _validate(
        superset_username="admin user",
        show_only_my_dashboards=True,
        git_email="bad",
        start_page="unknown",
        dashboards_table_density="spacious",
    )
    assert len(errors) >= 4
# #endregion test_validate_multiple_errors

# --- build_default_preference ---

# #region test_build_default_preference [C:2] [TYPE Function]
# @BRIEF Verify default preference DTO has correct initial values for all fields.
def test_build_default_preference():
    pref = build_default_preference("user-1")
    assert pref.user_id == "user-1"
    assert pref.superset_username is None
    assert pref.superset_username_normalized is None
    assert pref.show_only_my_dashboards is False
    assert pref.show_only_slug_dashboards is True
    assert pref.git_username is None
    assert pref.git_email is None
    assert pref.has_git_personal_access_token is False
    assert pref.git_personal_access_token_masked is None
    assert pref.start_page == "dashboards"
    assert pref.auto_open_task_drawer is True
    assert pref.dashboards_table_density == "comfortable"
    assert pref.telegram_id is None
    assert pref.email_address is None
    assert pref.notify_on_fail is True
    assert pref.created_at == pref.updated_at
# #endregion test_build_default_preference

# --- normalize_owner_tokens ---

# #region test_owner_tokens_none [C:2] [TYPE Function]
# @BRIEF None input returns empty list.
def test_owner_tokens_none():
    assert normalize_owner_tokens(None) == []
# #endregion test_owner_tokens_none

# #region test_owner_tokens_empty [C:2] [TYPE Function]
# @BRIEF Empty iterable returns empty list.
def test_owner_tokens_empty():
    assert normalize_owner_tokens([]) == []
# #endregion test_owner_tokens_empty

# #region test_owner_tokens_dict_username [C:2] [TYPE Function]
# @BRIEF Dict with username field produces normalized token.
def test_owner_tokens_dict_username():
    tokens = normalize_owner_tokens([{"username": "AdminUser"}])
    assert "adminuser" in tokens
# #endregion test_owner_tokens_dict_username

# #region test_owner_tokens_dict_names [C:2] [TYPE Function]
# @BRIEF Dict with first/last name produces individual and combined tokens.
def test_owner_tokens_dict_names():
    tokens = normalize_owner_tokens([{"first_name": "John", "last_name": "Doe"}])
    assert "john" in tokens
    assert "doe" in tokens
    assert "john_doe" in tokens
# #endregion test_owner_tokens_dict_names

# #region test_owner_tokens_string [C:2] [TYPE Function]
# @BRIEF Plain string items are normalized directly.
def test_owner_tokens_string():
    tokens = normalize_owner_tokens(["  Admin  "])
    assert "admin" in tokens
# #endregion test_owner_tokens_string

# #region test_owner_tokens_dedup [C:2] [TYPE Function]
# @BRIEF Duplicate tokens after normalization are collapsed.
def test_owner_tokens_dedup():
    tokens = normalize_owner_tokens(["admin", "  Admin  "])
    assert tokens == ["admin"]
# #endregion test_owner_tokens_dedup

# #region test_owner_tokens_skip_empty [C:2] [TYPE Function]
# @BRIEF Empty/whitespace/None items produce no tokens.
def test_owner_tokens_skip_empty():
    assert normalize_owner_tokens(["", "  ", None]) == []
# #endregion test_owner_tokens_skip_empty

# #region test_owner_tokens_dict_email [C:2] [TYPE Function]
# @BRIEF Dict with email field produces normalized email token.
def test_owner_tokens_dict_email():
    tokens = normalize_owner_tokens([{"email": "User@Example.com"}])
    assert "user@example.com" in tokens
# #endregion test_owner_tokens_dict_email

# #region test_owner_tokens_dict_multi [C:2] [TYPE Function]
# @BRIEF Dict with multiple fields produces all valid candidate tokens.
def test_owner_tokens_dict_multi():
    owners = [{"username": "jdoe", "first_name": "John", "last_name": "Doe", "email": "john@example.com"}]
    tokens = normalize_owner_tokens(owners)
    assert "jdoe" in tokens
    assert "john" in tokens
    assert "doe" in tokens
    assert "john@example.com" in tokens
# #endregion test_owner_tokens_dict_multi

# --- exception classes ---

# #region test_exception_profile_validation_error [C:2] [TYPE Function]
# @BRIEF ProfileValidationError stores errors list and has expected message.
def test_exception_profile_validation_error():
    err = ProfileValidationError(["field1 bad", "field2 bad"])
    assert err.errors == ["field1 bad", "field2 bad"]
    assert "validation failed" in str(err).lower()
    with pytest.raises(ProfileValidationError):
        raise ProfileValidationError(["test"])
# #endregion test_exception_profile_validation_error

# #region test_exception_environment_not_found [C:2] [TYPE Function]
# @BRIEF EnvironmentNotFoundError can be raised and caught as Exception.
def test_exception_environment_not_found():
    with pytest.raises(EnvironmentNotFoundError):
        raise EnvironmentNotFoundError("env-xyz")
# #endregion test_exception_environment_not_found

# #region test_exception_profile_authorization [C:2] [TYPE Function]
# @BRIEF ProfileAuthorizationError can be raised and caught as Exception.
def test_exception_profile_authorization():
    with pytest.raises(ProfileAuthorizationError):
        raise ProfileAuthorizationError("cross-user")
# #endregion test_exception_profile_authorization

# #endregion Test.ProfileUtils
