"""
Tests for the admin user management API (/api/admin/users).

Covers:
- Authentication enforcement (403 for non-admins)
- List users (empty, with doc-only users, with profile-only users, with both)
- Get single user detail
- Create / update user profile via PUT (upsert)
- Delete user profile
- Pagination and search filtering
"""

from datetime import datetime, timezone
from unittest.mock import MagicMock

import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.pool import StaticPool

from app.database import Base, get_db
from app.models import FileRecord, LocalUser, UserProfile

# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture()
def au_engine():
    """In-memory SQLite engine for admin-user tests."""
    engine = create_engine(
        "sqlite:///:memory:",
        connect_args={"check_same_thread": False},
        poolclass=StaticPool,
    )
    Base.metadata.create_all(bind=engine)
    yield engine
    Base.metadata.drop_all(bind=engine)


@pytest.fixture()
def au_session(au_engine):
    """DB session scoped to one test."""
    Session = sessionmaker(bind=au_engine)
    session = Session()
    yield session
    session.close()


@pytest.fixture()
def au_client(au_engine):
    """TestClient that uses an in-memory DB and overrides _require_admin to allow access."""
    from app.api.admin_users import _require_admin
    from app.main import app

    def override_db():
        Session = sessionmaker(bind=au_engine)
        session = Session()
        try:
            yield session
        finally:
            session.close()

    def override_require_admin():
        return {"email": "admin@test.com", "is_admin": True, "name": "Admin"}

    app.dependency_overrides[get_db] = override_db
    app.dependency_overrides[_require_admin] = override_require_admin
    with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
        yield client
    app.dependency_overrides.clear()


@pytest.fixture()
def au_client_nonadmin(au_engine):
    """TestClient without admin override — _require_admin returns 403."""
    from app.main import app

    def override_db():
        Session = sessionmaker(bind=au_engine)
        session = Session()
        try:
            yield session
        finally:
            session.close()

    app.dependency_overrides[get_db] = override_db
    with TestClient(app, base_url="http://localhost", raise_server_exceptions=False) as client:
        yield client
    app.dependency_overrides.clear()


def _make_file(session, owner_id: str, n: int = 1) -> list[FileRecord]:
    """Insert *n* FileRecord rows for the given owner."""
    records = []
    for i in range(n):
        rec = FileRecord(
            filehash=f"hash-{owner_id}-{i}",
            original_filename=f"doc{i}.pdf",
            local_filename=f"/tmp/{owner_id}_{i}.pdf",
            file_size=1024,
            mime_type="application/pdf",
            is_duplicate=False,
            owner_id=owner_id,
        )
        session.add(rec)
        records.append(rec)
    session.commit()
    return records


def _make_profile(session, user_id: str, **kwargs) -> UserProfile:
    """Insert a UserProfile row."""
    kwargs.setdefault("is_blocked", False)
    profile = UserProfile(user_id=user_id, **kwargs)
    session.add(profile)
    session.commit()
    session.refresh(profile)
    return profile


# ---------------------------------------------------------------------------
# Authentication tests
# ---------------------------------------------------------------------------


