"""WS5.3 — staff user-management surface tests.

Pins:
  * Permission gate (anonymous → 302 to login; non-staff → 403)
  * List filters (q, role, status, institution)
  * Pending queue + approve / reject endpoints (incl. promote-to-Institution)
  * Activate / deactivate guards (cannot deactivate self / superuser)
  * Bulk CSV import (header validation, dedup, role validation, audit)
"""
from __future__ import annotations

import io

import pytest
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse

from apps.core.authentication.models import Institution
from apps.core.permissions.roles import UserRole

User = get_user_model()


def _staff(email: str = "staff@example.com"):
    """Return a staff user permitted to manage users."""
    u = User.objects.create_user(email=email, password="x", is_staff=True)
    return u


# ---------------------------------------------------------------------- gate


@pytest.mark.django_db
def test_user_list_blocks_anonymous(client):
    resp = client.get(reverse("accounts:manage_users"))
    assert resp.status_code in (302, 403)


@pytest.mark.django_db
def test_user_list_blocks_non_staff(client, django_user_model):
    learner = django_user_model.objects.create_user(email="learner@x.com", password="x")
    client.force_login(learner)
    resp = client.get(reverse("accounts:manage_users"))
    assert resp.status_code == 403


@pytest.mark.django_db
def test_user_list_renders_for_staff(client):
    staff = _staff()
    User.objects.create_user(email="alice@x.com", password="x", first_name="Alice")
    client.force_login(staff)
    resp = client.get(reverse("accounts:manage_users"))
    assert resp.status_code == 200
    assert b"alice@x.com" in resp.content


# -------------------------------------------------------------------- list filters


@pytest.mark.django_db
def test_user_list_search_q(client):
    staff = _staff()
    User.objects.create_user(email="match-search@x.com", password="x", first_name="Bob")
    User.objects.create_user(email="other@x.com", password="x", first_name="Cara")
    client.force_login(staff)
    resp = client.get(reverse("accounts:manage_users") + "?q=match-search")
    assert resp.status_code == 200
    assert b"match-search@x.com" in resp.content
    assert b"other@x.com" not in resp.content


@pytest.mark.django_db
def test_user_list_filter_status_pending(client):
    staff = _staff()
    p = User.objects.create_user(email="pending@x.com", password="x")
    p.pending_admin_review = True
    p.non_partner_institution_name = "Mystery U"
    p.save()
    User.objects.create_user(email="active@x.com", password="x")
    client.force_login(staff)
    resp = client.get(reverse("accounts:manage_users") + "?status=pending")
    assert b"pending@x.com" in resp.content
    assert b"active@x.com" not in resp.content


@pytest.mark.django_db
def test_user_list_filter_role(client):
    staff = _staff()
    User.objects.create_user(email="i@x.com", password="x", role=UserRole.INSTRUCTOR)
    User.objects.create_user(email="l@x.com", password="x", role=UserRole.LEARNER)
    client.force_login(staff)
    resp = client.get(reverse("accounts:manage_users") + f"?role={UserRole.INSTRUCTOR}")
    assert b"i@x.com" in resp.content
    assert b"l@x.com" not in resp.content


# ------------------------------------------------------------ pending queue


@pytest.mark.django_db(transaction=True)
def test_approve_pending_clears_flag(client):
    staff = _staff()
    pending = User.objects.create_user(email="approve@x.com", password="x")
    pending.pending_admin_review = True
    pending.non_partner_institution_name = "New Uni"
    pending.save()
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_approve_pending", kwargs={"pk": pending.pk}))
    assert resp.status_code == 302
    pending.refresh_from_db()
    assert pending.pending_admin_review is False
    assert pending.is_active is True


@pytest.mark.django_db(transaction=True)
def test_approve_promotes_institution_when_requested(client):
    staff = _staff()
    pending = User.objects.create_user(email="promote@x.com", password="x")
    pending.pending_admin_review = True
    pending.non_partner_institution_name = "Promoted Uni"
    pending.save()
    client.force_login(staff)
    resp = client.post(
        reverse("accounts:manage_approve_pending", kwargs={"pk": pending.pk}),
        {"promote_institution": "on"},
    )
    assert resp.status_code == 302
    pending.refresh_from_db()
    assert pending.institution is not None
    assert pending.institution.name == "Promoted Uni"
    assert pending.non_partner_institution_name == ""
    assert Institution.objects.filter(name="Promoted Uni").exists()


