import shutil
import tempfile
from datetime import timedelta
from unittest.mock import patch

import jwt
from django.conf import settings
from django.contrib.auth.hashers import check_password, make_password
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import override_settings
from django.utils import timezone
from rest_framework.test import APITestCase

from .models import KycSubmission, OtpCode, User


class OtpAuthTests(APITestCase):
	send_otp_url = "/api/v1/auth/send-otp"
	verify_otp_url = "/api/v1/auth/verify-otp"
	delivery_report_url = "/api/v1/sms/delivery-callback"

	def test_delivery_report_callback_accepts_provider_payload(self):
		payload = {
			"messageId": "abc123",
			"status": "DELIVERED",
			"recipient": "+255712345678",
		}

		response = self.client.post(self.delivery_report_url, payload, format="json")

		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.data["detail"], "Delivery report received.")
		self.assertEqual(response.data["payload"]["status"], "DELIVERED")

	@patch("core.views._send_sms")
	def test_send_otp_creates_code_and_calls_sms_gateway(self, mock_send_sms):
		mock_send_sms.return_value = {"status": "queued"}

		response = self.client.post(self.send_otp_url, {"phone": "0712345678", "role": "buyer"}, format="json")

		self.assertEqual(response.status_code, 200)
		otp_record = OtpCode.objects.get(phone="+255712345678", purpose="login")
		self.assertEqual(len(otp_record.code), 6)
		self.assertEqual(otp_record.role, "buyer")
		mock_send_sms.assert_called_once()
		self.assertEqual(response.data["phone"], "+255712345678")
		self.assertEqual(response.data["role"], "buyer")

	@patch("core.views._send_sms")
	def test_send_otp_requires_supported_role(self, mock_send_sms):
		response = self.client.post(self.send_otp_url, {"phone": "0712345678"}, format="json")

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "role must be either 'buyer' or 'seller'.")
		mock_send_sms.assert_not_called()

	@patch("core.views._send_sms")
	def test_send_otp_rejects_invalid_role(self, mock_send_sms):
		response = self.client.post(self.send_otp_url, {"phone": "0712345678", "role": "admin"}, format="json")

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "role must be either 'buyer' or 'seller'.")
		mock_send_sms.assert_not_called()

	@patch("core.views._send_sms")
	def test_verify_otp_creates_user_and_returns_token(self, mock_send_sms):
		mock_send_sms.return_value = {"status": "queued"}
		self.client.post(self.send_otp_url, {"phone": "0712345678", "role": "buyer"}, format="json")

		otp_record = OtpCode.objects.get(phone="+255712345678", purpose="login")
		response = self.client.post(
			self.verify_otp_url,
			{"phone": "0712345678", "otp": otp_record.code},
			format="json",
		)

		self.assertEqual(response.status_code, 200)
		self.assertTrue(User.objects.filter(phone="+255712345678").exists())
		otp_record.refresh_from_db()
		self.assertIsNotNone(otp_record.consumed_at)
		payload = jwt.decode(
			response.data["token"],
			settings.JWT_SECRET_KEY,
			algorithms=[settings.JWT_ALGORITHM],
		)
		self.assertEqual(payload["phone"], "+255712345678")
		self.assertEqual(payload["sub"], str(User.objects.get(phone="+255712345678").id))

	@patch("core.views._send_sms")
	def test_verify_otp_creates_user_with_role_from_send_otp(self, mock_send_sms):
		mock_send_sms.return_value = {"status": "queued"}
		self.client.post(self.send_otp_url, {"phone": "0712345000", "role": "seller"}, format="json")

		otp_record = OtpCode.objects.get(phone="+255712345000", purpose="login")
		response = self.client.post(
			self.verify_otp_url,
			{"phone": "0712345000", "otp": otp_record.code},
			format="json",
		)

		self.assertEqual(response.status_code, 200)
		user = User.objects.get(phone="+255712345000")
		self.assertEqual(user.role, "seller")

	@override_settings(OTP_SEND_RATE_LIMIT_COUNT=2, OTP_SEND_RATE_LIMIT_WINDOW_MINUTES=10)
	@patch("core.views._send_sms")
	def test_send_otp_rate_limits_recent_requests(self, mock_send_sms):
		OtpCode.objects.create(
			phone="+255712345678",
			code="111111",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
		)
		OtpCode.objects.create(
			phone="+255712345678",
			code="222222",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
		)

		response = self.client.post(self.send_otp_url, {"phone": "0712345678", "role": "buyer"}, format="json")

		self.assertEqual(response.status_code, 429)
		self.assertEqual(response.data["detail"], "Too many OTP requests. Please try again later.")
		mock_send_sms.assert_not_called()

	def test_verify_otp_rejects_expired_code(self):
		otp_record = OtpCode.objects.create(
			phone="+255712345678",
			code="123456",
			purpose="login",
			expires_at=timezone.now() - timedelta(minutes=1),
		)

		response = self.client.post(
			self.verify_otp_url,
			{"phone": "+255712345678", "otp": otp_record.code},
			format="json",
		)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "OTP has expired.")

	def test_verify_otp_rejects_invalid_code(self):
		OtpCode.objects.create(
			phone="+255712345678",
			code="123456",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
		)

		response = self.client.post(
			self.verify_otp_url,
			{"phone": "+255712345678", "otp": "654321"},
			format="json",
		)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "Invalid OTP.")

	def test_verify_otp_rejects_already_used_code(self):
		OtpCode.objects.create(
			phone="+255712345678",
			code="123456",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
			consumed_at=timezone.now(),
		)

		response = self.client.post(
			self.verify_otp_url,
			{"phone": "+255712345678", "otp": "123456"},
			format="json",
		)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "OTP already used. Request a new code.")

	def test_verify_otp_reports_submitted_expired_code(self):
		OtpCode.objects.create(
			phone="+255712345678",
			code="111111",
			purpose="login",
			expires_at=timezone.now() - timedelta(minutes=1),
		)
		OtpCode.objects.create(
			phone="+255712345678",
			code="222222",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
		)

		response = self.client.post(
			self.verify_otp_url,
			{"phone": "+255712345678", "otp": "111111"},
			format="json",
		)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "OTP has expired.")

	@override_settings(OTP_MAX_VERIFY_ATTEMPTS=2)
	def test_verify_otp_locks_after_max_attempts(self):
		OtpCode.objects.create(
			phone="+255712345678",
			code="123456",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
		)

		first_response = self.client.post(
			self.verify_otp_url,
			{"phone": "+255712345678", "otp": "000000"},
			format="json",
		)
		second_response = self.client.post(
			self.verify_otp_url,
			{"phone": "+255712345678", "otp": "999999"},
			format="json",
		)

		self.assertEqual(first_response.status_code, 400)
		self.assertEqual(first_response.data["detail"], "Invalid OTP.")
		self.assertEqual(second_response.status_code, 400)
		self.assertEqual(second_response.data["detail"], "OTP is locked. Request a new code.")

		otp_record = OtpCode.objects.get(phone="+255712345678", purpose="login")
		self.assertEqual(otp_record.attempts, 2)
		self.assertIsNotNone(otp_record.consumed_at)