class TestAdminUsersAuth:
    """Endpoints must be restricted to admin users."""

    @pytest.mark.unit
    def test_require_admin_raises_403_when_no_user(self):
        """_require_admin raises 403 when no user in session."""
        from app.api.admin_users import _require_admin

        mock_request = MagicMock()
        mock_request.session = {}

        with pytest.raises(HTTPException) as exc_info:
            _require_admin(mock_request)
        assert exc_info.value.status_code == 403

    @pytest.mark.unit
    def test_require_admin_raises_403_for_non_admin(self):
        """_require_admin raises 403 for a non-admin user."""
        from app.api.admin_users import _require_admin

        mock_request = MagicMock()
        mock_request.session = {"user": {"email": "user@test.com", "is_admin": False}}

        with pytest.raises(HTTPException) as exc_info:
            _require_admin(mock_request)
        assert exc_info.value.status_code == 403

    @pytest.mark.unit
    def test_require_admin_returns_user_for_admin(self):
        """_require_admin returns the user dict for an admin."""
        from app.api.admin_users import _require_admin

        mock_request = MagicMock()
        user = {"email": "admin@test.com", "is_admin": True}
        mock_request.session = {"user": user}

        result = _require_admin(mock_request)
        assert result == user

    @pytest.mark.integration
    def test_list_users_requires_admin(self, au_client_nonadmin):
        """GET /api/admin/users/ returns 403 for non-admins."""
        resp = au_client_nonadmin.get("/api/admin/users/")
        assert resp.status_code == 403

    @pytest.mark.integration
    def test_put_user_requires_admin(self, au_client_nonadmin):
        """PUT /api/admin/users/<id> returns 403 for non-admins."""
        resp = au_client_nonadmin.put(
            "/api/admin/users/user@example.com",
            json={"is_blocked": False},
        )
        assert resp.status_code == 403

    @pytest.mark.integration
    def test_delete_user_requires_admin(self, au_client_nonadmin):
        """DELETE /api/admin/users/<id> returns 403 for non-admins."""
        resp = au_client_nonadmin.delete("/api/admin/users/user@example.com")
        assert resp.status_code == 403

    @pytest.mark.integration
    def test_get_user_requires_admin(self, au_client_nonadmin):
        """GET /api/admin/users/<id> returns 403 for non-admins."""
        resp = au_client_nonadmin.get("/api/admin/users/user@example.com")
        assert resp.status_code == 403


# ---------------------------------------------------------------------------
# List endpoint
# ---------------------------------------------------------------------------


class TestListUsers:
    """Tests for GET /api/admin/users/."""

    @pytest.mark.unit
    def test_empty_returns_empty_list(self, au_client):
        """No users → empty list with total=0."""
        resp = au_client.get("/api/admin/users/")
        assert resp.status_code == 200
        data = resp.json()
        assert data["users"] == []
        assert data["total"] == 0

    @pytest.mark.unit
    def test_lists_users_with_docs(self, au_client, au_session):
        """Users who have documents appear in the list."""
        _make_file(au_session, "alice@example.com", 3)
        _make_file(au_session, "bob@example.com", 1)

        resp = au_client.get("/api/admin/users/")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 2

        ids = {u["user_id"] for u in data["users"]}
        assert "alice@example.com" in ids
        assert "bob@example.com" in ids

    @pytest.mark.unit
    def test_document_count_correct(self, au_client, au_session):
        """document_count reflects the number of files owned by each user."""
        _make_file(au_session, "carol@example.com", 5)

        resp = au_client.get("/api/admin/users/")
        assert resp.status_code == 200
        carol = next(u for u in resp.json()["users"] if u["user_id"] == "carol@example.com")
        assert carol["document_count"] == 5

    @pytest.mark.unit
    def test_lists_profile_only_users(self, au_client, au_session):
        """Users with a profile but no documents still appear."""
        _make_profile(au_session, "profileonly@example.com", display_name="Profile Only")

        resp = au_client.get("/api/admin/users/")
        assert resp.status_code == 200
        ids = {u["user_id"] for u in resp.json()["users"]}
        assert "profileonly@example.com" in ids

    @pytest.mark.unit
    def test_search_filter(self, au_client, au_session):
        """q= parameter filters by user_id substring."""
        _make_file(au_session, "alice@example.com")
        _make_file(au_session, "bob@example.com")

        resp = au_client.get("/api/admin/users/?q=alice")
        assert resp.status_code == 200
        data = resp.json()
        assert data["total"] == 1
        assert data["users"][0]["user_id"] == "alice@example.com"

    @pytest.mark.unit
    def test_pagination(self, au_client, au_session):
        """per_page and page parameters paginate results."""
        for i in range(6):
            _make_file(au_session, f"user{i:02d}@example.com")

        resp = au_client.get("/api/admin/users/?page=1&per_page=3")
        assert resp.status_code == 200
        data = resp.json()
        assert len(data["users"]) == 3
        assert data["total"] == 6
        assert data["pages"] == 2

    @pytest.mark.unit
    def test_profile_data_merged(self, au_client, au_session):
        """Profile fields (is_blocked, daily_upload_limit, …) are merged into list items."""
        _make_file(au_session, "managed@example.com")
        _make_profile(
            au_session,
            "managed@example.com",
            display_name="Managed User",
            daily_upload_limit=10,
            is_blocked=True,
        )

        resp = au_client.get("/api/admin/users/")
        assert resp.status_code == 200
        user = next(u for u in resp.json()["users"] if u["user_id"] == "managed@example.com")
        assert user["display_name"] == "Managed User"
        assert user["daily_upload_limit"] == 10
        assert user["is_blocked"] is True


