import hashlib
from unittest.mock import patch, MagicMock
import requests

from django.contrib.auth import get_user_model
from django.test import TestCase, RequestFactory
from django.urls import reverse
from django.core.exceptions import ValidationError
from django.contrib.sessions.middleware import SessionMiddleware

from accounts.models import FriendCode
from accounts.forms import FriendCodeForm, CustomUserCreationForm, UserSettingsForm
from accounts.templatetags import gravatar
from trades.models import TradeOffer
from tests.utils.rarity import RARITY_MAPPING

# Create your tests here.

# -----------------------------
# Model Tests
# -----------------------------
class CustomUserModelTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username="testuser",
            email="test@example.com",
            password="password123"
        )
    def test_set_default_friend_code(self):
        """User can manually set a friend code as their default."""
        fc1 = FriendCode.objects.create(
            friend_code="1234-5678-9012-3456",
            user=self.user,
            in_game_name="GameOne"
        )
        fc2 = FriendCode.objects.create(
            friend_code="2345-6789-0123-4567",
            user=self.user,
            in_game_name="GameTwo"
        )
        # Manually set fc2 as default.
        self.user.set_default_friend_code(fc2)
        self.user.refresh_from_db()
        self.assertEqual(self.user.default_friend_code, fc2)

    def test_set_default_friend_code_invalid(self):
        """
        Attempting to set a friend code that does not belong to the user should raise an exception.
        """
        other_user = get_user_model().objects.create_user(
            username="otheruser",
            email="other@example.com",
            password="password456"
        )
        fc_other = FriendCode.objects.create(
            friend_code="3456-7890-1234-5678",
            user=other_user,
            in_game_name="OtherGame"
        )
        with self.assertRaises(ValidationError):
            self.user.set_default_friend_code(fc_other)

    def test_remove_default_friend_code_with_multiple_codes(self):
        """
        When removing the default friend code and other friend codes exist,
        the default should be reassigned to another friend code.
        """
        fc1 = FriendCode.objects.create(
            friend_code="1234-5678-9012-3456",
            user=self.user,
            in_game_name="GameOne"
        )
        fc2 = FriendCode.objects.create(
            friend_code="2345-6789-0123-4567",
            user=self.user,
            in_game_name="GameTwo"
        )
        # Set fc2 as default.
        self.user.set_default_friend_code(fc2)
        # Removing fc2 should reassign the default to fc1.
        self.user.remove_default_friend_code(fc2)
        self.user.refresh_from_db()
        self.assertEqual(self.user.default_friend_code, fc1)

    def test_removing_only_friend_code_raises(self):
        """
        A user must always have a default friend code.
        Attempting to remove the only friend code (and thus the default)
        should be prohibited.
        """
        fc = FriendCode.objects.create(
            friend_code="1234-5678-9012-3456",
            user=self.user,
            in_game_name="OnlyGame"
        )
        self.user.refresh_from_db()
        self.assertEqual(self.user.default_friend_code, fc)
        with self.assertRaises(ValidationError):
            self.user.remove_default_friend_code(fc)

    def test_remove_non_default_friend_code_does_nothing(self):
        """
        When attempting to remove a friend code that isn't the default,
        the current default should remain unchanged.
        """
        fc1 = FriendCode.objects.create(
            friend_code="1234-5678-9012-3456",
            user=self.user,
            in_game_name="GameOne"
        )
        fc2 = FriendCode.objects.create(
            friend_code="2345-6789-0123-4567",
            user=self.user,
            in_game_name="GameTwo"
        )
        # By default, fc1 is the default friend code.
        self.assertEqual(self.user.default_friend_code, fc1)
        try:
            self.user.remove_default_friend_code(fc2)
        except Exception as e:
            self.fail("remove_default_friend_code raised an exception when removing a non-default code.")
        self.user.refresh_from_db()
        self.assertEqual(self.user.default_friend_code, fc1)