class OtpAdminApiTests(APITestCase):
	otp_admin_url = "/api/v1/admin/otps"

	def test_admin_otp_api_requires_admin_auth(self):
		response = self.client.get(self.otp_admin_url)
		self.assertEqual(response.status_code, 403)

	@override_settings(ADMIN_API_KEY="test-admin-key")
	def test_admin_otp_api_lists_sent_and_used_otps(self):
		used_otp = OtpCode.objects.create(
			phone="+255700000001",
			code="111111",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
			consumed_at=timezone.now(),
		)
		OtpCode.objects.create(
			phone="+255700000002",
			code="222222",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
		)

		response = self.client.get(self.otp_admin_url, HTTP_X_ADMIN_API_KEY="test-admin-key")

		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.data["count"], 2)
		ids = {item["id"] for item in response.data["results"]}
		self.assertIn(str(used_otp.id), ids)
		for item in response.data["results"]:
			if item["id"] == str(used_otp.id):
				self.assertTrue(item["is_used"])
				self.assertEqual(item["status"], "used")

	@override_settings(ADMIN_API_KEY="test-admin-key")
	def test_admin_otp_api_filters_used(self):
		OtpCode.objects.create(
			phone="+255700000003",
			code="333333",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
			consumed_at=timezone.now(),
		)
		active_otp = OtpCode.objects.create(
			phone="+255700000004",
			code="444444",
			purpose="login",
			expires_at=timezone.now() + timedelta(minutes=5),
		)

		response = self.client.get(self.otp_admin_url + "?used=false", HTTP_X_ADMIN_API_KEY="test-admin-key")

		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.data["count"], 1)
		self.assertEqual(response.data["results"][0]["id"], str(active_otp.id))
		self.assertFalse(response.data["results"][0]["is_used"])


