from unittest.mock import patch
from types import SimpleNamespace

from django.contrib.auth import get_user_model
from django.contrib.auth.hashers import check_password
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone

from .otp import SESSION_KEY
from .enrollment import issue_course_access
from .models import Course, Enrollment, Order, Student
from .forms import AdminUserChangeForm, PhoneForm
from .payment import ZarinpalGateway

User = get_user_model()


@override_settings(OTP_RESEND_SECONDS=60, OTP_TTL_SECONDS=120, OTP_MAX_ATTEMPTS=3)
class OTPAuthenticationTests(TestCase):
    def setUp(self):
        from django.core.cache import cache
        cache.clear()

    @patch('courses.otp._send_code')
    def test_registration_requires_valid_otp_and_does_not_store_plain_code(self, send_code):
        response = self.client.post(reverse('courses:register'), {'phone': '09121234567', 'name': 'کاربر تست'})
        self.assertRedirects(response, reverse('courses:verify_otp'))
        code = send_code.call_args.args[1]
        challenge = self.client.session[SESSION_KEY]
        self.assertNotEqual(challenge['code_hash'], code)
        self.assertTrue(check_password(code, challenge['code_hash']))
        self.assertFalse(User.objects.filter(phone='+989121234567').exists())
        response = self.client.post(reverse('courses:verify_otp'), {'code': code})
        self.assertRedirects(response, reverse('courses:profile'))
        self.assertTrue(User.objects.filter(phone='+989121234567', name='کاربر تست').exists())
        self.assertNotIn(SESSION_KEY, self.client.session)
        self.assertIn('_auth_user_id', self.client.session)

    @patch('courses.otp._send_code')
    def test_wrong_code_is_limited(self, send_code):
        User.objects.create_user(phone='+989121234567')
        self.client.post(reverse('courses:login'), {'phone': '09121234567'})
        for _ in range(4):
            self.client.post(reverse('courses:verify_otp'), {'code': '000000'})
        self.assertNotIn(SESSION_KEY, self.client.session)
        self.assertNotIn('_auth_user_id', self.client.session)

    def test_logout_is_post_only_and_flushes_session(self):
        user = User.objects.create_user(phone='+989121234567')
        self.client.force_login(user)
        self.assertEqual(self.client.get(reverse('courses:logout')).status_code, 405)
        response = self.client.post(reverse('courses:logout'))
        self.assertRedirects(response, reverse('courses:home'))
        self.assertNotIn('_auth_user_id', self.client.session)

    def test_login_rejects_unknown_phone(self):
        response = self.client.post(reverse('courses:login'), {'phone': '09121234567'})
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, 'حسابی با این شماره وجود ندارد')

    @patch('courses.otp._send_code')
    def test_resend_is_throttled_and_post_only(self, send_code):
        User.objects.create_user(phone='+989121234567')
        self.client.post(reverse('courses:login'), {'phone': '09121234567'})
        self.assertEqual(self.client.get(reverse('courses:resend_otp')).status_code, 405)
        self.client.post(reverse('courses:resend_otp'))
        self.assertEqual(send_code.call_count, 1)


