"""IILMP→Moodle role projection (SSO role sync)."""
from unittest import mock

import pytest
from django.contrib.auth import get_user_model
from django.test import override_settings

from apps.core.authentication import moodle_roles


class FakeMoodleClient:
    """Records WS calls; canned answer for the user lookup."""

    configured = True

    def __init__(self, lookup_result=None):
        self.calls = []
        self.lookup_result = lookup_result if lookup_result is not None else []

    def call(self, wsfunction, **params):
        self.calls.append((wsfunction, params))
        if wsfunction == "core_user_get_users_by_field":
            return self.lookup_result
        return None


def _user(role="instructor", lms_ref=None, email="sync@example.com"):
    return get_user_model().objects.create_user(
        email=email, password="pw", role=role, lms_ref=lms_ref
    )


def test_mapping_admin_and_instructor():
    class U:  # no DB needed for the pure mapping
        pass

    for role, expected in [
        ("admin", "manager"),
        ("system_admin", "manager"),
        ("rep_learning_admin", "manager"),
        ("instructor", "coursecreator"),
        ("learner", None),
        ("", None),
    ]:
        u = U()
        u.role = role
        assert moodle_roles.desired_moodle_role(u) == expected, role


@pytest.mark.django_db
def test_sync_assigns_mapped_role_via_lms_ref():
    user = _user(role="instructor", lms_ref=777)
    client = FakeMoodleClient()
    outcome = moodle_roles.sync_user(user, client=client)
    assert outcome == moodle_roles.OUTCOME_ASSIGNED
    assert client.calls == [
        (
            "core_role_assign_roles",
            {"assignments": [{"roleid": 2, "userid": 777, "contextid": 1}]},
        )
    ]


@pytest.mark.django_db
def test_sync_looks_up_by_email_and_persists_lms_ref():
    user = _user(role="admin")
    client = FakeMoodleClient(lookup_result=[{"id": 555, "email": user.email}])
    outcome = moodle_roles.sync_user(user, client=client)
    assert outcome == moodle_roles.OUTCOME_ASSIGNED
    user.refresh_from_db()
    assert user.lms_ref == 555
    assert client.calls[0][0] == "core_user_get_users_by_field"
    assert client.calls[1] == (
        "core_role_assign_roles",
        {"assignments": [{"roleid": 1, "userid": 555, "contextid": 1}]},
    )


@pytest.mark.django_db
def test_sync_reports_missing_moodle_account():
    user = _user(role="instructor")
    client = FakeMoodleClient(lookup_result=[])
    assert moodle_roles.sync_user(user, client=client) == moodle_roles.OUTCOME_NOT_IN_MOODLE


@pytest.mark.django_db
def test_sync_skips_unmapped_role_without_touching_moodle():
    user = _user(role="learner", lms_ref=42)
    client = FakeMoodleClient()
    assert moodle_roles.sync_user(user, client=client) == moodle_roles.OUTCOME_UNMAPPED
    assert client.calls == []


@pytest.mark.django_db
def test_demote_strips_managed_roles_from_unmapped_user():
    user = _user(role="learner", lms_ref=42)
    client = FakeMoodleClient()
    outcome = moodle_roles.sync_user(user, demote=True, client=client)
    assert outcome == moodle_roles.OUTCOME_DEMOTED
    (fn, params), = client.calls
    assert fn == "core_role_unassign_roles"
    stripped = {(u["roleid"], u["userid"], u["contextid"]) for u in params["unassignments"]}
    assert stripped == {(1, 42, 1), (2, 42, 1)}


@pytest.mark.django_db
def test_demote_keeps_desired_role_while_stripping_other():
    user = _user(role="instructor", lms_ref=42)
    client = FakeMoodleClient()
    outcome = moodle_roles.sync_user(user, demote=True, client=client)
    assert outcome == moodle_roles.OUTCOME_ASSIGNED
    fns = [fn for fn, _ in client.calls]
    assert fns == ["core_role_assign_roles", "core_role_unassign_roles"]
    # only manager (1) is stripped; coursecreator (2) was just assigned
    _, unassign = client.calls[1]
    assert [u["roleid"] for u in unassign["unassignments"]] == [1]


@pytest.mark.django_db
@override_settings(MOODLE_ROLE_SYNC_ENABLED=False)
def test_kill_switch():
    user = _user(role="admin", lms_ref=1)
    client = FakeMoodleClient()
    assert moodle_roles.sync_user(user, client=client) == moodle_roles.OUTCOME_DISABLED
    assert client.calls == []


@pytest.mark.django_db
def test_oidc_claims_schedule_sync_for_mapped_roles_only():
    from apps.core.authentication import oidc

    mapped = _user(role="instructor", email="teach@example.com")
    unmapped = _user(role="learner", email="learn@example.com")
    with mock.patch("apps.core.tasks.sync_moodle_role") as task:
        oidc._schedule_role_sync(mapped)
        oidc._schedule_role_sync(unmapped)
    task.apply_async.assert_called_once_with(args=[mapped.pk], countdown=20)


@pytest.mark.django_db
def test_management_command_single_email(capsys):
    from django.core.management import call_command

    user = _user(role="admin", lms_ref=9)
    with mock.patch.object(moodle_roles, "MoodleClient", return_value=FakeMoodleClient()):
        call_command("sync_moodle_roles", email=user.email)
    out = capsys.readouterr().out
    assert "assigned=1" in out