@pytest.mark.django_db(transaction=True)
def test_reject_requires_reason(client):
    staff = _staff()
    pending = User.objects.create_user(email="reject@x.com", password="x")
    pending.pending_admin_review = True
    pending.save()
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_reject_pending", kwargs={"pk": pending.pk}))
    assert resp.status_code == 302  # redirects with error message
    pending.refresh_from_db()
    # Still pending because no reason was given.
    assert pending.pending_admin_review is True
    assert pending.is_active is True


@pytest.mark.django_db(transaction=True)
def test_reject_with_reason_deactivates(client):
    staff = _staff()
    pending = User.objects.create_user(email="reject2@x.com", password="x")
    pending.pending_admin_review = True
    pending.save()
    client.force_login(staff)
    resp = client.post(
        reverse("accounts:manage_reject_pending", kwargs={"pk": pending.pk}),
        {"reason": "Couldn't verify the institution"},
    )
    assert resp.status_code == 302
    pending.refresh_from_db()
    assert pending.is_active is False
    assert pending.pending_admin_review is False


# ----------------------------------------------------- activate / deactivate


@pytest.mark.django_db(transaction=True)
def test_deactivate_user(client):
    staff = _staff()
    target = User.objects.create_user(email="target@x.com", password="x")
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_deactivate_user", kwargs={"pk": target.pk}))
    assert resp.status_code == 302
    target.refresh_from_db()
    assert target.is_active is False


@pytest.mark.django_db
def test_cannot_deactivate_self(client):
    staff = _staff()
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_deactivate_user", kwargs={"pk": staff.pk}))
    staff.refresh_from_db()
    assert staff.is_active is True


@pytest.mark.django_db
def test_cannot_deactivate_superuser(client):
    staff = _staff()
    su = User.objects.create_superuser(email="su@x.com", password="x")
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_deactivate_user", kwargs={"pk": su.pk}))
    su.refresh_from_db()
    assert su.is_active is True


@pytest.mark.django_db(transaction=True)
def test_activate_user(client):
    staff = _staff()
    target = User.objects.create_user(email="reactivate@x.com", password="x")
    target.is_active = False
    target.save()
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_activate_user", kwargs={"pk": target.pk}))
    assert resp.status_code == 302
    target.refresh_from_db()
    assert target.is_active is True


# ----------------------------------------------------------------- delete


@pytest.mark.django_db(transaction=True)
def test_delete_user_success(client):
    staff = _staff()
    target = User.objects.create_user(email="delete-me@x.com", password="x")
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_delete_user", kwargs={"pk": target.pk}))
    assert resp.status_code == 302
    assert not User.objects.filter(pk=target.pk).exists()


@pytest.mark.django_db
def test_cannot_delete_self(client):
    staff = _staff()
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_delete_user", kwargs={"pk": staff.pk}))
    assert resp.status_code == 302
    assert User.objects.filter(pk=staff.pk).exists()


@pytest.mark.django_db
def test_cannot_delete_superuser(client):
    staff = _staff()
    su = User.objects.create_superuser(email="su-del@x.com", password="x")
    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_delete_user", kwargs={"pk": su.pk}))
    assert resp.status_code == 302
    assert User.objects.filter(pk=su.pk).exists()


@pytest.mark.django_db
def test_delete_view_blocks_non_staff(client, django_user_model):
    learner = django_user_model.objects.create_user(email="not-staff-del@x.com", password="x")
    target = django_user_model.objects.create_user(email="victim-del@x.com", password="x")
    client.force_login(learner)
    resp = client.post(reverse("accounts:manage_delete_user", kwargs={"pk": target.pk}))
    assert resp.status_code == 403
    assert User.objects.filter(pk=target.pk).exists()