class PinSetupTests(APITestCase):
	pin_setup_url = "/api/v1/auth/pin/setup"

	def _issue_token_for_user(self, user):
		return jwt.encode(
			{
				"sub": str(user.id),
				"phone": user.phone,
				"role": user.role,
				"type": "access",
				"iat": int(timezone.now().timestamp()),
				"exp": int((timezone.now() + timedelta(minutes=30)).timestamp()),
			},
			settings.JWT_SECRET_KEY,
			algorithm=settings.JWT_ALGORITHM,
		)

	def test_pin_setup_requires_token(self):
		response = self.client.post(self.pin_setup_url, {"pin": "1234"}, format="json")
		self.assertEqual(response.status_code, 401)
		self.assertEqual(response.data["detail"], "Authorization token is required.")

	def test_pin_setup_rejects_invalid_pin(self):
		user = User.objects.create(phone="+255700111111")
		token = self._issue_token_for_user(user)

		response = self.client.post(
			self.pin_setup_url,
			{"pin": "12ab"},
			format="json",
			HTTP_AUTHORIZATION=f"Bearer {token}",
		)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "PIN must be exactly 4 digits.")

	def test_pin_setup_sets_first_pin(self):
		user = User.objects.create(phone="+255700111112")
		token = self._issue_token_for_user(user)

		response = self.client.post(
			self.pin_setup_url,
			{"pin": "1234"},
			format="json",
			HTTP_AUTHORIZATION=f"Bearer {token}",
		)

		self.assertEqual(response.status_code, 200)
		self.assertTrue(response.data["pin_set"])
		user.refresh_from_db()
		self.assertNotEqual(user.pin_hash, "1234")

	def test_pin_setup_accepts_quoted_bearer_token(self):
		user = User.objects.create(phone="+255700111116")
		token = self._issue_token_for_user(user)

		response = self.client.post(
			self.pin_setup_url,
			{"pin": "2468"},
			format="json",
			HTTP_AUTHORIZATION=f'Bearer "{token}"',
		)

		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.data["detail"], "PIN saved successfully.")

	def test_pin_setup_requires_current_pin_to_change(self):
		user = User.objects.create(phone="+255700111113", pin_hash=make_password("1234"))
		token = self._issue_token_for_user(user)

		response = self.client.post(
			self.pin_setup_url,
			{"pin": "5678"},
			format="json",
			HTTP_AUTHORIZATION=f"Bearer {token}",
		)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "current_pin is required to change PIN.")

	def test_pin_setup_rejects_wrong_current_pin(self):
		user = User.objects.create(phone="+255700111114", pin_hash=make_password("1234"))
		token = self._issue_token_for_user(user)

		response = self.client.post(
			self.pin_setup_url,
			{"pin": "5678", "current_pin": "9999"},
			format="json",
			HTTP_AUTHORIZATION=f"Bearer {token}",
		)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "Current PIN is incorrect.")

	def test_pin_setup_changes_pin_with_valid_current_pin(self):
		user = User.objects.create(phone="+255700111115", pin_hash=make_password("1234"))
		token = self._issue_token_for_user(user)

		response = self.client.post(
			self.pin_setup_url,
			{"pin": "5678", "current_pin": "1234"},
			format="json",
			HTTP_AUTHORIZATION=f"Bearer {token}",
		)

		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.data["detail"], "PIN saved successfully.")
		user.refresh_from_db()
		self.assertTrue(check_password("5678", user.pin_hash))