# -----------------------------
# FriendCode Model Tests
# -----------------------------
class FriendCodeModelTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username="testuser2",
            email="test2@example.com",
            password="password123"
        )

    def test_default_set_on_creation(self):
        """
        When creating a FriendCode for a user with no default,
        the new friend code is automatically set as the default.
        """
        fc = FriendCode.objects.create(
            friend_code="1234-5678-9012-3456",
            user=self.user,
            in_game_name="GameDefault"
        )
        self.user.refresh_from_db()
        self.assertEqual(self.user.default_friend_code, fc)

    def test_adding_additional_friend_code_preserves_default(self):
        """
        When additional friend codes are added to a user who already has a default,
        the initial friend code remains the default.
        """
        fc1 = FriendCode.objects.create(
            friend_code="1111-1111-1111-1111",
            user=self.user,
            in_game_name="PrimaryGame"
        )
        # fc1 becomes the default automatically.
        self.assertEqual(self.user.default_friend_code, fc1)
        fc2 = FriendCode.objects.create(
            friend_code="2222-2222-2222-2222",
            user=self.user,
            in_game_name="SecondaryGame"
        )
        self.user.refresh_from_db()
        self.assertEqual(self.user.default_friend_code, fc1)


# -----------------------------
# Form Tests
# -----------------------------
class FriendCodeFormTests(TestCase):
    def test_valid_friend_code(self):
        """Ensure valid friend code is cleaned and formatted properly."""
        form_data = {
            "friend_code": "1234567890123456",
            "in_game_name": "GameTest"
        }
        form = FriendCodeForm(data=form_data)
        self.assertTrue(form.is_valid())
        self.assertEqual(form.cleaned_data["friend_code"], "1234-5678-9012-3456")

    def test_invalid_friend_code_length(self):
        """Friend codes with incorrect length should cause validation errors."""
        form_data = {
            "friend_code": "12345",
            "in_game_name": "GameTest"
        }
        form = FriendCodeForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn("Friend code must be exactly 16 digits long.", form.errors["friend_code"])

    def test_invalid_friend_code_characters(self):
        """Friend codes containing non-digit characters should cause validation errors."""
        form_data = {
            "friend_code": "12345678901234ab",
            "in_game_name": "GameTest"
        }
        form = FriendCodeForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn("Friend code must be exactly 16 digits long.", form.errors["friend_code"])

    def test_friend_code_with_whitespace(self):
        """Ensure that leading/trailing whitespace is stripped."""
        form_data = {
            "friend_code": " 1234567890123456 ",
            "in_game_name": "WhitespaceGame"
        }
        form = FriendCodeForm(data=form_data)
        self.assertTrue(form.is_valid())
        self.assertEqual(form.cleaned_data["friend_code"], "1234-5678-9012-3456")

    def test_friend_code_with_dashes(self):
        """Proper dashes in the input should be accepted."""
        form_data = {
            "friend_code": "1234-5678-9012-3456",
            "in_game_name": "ExtraDashGame"
        }
        form = FriendCodeForm(data=form_data)
        self.assertTrue(form.is_valid())
        self.assertEqual(form.cleaned_data["friend_code"], "1234-5678-9012-3456")


class CustomUserCreationFormTests(TestCase):
    def _get_request_with_session(self):
        """
        Helper to create a Request object that has a session,
        so that the signup view and form can use request.session.
        """
        request = RequestFactory().get("/")
        middleware = SessionMiddleware(lambda r: None)
        middleware.process_request(request)
        request.session.save()
        return request

    def test_valid_custom_user_creation(self):
        """
        Test that the custom sign‑up form creates a user and associated friend code properly.
        """
        form_data = {
            "email": "new@example.com",
            "username": "newuser",
            "password1": "complexpass123",
            "password2": "complexpass123",
            "friend_code": "5555-5555-5555-5555",
            "in_game_name": "NewGame",
        }
        form = CustomUserCreationForm(data=form_data)
        self.assertTrue(form.is_valid())
        request = self._get_request_with_session()
        user = form.save(request)
        self.assertIsNotNone(user)
        # Check that the associated friend code exists and marked as the default.
        friend_code = user.default_friend_code
        self.assertIsNotNone(friend_code)
        self.assertEqual(friend_code.friend_code, "5555-5555-5555-5555")
        self.assertEqual(friend_code.in_game_name, "NewGame")

    def test_user_always_has_default_after_signup(self):
        """
        Ensure that after sign-up (which creates the initial friend code),
        the user always has a default friend code.
        """
        form_data = {
            "email": "another@example.com",
            "username": "anotheruser",
            "password1": "complexpass456",
            "password2": "complexpass456",
            "friend_code": "6666-6666-6666-6666",
            "in_game_name": "AnotherGame",
        }
        form = CustomUserCreationForm(data=form_data)
        self.assertTrue(form.is_valid())
        request = self._get_request_with_session()
        user = form.save(request)
        # Immediately after signup, the user should have a default friend code.
        self.assertIsNotNone(user.default_friend_code)

    def test_invalid_custom_user_creation_invalid_friend_code(self):
        """
        Supplying an invalid friend code (wrong length/format) should cause the form to fail.
        """
        form_data = {
            "email": "bad@example.com",
            "username": "baduser",
            "password1": "pass12345",
            "password2": "pass12345",
            "friend_code": "abcde",  # Invalid friend code
            "in_game_name": "BadGame",
        }
        form = CustomUserCreationForm(data=form_data)
        self.assertFalse(form.is_valid())
        self.assertIn("Friend code must be exactly 16 digits long.", form.errors["friend_code"])

    def test_invalid_custom_user_creation_password_mismatch(self):
        """
        The form should catch mismatched passwords.
        """
        form_data = {
            "email": "passmismatch@example.com",
            "username": "passmismatch",
            "password1": "pass12345",
            "password2": "differentpass",
            "friend_code": "5555-5555-5555-5555",
            "in_game_name": "MismatchGame",
        }
        form = CustomUserCreationForm(data=form_data)
        self.assertFalse(form.is_valid())
        # The error key may be '__all__' or 'password2' depending on the implementation.
        errors = form.errors.get("__all__") or form.errors.get("password2")
        self.assertTrue(errors, "Expected a password mismatch error.")


class UserSettingsFormTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username="settingsuser",
            email="settings@example.com",
            password="password123"
        )

    def test_toggle_show_friend_code_on_link_previews(self):
        """Test updating the user setting for showing friend code on link previews."""
        form_data = {"show_friend_code_on_link_previews": True}
        form = UserSettingsForm(form_data, instance=self.user)
        self.assertTrue(form.is_valid())
        form.save()
        self.user.refresh_from_db()
        self.assertTrue(self.user.show_friend_code_on_link_previews)


# -----------------------------
# View Tests
# -----------------------------
class FriendCodeViewsTests(TestCase):
    def setUp(self):
        self.user = get_user_model().objects.create_user(
            username="viewuser",
            email="viewuser@example.com",
            password="password123"
        )
        # Log in this user.
        self.client.login(username="viewuser", password="password123")
        # Create two friend codes.
        self.friend_code1 = FriendCode.objects.create(
            friend_code="7777-7777-7777-7777",
            user=self.user,
            in_game_name="ViewGameOne"
        )
        self.friend_code2 = FriendCode.objects.create(
            friend_code="8888-8888-8888-8888",
            user=self.user,
            in_game_name="ViewGameTwo"
        )
        # By default, friend_code1 is the default.

    def test_list_friend_codes_view(self):
        """The list view should display all friend codes with a correct default flag."""
        url = reverse("list_friend_codes")
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        friend_codes = response.context["friend_codes"]
        self.assertEqual(friend_codes.count(), 2)
        for fc in friend_codes:
            if fc.pk == self.friend_code1.pk:
                self.assertTrue(fc.is_default)
            else:
                self.assertFalse(fc.is_default)

    def test_list_friend_codes_view_unauthenticated(self):
        """An unauthenticated user should be redirected from the friend codes list view."""
        self.client.logout()
        url = reverse("list_friend_codes")
        response = self.client.get(url)
        self.assertNotEqual(response.status_code, 200)
        # Adjust the login URL as per your configuration.
        self.assertIn("/accounts/login/", response.url)

    def test_add_friend_code_view(self):
        """Test both GET and POST for adding a new friend code."""
        url = reverse("add_friend_code")
        # GET request.
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        # POST request.
        data = {"friend_code": "9999999999999999", "in_game_name": "ViewGameThree"}
        response = self.client.post(url, data)
        self.assertRedirects(response, reverse("list_friend_codes"))
        self.assertTrue(
            FriendCode.objects.filter(
                user=self.user,
                friend_code="9999-9999-9999-9999"
            ).exists()
        )
        # Ensure that adding a new friend code does not change the default.
        self.user.refresh_from_db()
        self.assertEqual(self.user.default_friend_code, self.friend_code1)

    def test_add_friend_code_view_invalid_data(self):
        """Submitting invalid friend code data should not create a new record."""
        url = reverse("add_friend_code")
        data = {"friend_code": "invalidfriendcode", "in_game_name": "InvalidGame"}
        response = self.client.post(url, data)
        # Extract the form from the response's context. If response.context is a list, use its first element.
        context = response.context[0] if isinstance(response.context, list) else response.context
        form = context.get("form")
        self.assertIsNotNone(form, "Form not found in response context")
        self.assertFormError(form, "friend_code", "Friend code must be exactly 16 digits long.")

    def test_edit_friend_code_view(self):
        """Test editing the in-game name of an existing friend code."""
        url = reverse("edit_friend_code", kwargs={"pk": self.friend_code2.pk})
        # GET request.
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        # POST request.
        new_data = {"in_game_name": "UpdatedGame"}
        response = self.client.post(url, new_data)
        self.assertEqual(response.status_code, 302)
        self.friend_code2.refresh_from_db()
        self.assertEqual(self.friend_code2.in_game_name, "UpdatedGame")

    def test_edit_friend_code_view_wrong_user(self):
        """A user should not be able to edit a friend code that does not belong to them."""
        other_user = get_user_model().objects.create_user(
            username="otheruser",
            email="other@example.com",
            password="password1234"
        )
        friend_code_other = FriendCode.objects.create(
            friend_code="0000-0000-0000-0000",
            user=other_user,
            in_game_name="OtherGame"
        )
        url = reverse("edit_friend_code", kwargs={"pk": friend_code_other.pk})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_edit_friend_code_view_invalid_data(self):
        """Invalid POST data for editing friend code should result in form errors."""
        url = reverse("edit_friend_code", kwargs={"pk": self.friend_code2.pk})
        new_data = {"in_game_name": ""}  # in_game_name is required.
        response = self.client.post(url, new_data)
        context = response.context[0] if isinstance(response.context, list) else response.context
        form = context.get("form")
        self.assertIsNotNone(form, "Form not found in response context")
        self.assertFormError(form, "in_game_name", "This field is required.")
 
    def test_delete_friend_code_view_only_code(self):
        """
        If the user has only one friend code, deletion should be disabled.
        This test uses a new user with a single friend code.
        """
        user_only = get_user_model().objects.create_user(
            username="onlyuser",
            email="onlyuser@example.com",
            password="password123"
        )
        friend_code_only = FriendCode.objects.create(
            friend_code="4444-4444-4444-4444",
            user=user_only,
            in_game_name="SoloGame"
        )
        self.client.logout()
        self.client.login(username="onlyuser", password="password123")
        url = reverse("delete_friend_code", kwargs={"pk": friend_code_only.pk})
        # GET request: deletion should be disabled.
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertIn("disable_delete", response.context)
        self.assertTrue(response.context["disable_delete"])
        # POST request should not delete the friend code.
        response = self.client.post(url, {})
        self.assertRedirects(response, reverse("list_friend_codes"))
        self.assertTrue(FriendCode.objects.filter(pk=friend_code_only.pk).exists())

    def test_delete_friend_code_view_default_code(self):
        """Deleting the default friend code should be prevented."""
        url = reverse("delete_friend_code", kwargs={"pk": self.friend_code1.pk})
        response = self.client.post(url, {})
        self.assertRedirects(response, reverse("list_friend_codes"))
        self.assertTrue(FriendCode.objects.filter(pk=self.friend_code1.pk).exists())

    def test_delete_friend_code_view_with_trade_offers(self):
        """
        If a friend code is associated with trade offers, deletion should be blocked.
        Instead of direct assignment, we patch the `exists` methods on the related managers.
        """
        self.trade_offer = TradeOffer.objects.create(
            initiated_by=self.friend_code2,
            is_closed=False,
            rarity_icon=RARITY_MAPPING[5],
            rarity_level=5
        )
        url = reverse("delete_friend_code", kwargs={"pk": self.friend_code2.pk})
        response = self.client.post(url, {})
        self.assertRedirects(response, reverse("list_friend_codes"))
        self.assertTrue(FriendCode.objects.filter(pk=self.friend_code2.pk).exists())
        self.trade_offer.delete()

    def test_change_default_friend_code_view(self):
        """Test that a POST to change the default friend code updates the user setting."""
        url = reverse("change_default_friend_code", kwargs={"pk": self.friend_code2.pk})
        response = self.client.post(url, {})
        self.assertRedirects(response, reverse("list_friend_codes"))
        self.user.refresh_from_db()
        self.assertEqual(self.user.default_friend_code.pk, self.friend_code2.pk)

    def test_change_default_friend_code_view_invalid_friend_code(self):
        """Posting a non-existent friend code id should return a 404 error."""
        url = reverse("change_default_friend_code", kwargs={"pk": 99999})
        response = self.client.post(url, {})
        self.assertEqual(response.status_code, 404)

    def test_change_default_friend_code_view_not_owned(self):
        """A friend code that does not belong to the current user should result in a 404."""
        other_user = get_user_model().objects.create_user(
            username="otheruser2",
            email="other2@example.com",
            password="password789"
        )
        friend_code_other = FriendCode.objects.create(
            friend_code="1111-1111-1111-1111",
            user=other_user,
            in_game_name="NotMine"
        )
        url = reverse("change_default_friend_code", kwargs={"pk": friend_code_other.pk})
        response = self.client.post(url, {})
        self.assertEqual(response.status_code, 404)

    def test_settings_view(self):
        """Settings view should allow updating of user settings."""
        url = reverse("settings")
        # GET request.
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        # POST request.
        data = {"show_friend_code_on_link_previews": True}
        response = self.client.post(url, data)
        self.assertRedirects(response, reverse("settings"))
        self.user.refresh_from_db()
        self.assertTrue(self.user.show_friend_code_on_link_previews)

    def test_profile_view(self):
        """Profile page should be accessible for authenticated users."""
        url = reverse("profile")
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)

    def test_profile_view_unauthenticated(self):
        """Unauthenticated users should be redirected from the profile page."""
        self.client.logout()
        url = reverse("profile")
        response = self.client.get(url)
        self.assertNotEqual(response.status_code, 200)

    def test_delete_friend_code_view_wrong_user(self):
        """A user should not be able to delete a friend code that does not belong to them."""
        other_user = get_user_model().objects.create_user(
            username="otherdeluser",
            email="otherdel@example.com",
            password="password321"
        )
        friend_code_other = FriendCode.objects.create(
            friend_code="2222-2222-2222-2222",
            user=other_user,
            in_game_name="OtherDelete"
        )
        url = reverse("delete_friend_code", kwargs={"pk": friend_code_other.pk})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)


