"""
Tests — App Appointments (Etapa 4)
=====================================
Modelos · Servicios (anti-solapamiento) · Vistas · API endpoints.
"""

from datetime import date, datetime, time, timedelta

from django.conf import settings
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone

from .models import BloqueoFecha, Cita, CitaLog, HorarioDoctora, Servicio
from .services import (
    cambiar_estado_cita,
    crear_cita,
    obtener_slots_disponibles,
    validar_disponibilidad,
)

User = get_user_model()


class BaseAppointmentTest(TestCase):
    """Clase base con fixtures comunes."""

    def setUp(self):
        self.paciente = User.objects.create_user(
            email="paciente@test.com",
            password="TestPass123!",
            nombre="Ana",
            apellido="Paciente",
            rol="paciente",
            is_verified=True,
        )
        self.doctora = User.objects.create_user(
            email="doctora@test.com",
            password="TestPass123!",
            nombre="Mariela",
            apellido="Doctora",
            rol="doctora",
            is_verified=True,
        )
        self.servicio = Servicio.objects.create(
            nombre="Consulta Ginecologica",
            duracion_minutos=30,
            precio=0,
            orden=1,
        )
        # Horario lunes a viernes 08:00-17:00
        for dia in range(5):
            HorarioDoctora.objects.create(
                dia_semana=dia,
                hora_inicio=time(8, 0),
                hora_fin=time(17, 0),
            )

    def _next_weekday(self, weekday=0):
        """Retorna la proxima fecha de un dia de semana (0=lunes)."""
        today = timezone.now().date()
        days_ahead = weekday - today.weekday()
        if days_ahead <= 0:
            days_ahead += 7
        return today + timedelta(days=days_ahead)

    def _make_datetime(self, fecha, hora_str="10:00"):
        """Crea un datetime aware para una fecha y hora."""
        hora = datetime.strptime(hora_str, "%H:%M").time()
        naive = datetime.combine(fecha, hora)
        return timezone.make_aware(naive, timezone.get_current_timezone())


# ============================================================
# TESTS DE MODELOS
# ============================================================
class ServicioModelTest(BaseAppointmentTest):
    def test_str(self):
        self.assertIn("30 min", str(self.servicio))

    def test_ordering(self):
        s2 = Servicio.objects.create(nombre="Control Prenatal", duracion_minutos=45, orden=2)
        servicios = list(Servicio.objects.all())
        self.assertEqual(servicios[0], self.servicio)
        self.assertEqual(servicios[1], s2)


class HorarioDoctoraModelTest(BaseAppointmentTest):
    def test_str(self):
        h = HorarioDoctora.objects.first()
        self.assertIn("08:00", str(h))

    def test_clean_hora_inicio_mayor(self):
        h = HorarioDoctora(dia_semana=6, hora_inicio=time(17, 0), hora_fin=time(8, 0))
        with self.assertRaises(ValidationError):
            h.clean()

    def test_unique_constraint(self):
        with self.assertRaises(Exception):
            HorarioDoctora.objects.create(
                dia_semana=0, hora_inicio=time(8, 0), hora_fin=time(17, 0)
            )