@override_settings(ZARINPAL_MERCHANT_ID='00000000-0000-0000-0000-000000000000')
class PurchaseAndAccessTests(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(phone='+989121111111')
        self.paid_course = Course.objects.create(name='دوره پولی', slug='paid', course_id='paid-1', price=500000)
        self.free_course = Course.objects.create(name='دوره رایگان', slug='free', course_id='free-1', price=0, is_free=True)
        self.client.force_login(self.user)

    @patch('courses.views.ZarinpalGateway.request_payment')
    def test_paid_checkout_never_grants_access_before_confirmation(self, request_payment):
        request_payment.return_value = SimpleNamespace(authority='A0001', payment_url='https://www.zarinpal.com/pg/StartPay/A0001', payload={'data': {'code': 100}})
        response = self.client.post(reverse('courses:checkout', args=[self.paid_course.id]))
        order = Order.objects.get(user=self.user, course=self.paid_course)
        self.assertEqual(order.status, 'Pending')
        self.assertEqual(order.gateway_authority, 'A0001')
        self.assertFalse(Enrollment.objects.filter(user=self.user, course=self.paid_course).exists())
        self.assertEqual(response.status_code, 302)
        self.assertEqual(response.url, 'https://www.zarinpal.com/pg/StartPay/A0001')
        self.assertRedirects(self.client.get(reverse('courses:player', args=[self.paid_course.id])), reverse('courses:checkout', args=[self.paid_course.id]))

    @patch('courses.views.ZarinpalGateway.verify_payment')
    def test_successful_gateway_callback_activates_course_and_redirects_to_player(self, verify_payment):
        order = Order.objects.create(user=self.user, course=self.paid_course, amount=500000, status='Pending', transaction_id='PAY-1', gateway_authority='A0001')
        verify_payment.return_value = ({'code': 100, 'ref_id': 456789}, {'data': {'code': 100, 'ref_id': 456789}})
        response = self.client.get(reverse('courses:zarinpal_callback', args=[order.id]), {'Status': 'OK', 'Authority': 'A0001'})
        self.assertRedirects(response, reverse('courses:player', args=[self.paid_course.id]))
        order.refresh_from_db()
        self.assertEqual(order.status, 'Completed')
        self.assertEqual(order.gateway_reference, '456789')
        self.assertTrue(Enrollment.objects.filter(user=self.user, course=self.paid_course).exists())

    def test_failed_gateway_callback_records_error_on_order(self):
        order = Order.objects.create(user=self.user, course=self.paid_course, amount=500000, status='Pending', transaction_id='PAY-2', gateway_authority='A0002')
        response = self.client.get(reverse('courses:zarinpal_callback', args=[order.id]), {'Status': 'NOK', 'Authority': 'A0002'})
        self.assertRedirects(response, reverse('courses:order_status', args=[order.id]))
        order.refresh_from_db()
        self.assertEqual(order.status, 'Failed')
        self.assertIn('لغو', order.payment_error)

    @override_settings(ZARINPAL_SANDBOX=True, ZARINPAL_MERCHANT_ID='real-merchant', ZARINPAL_SANDBOX_MERCHANT_ID='00000000-0000-0000-0000-000000000000')
    def test_sandbox_never_uses_production_merchant(self):
        gateway = ZarinpalGateway()
        self.assertEqual(gateway.merchant_id, '00000000-0000-0000-0000-000000000000')
        self.assertIn('sandbox.zarinpal.com', gateway.request_url)

    def test_completed_order_issues_access_idempotently(self):
        order = Order.objects.create(user=self.user, course=self.paid_course, amount=500000, status='Completed', transaction_id='TX-1')
        issue_course_access(order)
        issue_course_access(order)
        self.assertEqual(Enrollment.objects.filter(user=self.user, course=self.paid_course).count(), 1)
        self.assertEqual(self.client.get(reverse('courses:player', args=[self.paid_course.id])).status_code, 200)

    def test_free_checkout_grants_access_immediately(self):
        response = self.client.post(reverse('courses:checkout', args=[self.free_course.id]))
        self.assertRedirects(response, reverse('courses:player', args=[self.free_course.id]))
        self.assertTrue(Enrollment.objects.filter(user=self.user, course=self.free_course).exists())

    def test_users_cannot_see_another_users_order(self):
        other = User.objects.create_user(phone='+989122222222')
        order = Order.objects.create(user=other, course=self.paid_course, amount=1, status='Pending', transaction_id='TX-OTHER')
        self.assertEqual(self.client.get(reverse('courses:order_status', args=[order.id])).status_code, 404)

    def test_admin_can_activate_course(self):
        Enrollment.objects.create(user=self.user, course=self.paid_course, source='Admin')
        self.assertEqual(Student.objects.get(pk=self.user.pk).enrollments.count(), 1)

    def test_duplicate_course_activation_is_rejected_by_database(self):
        from django.db import IntegrityError, transaction
        Enrollment.objects.create(user=self.user, course=self.paid_course)
        with self.assertRaises(IntegrityError), transaction.atomic():
            Enrollment.objects.create(user=self.user, course=self.paid_course)

    def test_discount_must_be_lower_than_original_price(self):
        self.paid_course.discount_price = self.paid_course.price
        with self.assertRaises(ValidationError):
            self.paid_course.full_clean()

    def test_discount_and_toman_are_visible_on_home_and_checkout(self):
        self.paid_course.discount_price = 350000
        self.paid_course.is_featured = True
        self.paid_course.save(update_fields=['discount_price', 'is_featured'])
        home = self.client.get(reverse('courses:home'))
        self.assertContains(home, '۳۰٪ تخفیف')
        self.assertContains(home, '۵۰۰٬۰۰۰ تومان')
        self.assertContains(home, '۳۵۰٬۰۰۰ تومان')
        checkout = self.client.get(reverse('courses:checkout', args=[self.paid_course.id]))
        self.assertContains(checkout, 'مبلغ قابل پرداخت')
        self.assertContains(checkout, '۳۵۰٬۰۰۰ تومان')
        self.assertNotContains(checkout, '$')


class PhoneNormalizationTests(TestCase):
    def test_site_and_admin_forms_use_identical_normalization(self):
        cases = ('09121234567', '9121234567', '989121234567', '00989121234567', '۰۹۱۲۱۲۳۴۵۶۷')
        for raw_phone in cases:
            site_form = PhoneForm({'phone': raw_phone})
            admin_form = AdminUserChangeForm({'phone': raw_phone, 'password': '', 'is_active': True, 'date_joined': timezone.now().isoformat()}, instance=User(phone=raw_phone))
            self.assertTrue(site_form.is_valid(), site_form.errors)
            self.assertTrue(admin_form.is_valid(), admin_form.errors)
            self.assertEqual(site_form.cleaned_data['phone'], '+989121234567')
            self.assertEqual(admin_form.cleaned_data['phone'], '+989121234567')

    def test_invalid_phone_is_rejected_in_both_forms(self):
        for raw_phone in ('12345', '08121234567', '+98912123', ''):
            self.assertFalse(PhoneForm({'phone': raw_phone}).is_valid())
            self.assertFalse(AdminUserChangeForm({'phone': raw_phone, 'password': '', 'is_active': True}).is_valid())

    def test_user_manager_always_stores_canonical_phone(self):
        user = User.objects.create_user(phone='۰۹۱۲۱۲۳۴۵۶۷')
        self.assertEqual(user.phone, '+989121234567')