# -----------------------------
# Template Tags Tests
# -----------------------------
class TemplateTagTests(TestCase):
    def test_gravatar_hash(self):
        """Test that gravatar_hash returns the correct SHA256 hash."""
        email = "Test@Example.com"
        expected = hashlib.sha256(email.strip().lower().encode("utf-8")).hexdigest()
        result = gravatar.gravatar_hash(email)
        self.assertEqual(result, expected)

    def test_gravatar_url(self):
        """Ensure gravatar_url returns a URL with the proper parameters."""
        email = "user@example.com"
        size = 100
        url = gravatar.gravatar_url(email, size)
        self.assertIn("s=100", url)
        self.assertIn("https://www.gravatar.com/avatar/", url)

    def test_gravatar_profile_url_with_none(self):
        """Test gravatar_profile_url returns the generic profile URL if no email is provided."""
        url = gravatar.gravatar_profile_url()
        self.assertEqual(url, "https://www.gravatar.com/profile")

    def test_gravatar_filter(self):
        """Test that the gravatar filter returns an HTML image tag with expected attributes."""
        email = "user@example.com"
        size = 50
        result = gravatar.gravatar(email, size)
        self.assertIn('img src="', result)
        self.assertIn(f'width="{size}"', result)

    @patch("accounts.templatetags.gravatar.requests.get")
    def test_gravatar_profile_data_success(self, mock_get):
        """Test that gravatar_profile_data returns the first entry when JSON response is valid."""
        dummy_entry = {"name": "Test User"}
        mock_response = MagicMock()
        mock_response.json.return_value = {"entry": [dummy_entry]}
        mock_response.raise_for_status.return_value = None
        mock_get.return_value = mock_response
        data = gravatar.gravatar_profile_data("user@example.com")
        self.assertEqual(data, dummy_entry)

    @patch("accounts.templatetags.gravatar.requests.get")
    def test_gravatar_profile_data_failure(self, mock_get):
        """
        If requests.get fails or the JSON is not valid,
        gravatar_profile_data should return an empty dictionary.
        """
        mock_get.side_effect = requests.RequestException("Request failed")
        data = gravatar.gravatar_profile_data("user@example.com")
        self.assertEqual(data, {})

    def test_gravatar_no_hover(self):
        """Test that gravatar_no_hover returns an image tag with the additional 'ignore' class."""
        email = "hover@example.com"
        result = gravatar.gravatar_no_hover(email, 30)
        self.assertIn('class="ignore"', result)

    def test_gravatar_filter_with_empty_string(self):
        """Even if an empty email is passed, the gravatar filter should return an image tag."""
        result = gravatar.gravatar("", 40)
        self.assertIn('img src="', result)