class CitaModelTest(BaseAppointmentTest):
    def test_fecha_hora_fin(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = Cita.objects.create(
            paciente=self.paciente,
            servicio=self.servicio,
            fecha_hora=dt,
            duracion_minutos=30,
        )
        expected_fin = dt + timedelta(minutes=30)
        self.assertEqual(cita.fecha_hora_fin, expected_fin)

    def test_auto_duracion_from_servicio(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha)
        cita = Cita(paciente=self.paciente, servicio=self.servicio, fecha_hora=dt)
        cita.save()
        self.assertEqual(cita.duracion_minutos, 30)

    def test_puede_cancelar_true(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = Cita.objects.create(
            paciente=self.paciente,
            servicio=self.servicio,
            fecha_hora=dt,
            duracion_minutos=30,
            estado=Cita.Estado.PENDIENTE,
        )
        self.assertTrue(cita.puede_cancelar)

    def test_puede_cancelar_false_cancelada(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = Cita.objects.create(
            paciente=self.paciente,
            servicio=self.servicio,
            fecha_hora=dt,
            duracion_minutos=30,
            estado=Cita.Estado.CANCELADA,
        )
        self.assertFalse(cita.puede_cancelar)


class CitaLogModelTest(BaseAppointmentTest):
    def test_log_creation(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha)
        cita = Cita.objects.create(
            paciente=self.paciente,
            servicio=self.servicio,
            fecha_hora=dt,
            duracion_minutos=30,
        )
        log = CitaLog.objects.create(
            cita=cita,
            usuario=self.paciente,
            estado_anterior="",
            estado_nuevo="pendiente",
        )
        self.assertIn("->", str(log))


# ============================================================
# TESTS DE SERVICIOS (LOGICA DE NEGOCIO)
# ============================================================
class ValidarDisponibilidadTest(BaseAppointmentTest):
    def test_rechaza_fecha_pasada(self):
        past = timezone.now() - timedelta(hours=1)
        with self.assertRaises(ValidationError):
            validar_disponibilidad(past, 30)

    def test_rechaza_dia_bloqueado(self):
        fecha = self._next_weekday(0)
        BloqueoFecha.objects.create(fecha=fecha, motivo="Vacaciones")
        dt = self._make_datetime(fecha)
        with self.assertRaises(ValidationError):
            validar_disponibilidad(dt, 30)

    def test_rechaza_dia_sin_horario(self):
        # Domingo (6) no tiene horario
        fecha = self._next_weekday(6)
        dt = self._make_datetime(fecha)
        with self.assertRaises(ValidationError):
            validar_disponibilidad(dt, 30)

    def test_rechaza_fuera_de_horario(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "07:00")
        with self.assertRaises(ValidationError):
            validar_disponibilidad(dt, 30)

    def test_rechaza_solapamiento(self):
        fecha = self._next_weekday(0)
        dt1 = self._make_datetime(fecha, "10:00")
        Cita.objects.create(
            paciente=self.paciente,
            servicio=self.servicio,
            fecha_hora=dt1,
            duracion_minutos=30,
            estado=Cita.Estado.CONFIRMADA,
        )
        dt2 = self._make_datetime(fecha, "10:15")
        with self.assertRaises(ValidationError):
            validar_disponibilidad(dt2, 30)

    def test_acepta_slot_valido(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "14:00")
        self.assertTrue(validar_disponibilidad(dt, 30))

    def test_no_solapa_con_cita_cancelada(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        Cita.objects.create(
            paciente=self.paciente,
            servicio=self.servicio,
            fecha_hora=dt,
            duracion_minutos=30,
            estado=Cita.Estado.CANCELADA,
        )
        self.assertTrue(validar_disponibilidad(dt, 30))


class ObtenerSlotsDisponiblesTest(BaseAppointmentTest):
    def test_retorna_slots_dia_habil(self):
        fecha = self._next_weekday(0)
        slots = obtener_slots_disponibles(fecha, self.servicio)
        self.assertIsInstance(slots, list)
        # Debe tener al menos algunos slots (depende de hora actual vs fecha)
        if fecha > timezone.now().date():
            self.assertTrue(len(slots) > 0)

    def test_retorna_vacio_dia_bloqueado(self):
        fecha = self._next_weekday(0)
        BloqueoFecha.objects.create(fecha=fecha)
        slots = obtener_slots_disponibles(fecha, self.servicio)
        self.assertEqual(slots, [])

    def test_retorna_vacio_domingo(self):
        fecha = self._next_weekday(6)
        slots = obtener_slots_disponibles(fecha, self.servicio)
        self.assertEqual(slots, [])


class CrearCitaTest(BaseAppointmentTest):
    def test_crea_cita_exitosamente(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "11:00")
        cita = crear_cita(self.paciente, self.servicio, dt, "Consulta regular")
        self.assertEqual(cita.estado, Cita.Estado.PENDIENTE)
        self.assertEqual(cita.duracion_minutos, 30)
        self.assertEqual(CitaLog.objects.filter(cita=cita).count(), 1)

    def test_rechaza_solapamiento(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "11:00")
        crear_cita(self.paciente, self.servicio, dt)
        with self.assertRaises(ValidationError):
            crear_cita(self.paciente, self.servicio, dt)


class CambiarEstadoCitaTest(BaseAppointmentTest):
    def test_confirmar_pendiente(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "12:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        cambiar_estado_cita(cita, Cita.Estado.CONFIRMADA, self.doctora)
        self.assertEqual(cita.estado, Cita.Estado.CONFIRMADA)
        self.assertEqual(CitaLog.objects.filter(cita=cita).count(), 2)

    def test_cancelar_pendiente(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "12:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        cambiar_estado_cita(cita, Cita.Estado.CANCELADA, self.paciente, "Ya no puedo.")
        self.assertEqual(cita.estado, Cita.Estado.CANCELADA)

    def test_transicion_invalida(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "12:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        with self.assertRaises(ValidationError):
            cambiar_estado_cita(cita, Cita.Estado.COMPLETADA, self.doctora)

    def test_completar_confirmada(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "13:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        cambiar_estado_cita(cita, Cita.Estado.CONFIRMADA, self.doctora)
        cambiar_estado_cita(cita, Cita.Estado.COMPLETADA, self.doctora)
        self.assertEqual(cita.estado, Cita.Estado.COMPLETADA)

    def test_no_asistio_confirmada(self):
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "13:30")
        cita = crear_cita(self.paciente, self.servicio, dt)
        cambiar_estado_cita(cita, Cita.Estado.CONFIRMADA, self.doctora)
        cambiar_estado_cita(cita, Cita.Estado.NO_ASISTIO, self.doctora)
        self.assertEqual(cita.estado, Cita.Estado.NO_ASISTIO)


# ============================================================
# TESTS DE VISTAS
# ============================================================
class CalendarioViewTest(BaseAppointmentTest):
    def test_requiere_login(self):
        resp = self.client.get(reverse("appointments:calendario"))
        self.assertEqual(resp.status_code, 302)

    def test_acceso_paciente(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        resp = self.client.get(reverse("appointments:calendario"))
        self.assertEqual(resp.status_code, 200)
        self.assertContains(resp, "fullcalendar")

    def test_acceso_doctora(self):
        self.client.login(username="doctora@test.com", password="TestPass123!")
        resp = self.client.get(reverse("appointments:calendario"))
        self.assertEqual(resp.status_code, 200)


class HorariosViewTest(BaseAppointmentTest):
    def test_requiere_doctora(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        resp = self.client.get(reverse("appointments:horarios"))
        self.assertEqual(resp.status_code, 403)

    def test_acceso_doctora(self):
        self.client.login(username="doctora@test.com", password="TestPass123!")
        resp = self.client.get(reverse("appointments:horarios"))
        self.assertEqual(resp.status_code, 200)


class ApiSlotsTest(BaseAppointmentTest):
    def test_requiere_login(self):
        resp = self.client.get(reverse("appointments:api_slots"))
        self.assertEqual(resp.status_code, 302)

    def test_requiere_parametros(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        resp = self.client.get(reverse("appointments:api_slots"))
        self.assertEqual(resp.status_code, 400)

    def test_retorna_slots(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        resp = self.client.get(reverse("appointments:api_slots"), {
            "fecha": fecha.isoformat(),
            "servicio": str(self.servicio.id),
        })
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertIn("slots", data)


class ApiEventosTest(BaseAppointmentTest):
    def test_retorna_eventos(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        ahora = timezone.now()
        resp = self.client.get(reverse("appointments:api_eventos"), {
            "start": (ahora - timedelta(days=30)).isoformat(),
            "end": (ahora + timedelta(days=30)).isoformat(),
        })
        self.assertEqual(resp.status_code, 200)
        self.assertIsInstance(resp.json(), list)


class ReservarCitaViewTest(BaseAppointmentTest):
    def test_requiere_paciente(self):
        self.client.login(username="doctora@test.com", password="TestPass123!")
        resp = self.client.post(reverse("appointments:reservar"))
        self.assertEqual(resp.status_code, 403)

    def test_reserva_exitosa(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        resp = self.client.post(reverse("appointments:reservar"), {
            "servicio": str(self.servicio.id),
            "fecha": fecha.isoformat(),
            "hora": "10:00",
            "notas": "Consulta regular",
        })
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertTrue(data["success"])
        self.assertEqual(Cita.objects.filter(paciente=self.paciente).count(), 1)

    def test_rechaza_solapamiento_api(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        # Primera reserva
        self.client.post(reverse("appointments:reservar"), {
            "servicio": str(self.servicio.id),
            "fecha": fecha.isoformat(),
            "hora": "10:00",
        })
        # Intento solapar
        resp = self.client.post(reverse("appointments:reservar"), {
            "servicio": str(self.servicio.id),
            "fecha": fecha.isoformat(),
            "hora": "10:00",
        })
        self.assertEqual(resp.status_code, 400)
        self.assertIn("error", resp.json())


class CancelarCitaViewTest(BaseAppointmentTest):
    def test_cancelar_propia_cita(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        resp = self.client.post(reverse("appointments:cancelar", args=[cita.id]))
        self.assertEqual(resp.status_code, 200)
        cita.refresh_from_db()
        self.assertEqual(cita.estado, Cita.Estado.CANCELADA)


class ConfirmarCitaViewTest(BaseAppointmentTest):
    def test_confirmar_como_doctora(self):
        self.client.login(username="doctora@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        resp = self.client.post(reverse("appointments:confirmar", args=[cita.id]))
        self.assertEqual(resp.status_code, 200)
        cita.refresh_from_db()
        self.assertEqual(cita.estado, Cita.Estado.CONFIRMADA)

    def test_paciente_no_puede_confirmar(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        resp = self.client.post(reverse("appointments:confirmar", args=[cita.id]))
        self.assertEqual(resp.status_code, 403)


class GuardarHorarioViewTest(BaseAppointmentTest):
    def test_guardar_horario_sabado(self):
        self.client.login(username="doctora@test.com", password="TestPass123!")
        resp = self.client.post(reverse("appointments:guardar_horario"), {
            "dia_semana": "5",
            "hora_inicio": "09:00",
            "hora_fin": "13:00",
        })
        self.assertEqual(resp.status_code, 200)
        self.assertTrue(resp.json()["success"])
        self.assertTrue(HorarioDoctora.objects.filter(dia_semana=5).exists())


class BloquearFechaViewTest(BaseAppointmentTest):
    def test_bloquear_fecha(self):
        self.client.login(username="doctora@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        resp = self.client.post(reverse("appointments:bloquear_fecha"), {
            "fecha": fecha.isoformat(),
            "motivo": "Vacaciones",
        })
        self.assertEqual(resp.status_code, 200)
        self.assertTrue(resp.json()["success"])
        self.assertTrue(BloqueoFecha.objects.filter(fecha=fecha).exists())


class ApiDetalleCitaTest(BaseAppointmentTest):
    def test_detalle_como_paciente(self):
        self.client.login(username="paciente@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        resp = self.client.get(reverse("appointments:api_detalle_cita", args=[cita.id]))
        self.assertEqual(resp.status_code, 200)
        data = resp.json()
        self.assertEqual(data["servicio"], "Consulta Ginecologica")

    def test_detalle_como_doctora(self):
        self.client.login(username="doctora@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        resp = self.client.get(reverse("appointments:api_detalle_cita", args=[cita.id]))
        self.assertEqual(resp.status_code, 200)

    def test_detalle_no_autorizado(self):
        otro = User.objects.create_user(
            email="otro@test.com",
            password="TestPass123!",
            nombre="Otro",
            apellido="User",
            rol="paciente",
            is_verified=True,
        )
        self.client.login(username="otro@test.com", password="TestPass123!")
        fecha = self._next_weekday(0)
        dt = self._make_datetime(fecha, "10:00")
        cita = crear_cita(self.paciente, self.servicio, dt)
        resp = self.client.get(reverse("appointments:api_detalle_cita", args=[cita.id]))
        self.assertEqual(resp.status_code, 403)