@pytest.mark.django_db(transaction=True)
def test_delete_user_falls_back_to_deactivate_on_protected_error(client, monkeypatch):
    """When the user has linked records enforced with on_delete=PROTECT, the
    hard delete must never 500 — it should deactivate the account instead."""
    from django.db.models import ProtectedError

    staff = _staff()
    target = User.objects.create_user(email="protected@x.com", password="x")

    def _raise_protected(self, *args, **kwargs):
        raise ProtectedError("linked records exist", [])

    monkeypatch.setattr(User, "delete", _raise_protected)

    client.force_login(staff)
    resp = client.post(reverse("accounts:manage_delete_user", kwargs={"pk": target.pk}))
    assert resp.status_code == 302
    target.refresh_from_db()
    assert target.is_active is False


# --------------------------------------------------------------- bulk import


def _csv_file(text: str, name: str = "import.csv"):
    from django.core.files.uploadedfile import SimpleUploadedFile

    return SimpleUploadedFile(name, text.encode("utf-8"), content_type="text/csv")


@pytest.mark.django_db
def test_bulk_import_creates_users(client):
    staff = _staff()
    client.force_login(staff)
    csv_body = (
        "email,first_name,last_name,role\n"
        "import-a@x.com,Adam,Imp,learner\n"
        "import-b@x.com,Bea,Imp,instructor\n"
    )
    resp = client.post(
        reverse("accounts:manage_bulk_import"),
        {"csv": _csv_file(csv_body)},
    )
    assert resp.status_code == 200
    assert User.objects.filter(email="import-a@x.com").exists()
    assert User.objects.filter(email="import-b@x.com").exists()
    assert User.objects.get(email="import-b@x.com").role == UserRole.INSTRUCTOR


@pytest.mark.django_db
def test_bulk_import_rejects_missing_headers(client):
    staff = _staff()
    client.force_login(staff)
    csv_body = "email\nimport-c@x.com\n"
    resp = client.post(
        reverse("accounts:manage_bulk_import"),
        {"csv": _csv_file(csv_body)},
    )
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "Missing required column" in body
    assert not User.objects.filter(email="import-c@x.com").exists()


@pytest.mark.django_db
def test_bulk_import_dedups_existing_email(client):
    staff = _staff()
    User.objects.create_user(email="dup@x.com", password="x")
    client.force_login(staff)
    csv_body = (
        "email,first_name,last_name\n"
        "dup@x.com,Dup,Existing\n"
        "fresh@x.com,Fresh,User\n"
    )
    resp = client.post(
        reverse("accounts:manage_bulk_import"),
        {"csv": _csv_file(csv_body)},
    )
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "Email already exists" in body
    # The new one was still created.
    assert User.objects.filter(email="fresh@x.com").exists()


@pytest.mark.django_db
def test_bulk_import_rejects_unknown_role(client):
    staff = _staff()
    client.force_login(staff)
    csv_body = (
        "email,first_name,last_name,role\n"
        "import-bad@x.com,Bad,Role,unicorn_overlord\n"
    )
    resp = client.post(
        reverse("accounts:manage_bulk_import"),
        {"csv": _csv_file(csv_body)},
    )
    assert resp.status_code == 200
    body = resp.content.decode()
    assert "Unknown role" in body
    assert not User.objects.filter(email="import-bad@x.com").exists()


@pytest.mark.django_db
def test_bulk_import_resolves_institution_by_name(client):
    staff = _staff()
    inst = Institution.objects.create(name="Makerere University", is_active=True)
    client.force_login(staff)
    csv_body = (
        "email,first_name,last_name,institution\n"
        "import-inst@x.com,Inst,User,Makerere University\n"
    )
    client.post(reverse("accounts:manage_bulk_import"), {"csv": _csv_file(csv_body)})
    u = User.objects.get(email="import-inst@x.com")
    assert u.institution_id == inst.pk


# ----- WS5.3 UI follow-ups -------------------------------------------------


@pytest.mark.django_db
def test_bulk_import_skips_comment_lines(client):
    """The downloaded template ships with `#`-prefixed example rows. They must
    not turn into real users if the operator forgets to delete them."""
    staff = _staff()
    client.force_login(staff)
    csv_body = (
        "email,first_name,last_name\n"
        "# example.learner@x.com,Adaeze,Eze\n"
        "real-user@x.com,Real,User\n"
    )
    resp = client.post(reverse("accounts:manage_bulk_import"), {"csv": _csv_file(csv_body)})
    assert resp.status_code == 200
    assert User.objects.filter(email="real-user@x.com").exists()
    assert not User.objects.filter(email__startswith="# example").exists()
    assert not User.objects.filter(email="example.learner@x.com").exists()


