import pytest
from django.core import mail
from django.urls import reverse


@pytest.mark.django_db
def test_login_success(client):
    from django.contrib.auth import get_user_model

    User = get_user_model()
    User.objects.create_user(email="u@example.com", password="pw", email_verified=True)
    url = reverse("accounts:login")
    resp = client.post(url, {"email": "u@example.com", "password": "pw"})
    assert resp.status_code in (302, 303)
    assert resp.url == "/dashboard/"


@pytest.mark.django_db
def test_login_invalid(client):
    url = reverse("accounts:login")
    resp = client.post(url, {"email": "nope@example.com", "password": "bad"})
    assert resp.status_code == 200


@pytest.mark.django_db
def test_login_honours_safe_next(client):
    """OAuth SSO: a same-host ``next`` (e.g. /o/authorize/...) is followed."""
    from django.contrib.auth import get_user_model

    get_user_model().objects.create_user(
        email="sso@example.com", password="pw", email_verified=True
    )
    url = reverse("accounts:login")
    nxt = "/o/authorize/?client_id=moodle-sso&response_type=code"
    resp = client.post(url, {"email": "sso@example.com", "password": "pw", "next": nxt})
    assert resp.status_code in (302, 303)
    assert resp.url == nxt


@pytest.mark.django_db
def test_login_rejects_offsite_next(client):
    """An off-host ``next`` must not be honoured (open-redirect guard)."""
    from django.contrib.auth import get_user_model

    get_user_model().objects.create_user(
        email="safe@example.com", password="pw", email_verified=True
    )
    url = reverse("accounts:login")
    resp = client.post(
        url, {"email": "safe@example.com", "password": "pw", "next": "https://evil.example/"}
    )
    assert resp.status_code in (302, 303)
    assert resp.url == "/dashboard/"


@pytest.mark.django_db
def test_login_get_renders_next_hidden_field(client):
    """The ``next`` from the query string is carried into the form as a hidden input."""
    url = reverse("accounts:login")
    resp = client.get(url, {"next": "/o/authorize/?client_id=moodle-sso"})
    assert resp.status_code == 200
    assert b'name="next"' in resp.content
    assert b"/o/authorize/" in resp.content


@pytest.mark.django_db
def test_authenticated_login_follows_next(client):
    """Already-authenticated users hitting /login/?next= are bounced to next."""
    from django.contrib.auth import get_user_model

    user = get_user_model().objects.create_user(
        email="in@example.com", password="pw", email_verified=True
    )
    client.force_login(user)
    resp = client.get(reverse("accounts:login"), {"next": "/o/authorize/?x=1"})
    assert resp.status_code in (302, 303)
    assert resp.url == "/o/authorize/?x=1"


@pytest.mark.django_db
def test_password_reset_sends_email(client, settings, django_user_model):
    django_user_model.objects.create_user(email="reset@example.com", password="secret123")
    settings.EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
    url = reverse("accounts:password_reset")
    resp = client.post(url, {"email": "reset@example.com"})
    assert resp.status_code == 302
    assert len(mail.outbox) == 1
    assert mail.outbox[0].to == ["reset@example.com"]
    assert "password-reset" in mail.outbox[0].body


@pytest.mark.django_db
def test_password_change_requires_login(client):
    resp = client.get(reverse("accounts:password_change"))
    assert resp.status_code == 302
    assert "/accounts/login/" in resp.url


@pytest.mark.django_db
def test_password_change_success_keeps_session(client, django_user_model):
    user = django_user_model.objects.create_user(
        email="pw@example.com", password="OldPass!2026", email_verified=True
    )
    client.force_login(user)
    resp = client.post(
        reverse("accounts:password_change"),
        {
            "old_password": "OldPass!2026",
            "new_password1": "BrandNew!2026",
            "new_password2": "BrandNew!2026",
        },
    )
    assert resp.status_code == 302
    assert resp.url == reverse("accounts:profile")
    user.refresh_from_db()
    assert user.check_password("BrandNew!2026")
    # update_session_auth_hash keeps the user authenticated after the change.
    assert client.get(reverse("accounts:profile")).status_code == 200


@pytest.mark.django_db
def test_password_change_wrong_old_password_rejected(client, django_user_model):
    user = django_user_model.objects.create_user(
        email="pw2@example.com", password="OldPass!2026", email_verified=True
    )
    client.force_login(user)
    resp = client.post(
        reverse("accounts:password_change"),
        {
            "old_password": "WrongOld!2026",
            "new_password1": "BrandNew!2026",
            "new_password2": "BrandNew!2026",
        },
    )
    assert resp.status_code == 200
    user.refresh_from_db()
    assert user.check_password("OldPass!2026")  # unchanged


@pytest.mark.django_db
def test_password_change_writes_audit(client, django_user_model, monkeypatch):
    # The audit row is persisted via transaction.on_commit + a Celery task, so
    # under the rolled-back test transaction it never lands in the DB. Assert
    # instead that the view invokes the audit helper with the right payload —
    # that is the contract this view is responsible for.
    calls = []
    monkeypatch.setattr(
        "apps.core.authentication.views.audit_rep",
        lambda **kwargs: calls.append(kwargs),
    )
    user = django_user_model.objects.create_user(
        email="pw3@example.com", password="OldPass!2026", email_verified=True
    )
    client.force_login(user)
    client.post(
        reverse("accounts:password_change"),
        {
            "old_password": "OldPass!2026",
            "new_password1": "BrandNew!2026",
            "new_password2": "BrandNew!2026",
        },
    )
    assert len(calls) == 1
    payload = calls[0]
    assert payload["actor"] == user
    assert payload["target_model"] == "User"
    assert payload["changes"]["changed_fields"] == ["password"]