# ---------------------------------------------------------------------------
# Get single user
# ---------------------------------------------------------------------------


class TestGetUser:
    """Tests for GET /api/admin/users/{user_id}."""

    @pytest.mark.unit
    def test_get_user_with_docs(self, au_client, au_session):
        """Returns correct document_count and last_upload."""
        _make_file(au_session, "dana@example.com", 2)

        resp = au_client.get("/api/admin/users/dana@example.com")
        assert resp.status_code == 200
        data = resp.json()
        assert data["user_id"] == "dana@example.com"
        assert data["document_count"] == 2
        assert data["is_blocked"] is False

    @pytest.mark.unit
    def test_get_user_with_profile(self, au_client, au_session):
        """Returns profile data when a profile exists."""
        _make_file(au_session, "evan@example.com")
        _make_profile(au_session, "evan@example.com", notes="VIP user", daily_upload_limit=50)

        resp = au_client.get("/api/admin/users/evan@example.com")
        assert resp.status_code == 200
        data = resp.json()
        assert data["notes"] == "VIP user"
        assert data["daily_upload_limit"] == 50
        assert data["profile"] is not None

    @pytest.mark.unit
    def test_get_user_no_docs_no_profile_returns_defaults(self, au_client):
        """User with no docs and no profile returns zero counts and defaults."""
        resp = au_client.get("/api/admin/users/unknown@example.com")
        assert resp.status_code == 200
        data = resp.json()
        assert data["document_count"] == 0
        assert data["profile"] is None
        assert data["is_blocked"] is False


# ---------------------------------------------------------------------------
# Upsert (PUT) endpoint
# ---------------------------------------------------------------------------