class ProfileTests(APITestCase):
	profile_url = "/api/v1/profile"

	def _issue_token_for_user(self, user):
		return jwt.encode(
			{
				"sub": str(user.id),
				"phone": user.phone,
				"role": user.role,
				"type": "access",
				"iat": int(timezone.now().timestamp()),
				"exp": int((timezone.now() + timedelta(minutes=30)).timestamp()),
			},
			settings.JWT_SECRET_KEY,
			algorithm=settings.JWT_ALGORITHM,
		)

	def test_profile_requires_auth_token(self):
		response = self.client.get(self.profile_url)
		self.assertEqual(response.status_code, 401)
		self.assertEqual(response.data["detail"], "Authorization token is required.")

	def test_profile_returns_current_user(self):
		user = User.objects.create(
			phone="+255700111117",
			name="Jane Doe",
			email="jane@example.com",
			kyc_level=2,
			is_verified=True,
		)
		token = self._issue_token_for_user(user)

		response = self.client.get(self.profile_url, HTTP_AUTHORIZATION=f"Bearer {token}")

		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.data["id"], str(user.id))
		self.assertEqual(response.data["phone"], "+255700111117")
		self.assertEqual(response.data["name"], "Jane Doe")
		self.assertEqual(response.data["email"], "jane@example.com")
		self.assertTrue(response.data["is_verified"])


class KycSubmitTests(APITestCase):
	kyc_submit_url = "/api/v1/kyc/submit"

	def setUp(self):
		super().setUp()
		self._temp_media_root = tempfile.mkdtemp(prefix="kitonga-kyc-test-")

	def tearDown(self):
		shutil.rmtree(self._temp_media_root, ignore_errors=True)
		super().tearDown()

	def _issue_token_for_user(self, user):
		return jwt.encode(
			{
				"sub": str(user.id),
				"phone": user.phone,
				"role": user.role,
				"type": "access",
				"iat": int(timezone.now().timestamp()),
				"exp": int((timezone.now() + timedelta(minutes=30)).timestamp()),
			},
			settings.JWT_SECRET_KEY,
			algorithm=settings.JWT_ALGORITHM,
		)

	def _dummy_image(self, name="nida.png"):
		return SimpleUploadedFile(name, b"fake-image-content", content_type="image/png")

	@override_settings(MEDIA_ROOT=None)
	def test_kyc_submit_requires_auth_token(self):
		response = self.client.post(self.kyc_submit_url, {}, format="multipart")
		self.assertEqual(response.status_code, 401)
		self.assertEqual(response.data["detail"], "Authorization token is required.")

	def test_kyc_submit_requires_all_fields(self):
		user = User.objects.create(phone="+255700222221")
		token = self._issue_token_for_user(user)

		with override_settings(MEDIA_ROOT=self._temp_media_root):
			response = self.client.post(
				self.kyc_submit_url,
				{"full_name": "John", "nin": "123456789"},
				format="multipart",
				HTTP_AUTHORIZATION=f"Bearer {token}",
			)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "nida_image is required.")

	def test_kyc_submit_creates_submission_and_links_to_user(self):
		user = User.objects.create(phone="+255700222222", role="seller")
		token = self._issue_token_for_user(user)

		with override_settings(MEDIA_ROOT=self._temp_media_root):
			response = self.client.post(
				self.kyc_submit_url,
				{
					"full_name": "Jane Seller",
					"nin": "NIN-12345678",
					"nida_image": self._dummy_image(),
				},
				format="multipart",
				HTTP_AUTHORIZATION=f"Bearer {token}",
			)

		self.assertEqual(response.status_code, 200)
		self.assertEqual(response.data["detail"], "KYC submitted successfully.")
		submission = KycSubmission.objects.get(user=user)
		self.assertEqual(submission.full_name, "Jane Seller")
		self.assertEqual(submission.nin, "NIN-12345678")
		self.assertEqual(submission.status, "pending")
		user.refresh_from_db()
		self.assertEqual(user.name, "Jane Seller")
		self.assertEqual(user.kyc_level, 1)

	def test_kyc_submit_rejects_duplicate_nin_from_another_user(self):
		owner = User.objects.create(phone="+255700222223")
		other = User.objects.create(phone="+255700222224")
		token = self._issue_token_for_user(other)

		with override_settings(MEDIA_ROOT=self._temp_media_root):
			KycSubmission.objects.create(
				user=owner,
				full_name="Existing Owner",
				nin="NIN-9999",
				nida_image=self._dummy_image("owner.png"),
			)
			response = self.client.post(
				self.kyc_submit_url,
				{
					"full_name": "Other User",
					"nin": "NIN-9999",
					"nida_image": self._dummy_image("other.png"),
				},
				format="multipart",
				HTTP_AUTHORIZATION=f"Bearer {token}",
			)

		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.data["detail"], "nin is already used by another user.")
