import pytest
from django.contrib.auth.tokens import default_token_generator
from django.core import mail
from django.test import override_settings
from django.urls import reverse
from django.utils.encoding import (
    force_bytes,
    force_str,
)
from django.utils.http import urlsafe_base64_encode
from rest_framework import status

from tests.conftest import PWD
from tests.utils import (
    TestsMixin,
    override_api_settings,
)
from vq_django_auth.choices import AuthMethod


@pytest.mark.django_db
class TestAPIBasic(TestsMixin):
    """
    Tests for the vq_django_auth API endpoints.

    These tests verify the functionality of authentication endpoints including:
    - Login and logout
    - Password change and reset
    - User detail operations
    - Token handling

    The tests ensure that proper validation, security measures,
    and expected behavior are maintained across different configurations.
    """

    def setup_method(self, method):
        self.init()

    def _generate_uid_and_token(self, user):
        """
        Generate the UID and token for password reset verification.

        Args:
            user: The user model instance for which to generate tokens

        Returns:
            dict: A dictionary containing:
                - uid: Base64-encoded user ID
                - token: One-time password reset token
        """
        result = {}

        result["uid"] = urlsafe_base64_encode(force_bytes(user.pk))
        result["token"] = default_token_generator.make_token(user)
        return result

    def test_login_session(self, user):
        login_payload = {
            "email": user.email,
            "password": PWD,
        }
        self.post(self.login_url, login_payload)

        assert self.response.status_code == status.HTTP_204_NO_CONTENT, "Login failed"

    @override_api_settings(AUTH_METHOD=AuthMethod.TOKEN)
    def test_login_token(self, user):
        login_payload = {
            "email": user.email,
            "password": PWD,
        }
        self.post(self.login_url, login_payload)

        assert self.response.status_code == status.HTTP_200_OK, "Login failed"

    def test_password_change(self, user):
        """
        Password change functionality works correctly

        Given:
        - A user exists in the system
        - The user is logged in with valid credentials

        When:
        - The user attempts to change their password

        Then:
        - The password should be updated successfully
        - The old password should no longer work
        - The new password should allow login
        """
        login_payload = {
            "email": user.email,
            "password": PWD,
        }
        self.client.force_login(user)

        new_password_payload = {
            "old_password": PWD,
            "new_password1": "new_person",
            "new_password2": "new_person",
        }
        self.post(
            self.password_change_url,
            data=new_password_payload,
        )
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )
        # test auto login
        self.get(self.user_url)
        assert self.response.status_code == status.HTTP_200_OK, self.response.content

        # user should not be able to login using old password
        self.post(self.login_url, data=login_payload)
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

        # new password should work
        login_payload["password"] = new_password_payload["new_password1"]
        self.post(self.login_url, data=login_payload)
        assert self.response.status_code == status.HTTP_204_NO_CONTENT, (
            self.response.status_code,
            status.HTTP_204_NO_CONTENT,
            self.response.content,
        )

    def test_password_change_errors(self, user):
        """
        Password change validation correctly handles error conditions

        Given:
        - A user exists in the system
        - The user is logged in with valid credentials

        When:
        - The user submits mismatched new passwords
        - The user submits an empty password change payload

        Then:
        - Both requests should fail with 400 Bad Request errors
        """
        self.client.force_login(user)

        # pass1 and pass2 are not equal
        new_password_payload = {
            "new_password1": "new_person1",
            "new_password2": "new_person",
        }
        self.post(
            self.password_change_url,
            data=new_password_payload,
        )
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

        # send empty payload
        self.post(self.password_change_url, data={})
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

    @override_api_settings(AUTH_METHOD=AuthMethod.TOKEN)
    def test_password_change_honors_password_validators(self, settings, user):
        """
        Password change respects Django password validators

        Given:
        - A user exists in the system
        - The user is logged in with valid credentials
        - NumericPasswordValidator is configured

        When:
        - The user attempts to set a password containing only numbers

        Then:
        - The request should fail with a 400 status code
        """
        settings.AUTH_PASSWORD_VALIDATORS = [
            {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}
        ]

        self.client.force_login(user)

        new_password_payload = {"new_password1": 123, "new_password2": 123}
        self.post(self.password_change_url, data=new_password_payload)
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

    @override_api_settings(AUTH_METHOD=AuthMethod.TOKEN)
    def test_password_change_with_old_password(self, user):
        """
        Password change requires old password when enabled

        Given:
        - A user exists in the system
        - The user is logged in with valid credentials

        When:
        - The user attempts to change password with incorrect old password
        - Then the user attempts with correct old password

        Then:
        - The first attempt should fail with a 400 status
        - The second attempt should succeed
        - The new password should work for login
        """
        login_payload = {
            "email": user.email,
            "password": PWD,
        }
        self.client.force_login(user)

        new_password_payload = {
            "old_password": f"{PWD}!",  # wrong password
            "new_password1": "new_person",
            "new_password2": "new_person",
        }
        self.post(
            self.password_change_url,
            data=new_password_payload,
        )
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

        new_password_payload = {
            "old_password": PWD,
            "new_password1": "new_person",
            "new_password2": "new_person",
        }
        self.post(
            self.password_change_url,
            data=new_password_payload,
        )
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )

        # user should not be able to login using old password
        self.post(self.login_url, data=login_payload)
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

        # new password should work
        login_payload["password"] = new_password_payload["new_password1"]
        self.post(self.login_url, data=login_payload)
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )

    def test_password_reset(self, user):
        """
        Password reset flow works correctly

        Given:
        - A user exists with a valid email

        When:
        - Password reset is requested with the user's email
        - Password reset is attempted with valid and invalid tokens

        Then:
        - Reset email should be sent
        - Invalid tokens/UIDs should be rejected
        - Valid token and UID should allow password reset
        - User should be able to login with new password
        """
        new_pass = "new-very-secure"

        # call password reset
        mail_count = len(mail.outbox)
        payload = {"email": user.email}
        self.post(self.password_reset_url, data=payload)
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )
        assert len(mail.outbox) == mail_count + 1

        url_kwargs = self._generate_uid_and_token(user)
        url = reverse("rest_password_reset_confirm")

        # wrong token
        data = {
            "new_password1": new_pass,
            "new_password2": new_pass,
            "uid": force_str(url_kwargs["uid"]),
            "token": "-wrong-token-",
        }
        self.post(url, data=data)
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

        # wrong uid
        data = {
            "new_password1": new_pass,
            "new_password2": new_pass,
            "uid": "-wrong-uid-",
            "token": url_kwargs["token"],
        }
        self.post(url, data=data)
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

        # wrong token and uid
        data = {
            "new_password1": new_pass,
            "new_password2": new_pass,
            "uid": "-wrong-uid-",
            "token": "-wrong-token-",
        }
        self.post(url, data=data)
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

        # valid payload
        data = {
            "new_password1": new_pass,
            "new_password2": new_pass,
            "uid": force_str(url_kwargs["uid"]),
            "token": url_kwargs["token"],
        }
        url = reverse("rest_password_reset_confirm")
        self.post(url, data=data)
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )

        payload = {
            "email": user.email,
            "password": new_pass,
        }
        self.post(self.login_url, data=payload)
        assert self.response.status_code == status.HTTP_204_NO_CONTENT, (
            self.response.status_code,
            status.HTTP_204_NO_CONTENT,
            self.response.content,
        )

    def test_password_reset_with_email_in_different_case(self, user):
        """
        Password reset works with email case insensitivity

        Given:
        - A user exists with email in lowercase

        When:
        - Password reset is requested with the same email in uppercase

        Then:
        - Reset email should be sent successfully
        """

        # call password reset in upper case
        mail_count = len(mail.outbox)
        payload = {"email": user.email.upper()}
        self.post(self.password_reset_url, data=payload)
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )
        assert len(mail.outbox) == mail_count + 1

    def test_password_reset_with_invalid_email(self, user):
        """
        Password reset with invalid email doesn't reveal user existence

        Given:
        - A user exists in the system

        When:
        - Password reset is requested with a non-existent email

        Then:
        - No error message should be returned
        - No reset email should be sent
        """

        # call password reset
        mail_count = len(mail.outbox)
        payload = {"email": "nonexisting@email.com"}
        self.post(self.password_reset_url, data=payload)
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )
        assert len(mail.outbox) == mail_count

    @override_settings(
        AUTH_PASSWORD_VALIDATORS=[
            {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}
        ]
    )
    def test_password_reset_honors_password_validators(self, user):
        """
        Password reset respects Django password validators

        Given:
        - A user exists in the system
        - NumericPasswordValidator is configured

        When:
        - The user attempts to reset their password with a numeric-only password

        Then:
        - The request should fail with a 400 status code
        """

        url_kwargs = self._generate_uid_and_token(user)
        data = {
            "new_password1": 123,
            "new_password2": 123,
            "uid": force_str(url_kwargs["uid"]),
            "token": url_kwargs["token"],
        }
        self.post(self.password_reset_confirm, data=data)
        assert self.response.status_code == status.HTTP_400_BAD_REQUEST, (
            self.response.status_code,
            status.HTTP_400_BAD_REQUEST,
            self.response.content,
        )

    def test_password_reset_change_password(self, user):
        """
        Password reset successfully changes user password

        Given:
        - A user exists in the system
        - The password is changed by reset

        When:
        - The user attempts to login with the new password

        Then:
        - The request should succeed with a 200 status code
        """

        url_kwargs = self._generate_uid_and_token(user)
        data = {
            "new_password1": "Very-Secure-Pa$$w0rd",
            "new_password2": "Very-Secure-Pa$$w0rd",
            "uid": force_str(url_kwargs["uid"]),
            "token": url_kwargs["token"],
        }
        self.post(self.password_reset_confirm, data=data)
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )

        login_data = {
            "email": user.email,
            "password": data["new_password1"],
        }

        self.post(self.login_url, data=login_data)
        assert self.response.status_code == status.HTTP_204_NO_CONTENT, (
            self.response.status_code,
            status.HTTP_204_NO_CONTENT,
            self.response.content,
        )

    @override_api_settings(AUTH_METHOD=AuthMethod.TOKEN)
    def test_user_details(self, user):
        """
        User details can be retrieved and updated

        Given:
        - A user exists in the system
        - The user is logged in

        When:
        - The user requests their profile details
        - The user updates their profile information

        Then:
        - The user details should be returned successfully
        - The profile should be updated with the new information
        """

        self.client.force_login(user)
        self.get(self.user_url)
        assert self.response.status_code == status.HTTP_200_OK, (
            self.response.status_code,
            status.HTTP_200_OK,
            self.response.content,
        )

    def test_rest_session_login_sets_session_cookie(self, settings, user):
        """
        Session login sets session cookie based on AUTH_METHOD setting

        Given:
        - A user exists in the system

        When:
        - AUTH_METHOD is TOKEN and the user logs in
        - AUTH_METHOD is SESSION and the user logs in

        Then:
        - With AUTH_METHOD=TOKEN, no session cookie should be set
        - With AUTH_METHOD=SESSION, a session cookie should be set
        """

        payload = {
            "email": user.email,
            "password": PWD,
        }

        with override_api_settings(AUTH_METHOD=AuthMethod.TOKEN):
            self.post(self.login_url, data=payload)
            assert self.response.status_code == status.HTTP_200_OK, (
                self.response.status_code,
                status.HTTP_200_OK,
                self.response.content,
            )
            assert settings.SESSION_COOKIE_NAME not in self.response.cookies.keys()

        with override_api_settings(AUTH_METHOD=AuthMethod.SESSION):
            self.post(self.login_url, data=payload)
            assert self.response.status_code == status.HTTP_204_NO_CONTENT, (
                self.response.status_code,
                status.HTTP_204_NO_CONTENT,
                self.response.content,
            )
            assert settings.SESSION_COOKIE_NAME in self.response.cookies.keys()