class TestUpsertUserProfile:
    """Tests for PUT /api/admin/users/{user_id}."""

    @pytest.mark.unit
    def test_create_profile(self, au_client, au_session):
        """PUT on a user without a profile creates it."""
        resp = au_client.put(
            "/api/admin/users/newuser@example.com",
            json={"display_name": "New User", "daily_upload_limit": 20, "is_blocked": False},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["user_id"] == "newuser@example.com"
        assert data["display_name"] == "New User"
        assert data["daily_upload_limit"] == 20

        # Persisted in DB
        profile = au_session.query(UserProfile).filter_by(user_id="newuser@example.com").first()
        assert profile is not None
        assert profile.display_name == "New User"

    @pytest.mark.unit
    def test_update_existing_profile(self, au_client, au_session):
        """PUT on an existing profile updates it."""
        _make_profile(au_session, "existing@example.com", display_name="Old Name")

        resp = au_client.put(
            "/api/admin/users/existing@example.com",
            json={"display_name": "New Name", "is_blocked": True},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["display_name"] == "New Name"
        assert data["is_blocked"] is True

    @pytest.mark.unit
    def test_block_user(self, au_client, au_session):
        """Setting is_blocked=True stores correctly."""
        resp = au_client.put(
            "/api/admin/users/blocked@example.com",
            json={"is_blocked": True},
        )
        assert resp.status_code == 200
        assert resp.json()["is_blocked"] is True

    @pytest.mark.unit
    def test_null_upload_limit(self, au_client, au_session):
        """daily_upload_limit can be null (use global default)."""
        resp = au_client.put(
            "/api/admin/users/nulllimit@example.com",
            json={"daily_upload_limit": None, "is_blocked": False},
        )
        assert resp.status_code == 200
        assert resp.json()["daily_upload_limit"] is None

    @pytest.mark.unit
    def test_zero_upload_limit_means_unlimited(self, au_client):
        """daily_upload_limit=0 is a valid value meaning 'unlimited'."""
        resp = au_client.put(
            "/api/admin/users/zerolimit@example.com",
            json={"daily_upload_limit": 0, "is_blocked": False},
        )
        assert resp.status_code == 200
        assert resp.json()["daily_upload_limit"] == 0


# ---------------------------------------------------------------------------
# Delete endpoint
# ---------------------------------------------------------------------------


class TestDeleteUserProfile:
    """Tests for DELETE /api/admin/users/{user_id}."""

    @pytest.mark.unit
    def test_delete_existing_profile(self, au_client, au_session):
        """DELETE removes an existing profile; returns 204."""
        _make_profile(au_session, "todelete@example.com")

        resp = au_client.delete("/api/admin/users/todelete@example.com")
        assert resp.status_code == 204

        remaining = au_session.query(UserProfile).filter_by(user_id="todelete@example.com").first()
        assert remaining is None

    @pytest.mark.unit
    def test_delete_nonexistent_profile_returns_404(self, au_client):
        """DELETE on unknown user_id returns 404."""
        resp = au_client.delete("/api/admin/users/doesnotexist@example.com")
        assert resp.status_code == 404

    @pytest.mark.unit
    def test_delete_profile_does_not_remove_documents(self, au_client, au_session):
        """Deleting a profile must not remove documents owned by that user."""
        _make_file(au_session, "hasfiles@example.com", 3)
        _make_profile(au_session, "hasfiles@example.com")

        resp = au_client.delete("/api/admin/users/hasfiles@example.com")
        assert resp.status_code == 204

        doc_count = au_session.query(FileRecord).filter_by(owner_id="hasfiles@example.com").count()
        assert doc_count == 3


# ---------------------------------------------------------------------------
# Model tests
# ---------------------------------------------------------------------------


class TestUserProfileModel:
    """Unit tests for the UserProfile SQLAlchemy model."""

    @pytest.mark.unit
    def test_user_profile_has_required_columns(self, au_session):
        """UserProfile can be created with minimal required fields."""
        profile = UserProfile(user_id="test@example.com", is_blocked=False)
        au_session.add(profile)
        au_session.commit()
        au_session.refresh(profile)

        assert profile.id is not None
        assert profile.user_id == "test@example.com"
        assert profile.is_blocked is False
        assert profile.display_name is None
        assert profile.daily_upload_limit is None
        assert profile.notes is None

    @pytest.mark.unit
    def test_user_profile_unique_user_id(self, au_session):
        """Two profiles with the same user_id should raise an integrity error."""
        from sqlalchemy.exc import IntegrityError

        au_session.add(UserProfile(user_id="dup@example.com", is_blocked=False))
        au_session.commit()
        au_session.add(UserProfile(user_id="dup@example.com", is_blocked=False))
        with pytest.raises(IntegrityError):
            au_session.commit()
        au_session.rollback()


# ---------------------------------------------------------------------------
# Complimentary plan tests
# ---------------------------------------------------------------------------


class TestComplimentaryPlan:
    """Tests for the is_complimentary field and admin auto-creation logic."""

    @pytest.mark.unit
    def test_create_profile_with_complimentary_flag(self, au_client, au_session):
        """PUT can create a profile with is_complimentary=True."""
        resp = au_client.put(
            "/api/admin/users/comp@example.com",
            json={"subscription_tier": "business", "is_complimentary": True, "is_blocked": False},
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["is_complimentary"] is True
        assert data["subscription_tier"] == "business"

        profile = au_session.query(UserProfile).filter_by(user_id="comp@example.com").first()
        assert profile is not None
        assert profile.is_complimentary is True

    @pytest.mark.unit
    def test_update_profile_set_complimentary(self, au_client, au_session):
        """PUT can toggle is_complimentary on an existing profile."""
        _make_profile(au_session, "toggle@example.com", is_complimentary=False)

        resp = au_client.put(
            "/api/admin/users/toggle@example.com",
            json={"is_blocked": False, "is_complimentary": True},
        )
        assert resp.status_code == 200
        assert resp.json()["is_complimentary"] is True

    @pytest.mark.unit
    def test_list_users_includes_complimentary_field(self, au_client, au_session):
        """GET /api/admin/users/ returns is_complimentary per user."""
        _make_profile(au_session, "complist@example.com", is_complimentary=True)

        resp = au_client.get("/api/admin/users/")
        assert resp.status_code == 200
        users = {u["user_id"]: u for u in resp.json()["users"]}
        assert "complist@example.com" in users
        assert users["complist@example.com"]["is_complimentary"] is True

    @pytest.mark.unit
    def test_get_user_includes_complimentary_field(self, au_client, au_session):
        """GET /api/admin/users/<id> returns is_complimentary in profile."""
        _make_profile(au_session, "getcomp@example.com", is_complimentary=True, subscription_tier="business")

        resp = au_client.get("/api/admin/users/getcomp%40example.com")
        assert resp.status_code == 200
        data = resp.json()
        assert data["is_complimentary"] is True
        assert data["profile"]["is_complimentary"] is True

    @pytest.mark.unit
    def test_complimentary_defaults_to_false(self, au_client, au_session):
        """Newly created profiles have is_complimentary=False by default."""
        resp = au_client.put(
            "/api/admin/users/nocomp@example.com",
            json={"is_blocked": False},
        )
        assert resp.status_code == 200
        assert resp.json()["is_complimentary"] is False

    @pytest.mark.unit
    def test_profile_model_complimentary_column(self, au_session):
        """UserProfile model stores is_complimentary correctly."""
        profile = UserProfile(user_id="modelcomp@example.com", is_complimentary=True)
        au_session.add(profile)
        au_session.commit()
        au_session.refresh(profile)
        assert profile.is_complimentary is True


# ---------------------------------------------------------------------------
# _ensure_user_profile admin auto-creation tests
# ---------------------------------------------------------------------------


class TestEnsureUserProfileAdmin:
    """Tests for _ensure_user_profile admin-specific behaviour."""

    @pytest.mark.unit
    def test_admin_login_creates_highest_tier_profile(self, au_session):
        """Admin first login creates a profile with the highest subscription tier."""
        from app.auth import _ensure_user_profile
        from app.utils.subscription import TIER_ORDER

        user_data = {
            "preferred_username": "admin",
            "email": "admin@local.docuelevate",
            "name": "Administrator",
            "is_admin": True,
        }
        _ensure_user_profile(au_session, user_data, is_admin=True)

        # user_id uses preferred_username (sub not provided)
        profile = au_session.query(UserProfile).filter_by(user_id="admin").first()
        assert profile is not None
        assert profile.subscription_tier == TIER_ORDER[-1]
        assert profile.is_complimentary is True
        assert profile.onboarding_completed is True

    @pytest.mark.unit
    def test_regular_user_login_creates_free_profile(self, au_session):
        """Regular user login creates a profile with the free tier."""
        from app.auth import _ensure_user_profile

        user_data = {
            "preferred_username": "regular",
            "email": "user@example.com",
            "name": "Regular User",
        }
        _ensure_user_profile(au_session, user_data, is_admin=False)

        # user_id uses preferred_username (sub not provided)
        profile = au_session.query(UserProfile).filter_by(user_id="regular").first()
        assert profile is not None
        assert profile.subscription_tier == "free"
        assert profile.is_complimentary is False

    @pytest.mark.unit
    def test_admin_login_sets_complimentary_on_existing_profile(self, au_session):
        """Existing admin profile gets is_complimentary=True on login."""
        existing = UserProfile(user_id="existadmin", is_complimentary=False, subscription_tier="starter")
        au_session.add(existing)
        au_session.commit()

        from app.auth import _ensure_user_profile

        user_data = {"preferred_username": "existadmin", "email": "ea@example.com"}
        _ensure_user_profile(au_session, user_data, is_admin=True)

        au_session.refresh(existing)
        assert existing.is_complimentary is True

    @pytest.mark.unit
    def test_admin_login_does_not_downgrade_existing_tier(self, au_session):
        """Existing admin profile with the highest tier keeps that tier on re-login."""
        from app.auth import _ensure_user_profile
        from app.utils.subscription import TIER_ORDER

        highest = TIER_ORDER[-1]
        existing = UserProfile(user_id="toptieradmin", is_complimentary=False, subscription_tier=highest)
        au_session.add(existing)
        au_session.commit()

        user_data = {"preferred_username": "toptieradmin", "email": "tt@example.com"}
        _ensure_user_profile(au_session, user_data, is_admin=True)

        au_session.refresh(existing)
        assert existing.subscription_tier == highest
        assert existing.is_complimentary is True

    @pytest.mark.unit
    def test_admin_login_upgrades_free_tier_on_existing_profile(self, au_session):
        """Existing admin profile on free tier gets upgraded to highest tier."""
        from app.auth import _ensure_user_profile
        from app.utils.subscription import TIER_ORDER

        existing = UserProfile(user_id="freeadmin", is_complimentary=False, subscription_tier="free")
        au_session.add(existing)
        au_session.commit()

        user_data = {"preferred_username": "freeadmin", "email": "fa@example.com"}
        _ensure_user_profile(au_session, user_data, is_admin=True)

        au_session.refresh(existing)
        assert existing.subscription_tier == TIER_ORDER[-1]
        assert existing.is_complimentary is True

    @pytest.mark.unit
    def test_ensure_user_profile_no_identifier_logs_warning(self, au_session):
        """_ensure_user_profile logs a warning when no stable user id is present."""
        from app.auth import _ensure_user_profile

        _ensure_user_profile(au_session, {}, is_admin=False)
        # No profile should have been created
        count = au_session.query(UserProfile).count()
        assert count == 0


# ---------------------------------------------------------------------------
# Local user admin management: update, send-password-reset, set-password
# ---------------------------------------------------------------------------


def _make_local_user(session, email: str = "lu@example.com", username: str = "luuser", **kwargs) -> LocalUser:
    """Insert a LocalUser row and return it."""
    from app.utils.local_auth import hash_password

    defaults = {
        "hashed_password": hash_password("password123"),
        "is_active": True,
        "is_admin": False,
    }
    defaults.update(kwargs)
    user = LocalUser(email=email, username=username, **defaults)
    session.add(user)
    session.commit()
    session.refresh(user)
    return user


class TestAdminUpdateLocalUser:
    """Tests for PATCH /api/admin/users/local/{id}."""

    @pytest.mark.unit
    def test_update_email(self, au_client, au_session):
        """PATCH can change the email address of a local user."""
        user = _make_local_user(au_session, email="old@example.com", username="updateemail")

        resp = au_client.patch(
            f"/api/admin/users/local/{user.id}",
            json={"email": "new@example.com"},
        )
        assert resp.status_code == 200
        assert resp.json()["email"] == "new@example.com"

        au_session.refresh(user)
        assert user.email == "new@example.com"

    @pytest.mark.unit
    def test_update_email_syncs_user_profile(self, au_client, au_session):
        """PATCH email also updates UserProfile.user_id for the matching profile."""
        user = _make_local_user(au_session, email="synced@example.com", username="synceduser")
        _make_profile(au_session, "synced@example.com")

        au_client.patch(
            f"/api/admin/users/local/{user.id}",
            json={"email": "synced_new@example.com"},
        )

        from app.models import UserProfile

        old_profile = au_session.query(UserProfile).filter_by(user_id="synced@example.com").first()
        new_profile = au_session.query(UserProfile).filter_by(user_id="synced_new@example.com").first()
        assert old_profile is None
        assert new_profile is not None

    @pytest.mark.unit
    def test_update_email_conflict_returns_409(self, au_client, au_session):
        """PATCH returns 409 when the new email is already taken."""
        _make_local_user(au_session, email="taken@example.com", username="takenuser")
        user = _make_local_user(au_session, email="mine@example.com", username="myuser")

        resp = au_client.patch(
            f"/api/admin/users/local/{user.id}",
            json={"email": "taken@example.com"},
        )
        assert resp.status_code == 409

    @pytest.mark.unit
    def test_update_is_admin(self, au_client, au_session):
        """PATCH can grant or revoke admin privileges."""
        user = _make_local_user(au_session, email="grantadmin@example.com", username="grantadmin")
        assert user.is_admin is False

        resp = au_client.patch(
            f"/api/admin/users/local/{user.id}",
            json={"is_admin": True},
        )
        assert resp.status_code == 200
        assert resp.json()["is_admin"] is True

        au_session.refresh(user)
        assert user.is_admin is True

    @pytest.mark.unit
    def test_update_is_active(self, au_client, au_session):
        """PATCH can deactivate a user account."""
        user = _make_local_user(au_session, email="deactivate@example.com", username="deactivateuser")

        resp = au_client.patch(
            f"/api/admin/users/local/{user.id}",
            json={"is_active": False},
        )
        assert resp.status_code == 200
        assert resp.json()["is_active"] is False

        au_session.refresh(user)
        assert user.is_active is False

    @pytest.mark.unit
    def test_update_display_name(self, au_client, au_session):
        """PATCH can update the display name."""
        user = _make_local_user(au_session, email="displayname@example.com", username="displaynameuser")

        resp = au_client.patch(
            f"/api/admin/users/local/{user.id}",
            json={"display_name": "Alice Wonderland"},
        )
        assert resp.status_code == 200
        assert resp.json()["display_name"] == "Alice Wonderland"

    @pytest.mark.unit
    def test_update_nonexistent_user_returns_404(self, au_client):
        """PATCH on unknown ID returns 404."""
        resp = au_client.patch("/api/admin/users/local/99999", json={"email": "x@example.com"})
        assert resp.status_code == 404


class TestAdminSendPasswordReset:
    """Tests for POST /api/admin/users/local/{id}/send-password-reset."""

    @pytest.mark.unit
    def test_send_reset_email_success(self, au_client, au_session):
        """Returns sent=True when SMTP is configured and sending succeeds."""
        from unittest.mock import patch

        user = _make_local_user(au_session, email="resetme@example.com", username="resetmeuser")

        with (
            patch("app.api.admin_users.settings") as mock_settings,
            patch("app.api.admin_users.send_password_reset_email") as mock_send,
        ):
            mock_settings.email_host = "smtp.example.com"
            mock_settings.version = "test"
            resp = au_client.post(f"/api/admin/users/local/{user.id}/send-password-reset")

        assert resp.status_code == 200
        data = resp.json()
        assert data["sent"] is True
        assert data["email"] == "resetme@example.com"
        mock_send.assert_called_once()

    @pytest.mark.unit
    def test_send_reset_email_no_smtp_returns_not_sent(self, au_client, au_session):
        """Returns sent=False with reason when SMTP is not configured."""
        from unittest.mock import patch

        user = _make_local_user(au_session, email="nosmtp@example.com", username="nosmtpuser")

        with patch("app.api.admin_users.settings") as mock_settings:
            mock_settings.email_host = ""
            resp = au_client.post(f"/api/admin/users/local/{user.id}/send-password-reset")

        assert resp.status_code == 200
        data = resp.json()
        assert data["sent"] is False
        assert "smtp" in data["reason"].lower()

    @pytest.mark.unit
    def test_send_reset_email_smtp_failure_returns_not_sent(self, au_client, au_session):
        """Returns sent=False with reason when SMTP sending fails."""
        from unittest.mock import patch

        user = _make_local_user(au_session, email="smtperr@example.com", username="smtperruser")

        with (
            patch("app.api.admin_users.settings") as mock_settings,
            patch("app.api.admin_users.send_password_reset_email", side_effect=RuntimeError("connection refused")),
        ):
            mock_settings.email_host = "smtp.example.com"
            resp = au_client.post(f"/api/admin/users/local/{user.id}/send-password-reset")

        assert resp.status_code == 200
        assert resp.json()["sent"] is False

    @pytest.mark.unit
    def test_send_reset_email_unknown_user_returns_404(self, au_client):
        """Returns 404 for unknown local_user_id."""
        resp = au_client.post("/api/admin/users/local/99999/send-password-reset")
        assert resp.status_code == 404

    @pytest.mark.unit
    def test_send_reset_stores_token(self, au_client, au_session):
        """Password reset token is persisted to the DB."""
        from unittest.mock import patch

        user = _make_local_user(au_session, email="tokenstore@example.com", username="tokenstoreuser")
        assert user.password_reset_token is None

        with (
            patch("app.api.admin_users.settings") as mock_settings,
            patch("app.api.admin_users.send_password_reset_email"),
        ):
            mock_settings.email_host = "smtp.example.com"
            au_client.post(f"/api/admin/users/local/{user.id}/send-password-reset")

        au_session.refresh(user)
        assert user.password_reset_token is not None
        assert user.password_reset_sent_at is not None


class TestAdminSetPassword:
    """Tests for POST /api/admin/users/local/{id}/set-password."""

    @pytest.mark.unit
    def test_set_password_success(self, au_client, au_session):
        """Returns updated=True and changes the hashed password."""
        from app.utils.local_auth import verify_password

        user = _make_local_user(au_session, email="setpw@example.com", username="setpwuser")

        resp = au_client.post(
            f"/api/admin/users/local/{user.id}/set-password",
            json={"password": "brandnewpassword"},
        )
        assert resp.status_code == 200
        assert resp.json()["updated"] is True

        au_session.refresh(user)
        assert verify_password("brandnewpassword", user.hashed_password)

    @pytest.mark.unit
    def test_set_password_too_short_returns_422(self, au_client, au_session):
        """Returns 422 when password is shorter than 8 characters."""
        user = _make_local_user(au_session, email="shortpw@example.com", username="shortpwuser")

        resp = au_client.post(
            f"/api/admin/users/local/{user.id}/set-password",
            json={"password": "short"},
        )
        assert resp.status_code == 422

    @pytest.mark.unit
    def test_set_password_clears_reset_token(self, au_client, au_session):
        """Setting a password clears any outstanding password_reset_token."""
        from app.utils.local_auth import generate_token

        user = _make_local_user(au_session, email="cleartok@example.com", username="cleartokuser")
        user.password_reset_token = generate_token()
        user.password_reset_sent_at = datetime.now(tz=timezone.utc)
        au_session.commit()

        au_client.post(
            f"/api/admin/users/local/{user.id}/set-password",
            json={"password": "clearedpassword"},
        )

        au_session.refresh(user)
        assert user.password_reset_token is None
        assert user.password_reset_sent_at is None

    @pytest.mark.unit
    def test_set_password_unknown_user_returns_404(self, au_client):
        """Returns 404 for unknown local_user_id."""
        resp = au_client.post(
            "/api/admin/users/local/99999/set-password",
            json={"password": "doesnotmatter"},
        )
        assert resp.status_code == 404