@pytest.mark.django_db
def test_bulk_import_template_endpoint_serves_csv(client):
    staff = _staff()
    client.force_login(staff)
    resp = client.get(reverse("accounts:manage_bulk_import_template"))
    assert resp.status_code == 200
    assert resp["Content-Type"] == "text/csv"
    assert "iilmp-user-import-template.csv" in resp["Content-Disposition"]
    body = resp.content.decode()
    assert body.startswith("email,first_name,last_name,phone,institution,role")
    assert "# example.learner" in body  # commented example rows


@pytest.mark.django_db
def test_bulk_import_template_blocks_non_staff(client, django_user_model):
    learner = django_user_model.objects.create_user(email="t-bait@x.com", password="x")
    client.force_login(learner)
    resp = client.get(reverse("accounts:manage_bulk_import_template"))
    assert resp.status_code in (302, 403)


@pytest.mark.django_db
def test_user_edit_page_renders(client):
    staff = _staff()
    target = User.objects.create_user(email="edit-me@x.com", password="x", first_name="Old")
    client.force_login(staff)
    resp = client.get(reverse("accounts:manage_edit_user", kwargs={"pk": target.pk}))
    assert resp.status_code == 200
    assert b"edit-me@x.com" in resp.content


@pytest.mark.django_db(transaction=True)
def test_user_edit_persists_role_and_institution(client):
    staff = _staff()
    target = User.objects.create_user(email="role-change@x.com", password="x")
    inst = Institution.objects.create(name="Editable U", is_active=True)
    client.force_login(staff)
    resp = client.post(
        reverse("accounts:manage_edit_user", kwargs={"pk": target.pk}),
        {
            "first_name": "Bola",
            "last_name": "Adesanya",
            "phone": "",
            "role": UserRole.INSTRUCTOR,
            "institution": inst.pk,
            "email_verified": "on",
        },
    )
    assert resp.status_code == 302
    target.refresh_from_db()
    assert target.role == UserRole.INSTRUCTOR
    assert target.institution_id == inst.pk
    assert target.first_name == "Bola"
    assert target.email_verified is True


@pytest.mark.django_db(transaction=True)
def test_user_edit_can_assign_additional_roles(client):
    staff = _staff()
    target = User.objects.create_user(email="multi-role@x.com", password="x", role=UserRole.ALUMNI)
    ct = ContentType.objects.get_for_model(Group)
    from django.contrib.auth.models import Permission

    perm, _ = Permission.objects.get_or_create(
        codename="employer_perm_a", content_type=ct, defaults={"name": "employer_perm_a"}
    )
    group = Group.objects.create(name="Employer (test)")
    group.permissions.set([perm])

    client.force_login(staff)
    resp = client.post(
        reverse("accounts:manage_edit_user", kwargs={"pk": target.pk}),
        {
            "first_name": "",
            "last_name": "",
            "phone": "",
            "role": UserRole.ALUMNI,
            "institution": "",
            "additional_roles": [group.pk],
        },
    )
    assert resp.status_code == 302
    target.refresh_from_db()
    assert target.groups.filter(pk=group.pk).exists()
    # Primary enum-mapped role group is untouched by the additional_roles sync.
    assert target.groups.filter(name=UserRole.ALUMNI).exists()
    assert target.has_perm(f"{ct.app_label}.employer_perm_a")


@pytest.mark.django_db
def test_user_edit_blocks_non_staff(client, django_user_model):
    learner = django_user_model.objects.create_user(email="not-staff@x.com", password="x")
    target = django_user_model.objects.create_user(email="victim@x.com", password="x")
    client.force_login(learner)
    resp = client.get(reverse("accounts:manage_edit_user", kwargs={"pk": target.pk}))
    assert resp.status_code == 403


@pytest.mark.django_db
def test_pending_user_review_count_tag():
    from apps.core.templatetags.user_admin_tags import pending_user_review_count

    assert pending_user_review_count() == 0
    u = User.objects.create_user(email="counted@x.com", password="x")
    u.pending_admin_review = True
    u.save()
    assert pending_user_review_count() == 1
