"""Shared auth, signing, and exam-world fixtures for scheduler tests."""

from __future__ import annotations

import time
from datetime import date, datetime, time as dt_time, timedelta
from itertools import count
from unittest.mock import patch

from django.conf import settings
from django.test import TestCase
from rest_framework.test import APIClient

from api.models import (
	AccessToken,
	EsExamActivity,
	EsExamPeriod,
	EsExamPeriodUnavailability,
	EsExamRequirement,
	EsExamRequirementPrecedingExamination,
	EsExamRequirementPrecedingExaminationGroup,
	EsExamRequirementSameLocation,
	EsExamRequirementSameLocationGroup,
	EsExamRequirementSameTime,
	EsExamRequirementSameTimeGroup,
	EsExamRequirementStudent,
	EsExamRequirementUnavailability,
	EsInvigilator,
	EsInvigilatorInvigilatorRole,
	EsInvigilatorRole,
	EsLocation,
	EsSession,
	EsSessionStart,
	EsSessionStartDay,
	EsStaff,
	EsStudent,
	EsStudentGroup,
	EsStudentStudentGroup,
	User,
	UserPermission,
)
from api.services.exam_scheduler.slots import _datetime_to_slot
from api.views.admin.base import AdminApiBase

SCHEDULER_PERMISSIONS = [
	"exam_activity;schedule",
	"exam_activity;schedule_direct",
	"exam_activity;unschedule",
	"session;update_invigilator",
	"session;assign_invigilator",
	"session;list",
	"invigilator_setting;update",
	"invigilator_setting;update_v2",
]

PERIOD_START = date(2026, 3, 2)
PERIOD_END = date(2026, 3, 6)
SESSION_TIME = dt_time(9, 0)
AFTERNOON_TIME = dt_time(14, 0)
WEEKDAYS = (0, 1, 2, 3, 4)

_code_seq = count(1)


def unique_code(prefix="T"):
	return f"{prefix}{next(_code_seq):05d}"


def signed_payload(payload):
	data = dict(payload)
	data.setdefault("timestamp", int(time.time()))
	to_sign = dict(data)
	data["signature"] = AdminApiBase.sign(to_sign, settings.API_SECRET_KEY)
	return data


def post_signed(client, path, payload):
	return client.post(path, signed_payload(payload), format="json")


def create_admin_client(permissions=None):
	if permissions is None:
		permissions = list(SCHEDULER_PERMISSIONS)
	user = User.objects.create_user(
		email=f"{unique_code('u')}@es.test",
		password="qwe123",
		name="Scheduler Tester",
		user_type=User.USER_TYPE["admin"],
		status=User.STATUS_TO_CODE["active"],
	)
	for permission in permissions:
		UserPermission.objects.create(user=user, permission=permission)
	token = AccessToken.objects.create(user=user, type="web", device=unique_code("dev"))
	client = APIClient()
	client.credentials(HTTP_AUTHORIZATION=f"Token {token.token}")
	return user, client, token


class ExamWorld:
	def __init__(self, period, session_start, location, requirement, activity):
		self.period = period
		self.session_start = session_start
		self.location = location
		self.requirement = requirement
		self.activity = activity

	def datetime_for(self, day_offset=0, clock=None):
		clock = SESSION_TIME if clock is None else clock
		return datetime.combine(PERIOD_START + timedelta(days=day_offset), clock)

	def slot_for(self, day_offset=0, clock=None):
		return _datetime_to_slot(self.period, self.datetime_for(day_offset, clock))

	def monday_slot(self):
		return self.slot_for(0)

	def add_session_start(self, start_time, days=WEEKDAYS, status=None):
		if status is None:
			status = EsSessionStart.STATUS_TO_CODE["active"]
		session_start = EsSessionStart.objects.create(
			exam_period=self.period,
			code=unique_code("SS"),
			start_time=start_time,
			status=status,
		)
		for day in days:
			EsSessionStartDay.objects.create(session_start=session_start, day=day)
		return session_start

	def add_location(self, *, row=10, column=10, is_partition=False, tt_id=None, status=None):
		if status is None:
			status = EsLocation.STATUS_TO_CODE["active"]
		return EsLocation.objects.create(
			code=unique_code("LOC"),
			name=unique_code("Room"),
			seat_order=EsLocation.SEAT_ORDER_TO_CODE["column_first"],
			row=row,
			column=column,
			is_partition=is_partition,
			tt_id=tt_id,
			status=status,
		)

	def add_requirement(self, **kwargs):
		defaults = {
			"exam_period": self.period,
			"code": unique_code("REQ"),
			"name": unique_code("Requirement"),
			"planned_size": 1,
			"writing_time": dt_time(1, 0),
			"reading_time": dt_time(0, 0),
			"location_required": True,
			"exclusive_use": False,
		}
		defaults.update(kwargs)
		return EsExamRequirement.objects.create(**defaults)

	def add_activity(self, requirement=None, **kwargs):
		requirement = requirement or self.requirement
		defaults = {
			"exam_requirement": requirement,
			"code": unique_code("ACT"),
			"name": unique_code("Activity"),
			"is_scheduled": False,
		}
		defaults.update(kwargs)
		return EsExamActivity.objects.create(**defaults)

	def add_exam(self, *, create_activity=True, **requirement_kwargs):
		requirement = self.add_requirement(**requirement_kwargs)
		activity = self.add_activity(requirement) if create_activity else None
		return requirement, activity

	def add_student(self, requirement=None, **kwargs):
		requirement = self.requirement if requirement is None else requirement
		student = EsStudent.objects.create(
			code=unique_code("STU"),
			name=unique_code("Student"),
			status=EsStudent.STATUS_TO_CODE["active"],
			**kwargs,
		)
		if requirement is not None:
			EsExamRequirementStudent.objects.create(exam_requirement=requirement, student=student)
		return student

	def add_student_group(self, student, tt_id=101):
		group = EsStudentGroup.objects.create(
			code=unique_code("SG"),
			name=unique_code("StudentGroup"),
			status=EsStudent.STATUS_TO_CODE["active"],
			tt_id=tt_id,
		)
		EsStudentStudentGroup.objects.create(student=student, student_group=group)
		return group

	def add_invigilator(self, *, with_staff=False, staff_tt_id=501):
		staff = None
		if with_staff:
			staff = EsStaff.objects.create(
				code=unique_code("STA"),
				name=unique_code("Staff"),
				status=EsStaff.STATUS_TO_CODE["active"],
				tt_id=staff_tt_id,
				is_invigilator=True,
			)
		return EsInvigilator.objects.create(
			code=unique_code("INV"),
			name=unique_code("Invigilator"),
			status=EsInvigilator.STATUS_TO_CODE["active"],
			staff=staff,
		)

	def add_invigilator_role(self, **kwargs):
		defaults = {
			"code": unique_code("IR"),
			"name": unique_code("Role"),
			"color": "#7C5CFF",
			"status": EsInvigilatorRole.STATUS_TO_CODE["active"],
		}
		defaults.update(kwargs)
		return EsInvigilatorRole.objects.create(**defaults)

	def attach_invigilator_role(self, invigilator, role):
		return EsInvigilatorInvigilatorRole.objects.create(
			invigilator=invigilator,
			role=role,
		)

	def add_session(self, *, start_time=None, location=None, duration=None, invigilators_required=1, students_enrolled=1, description=None):
		if start_time is None:
			start_time = self.datetime_for(0)
		if duration is None:
			duration = dt_time(1, 0)
		return EsSession.objects.create(
			name=unique_code("SES"),
			location=location if location is not None else self.location,
			start_time=start_time,
			duration=duration,
			students_enrolled=students_enrolled,
			invigilators_required=invigilators_required,
			description=description,
		)

	def mark_scheduled(self, activity, *, time_slot=None, location=None, session=None):
		if time_slot is None:
			time_slot = self.monday_slot()
		if location is None and activity.exam_requirement.location_required:
			location = self.location
		activity.time_slot = time_slot
		activity.location = location
		activity.session = session
		activity.is_scheduled = True
		activity.save()
		return activity

	def link_same_time(self, *requirements):
		group = EsExamRequirementSameTimeGroup.objects.create()
		for requirement in requirements:
			for activity in requirement.esexamactivity_set.all():
				EsExamRequirementSameTime.objects.create(
					group=group,
					exam_requirement=requirement,
					exam_activity=activity,
				)
		return group

	def link_same_location(self, *requirements):
		group = EsExamRequirementSameLocationGroup.objects.create()
		for requirement in requirements:
			for activity in requirement.esexamactivity_set.all():
				EsExamRequirementSameLocation.objects.create(
					group=group,
					exam_requirement=requirement,
					exam_activity=activity,
				)
		return group

	def link_preceding(self, predecessor, successor):
		group = EsExamRequirementPrecedingExaminationGroup.objects.create()
		successor_activity = successor.esexamactivity_set.first()
		EsExamRequirementPrecedingExamination.objects.create(
			group=group,
			exam_requirement=predecessor,
			exam_activity=successor_activity,
		)
		return group

	def add_period_unavailability(self, unavailable_date):
		return EsExamPeriodUnavailability.objects.create(
			exam_period=self.period,
			unavailable_date=unavailable_date,
		)

	def add_requirement_unavailability(self, requirement, time_slot):
		return EsExamRequirementUnavailability.objects.create(
			exam_requirement=requirement,
			time_slot=time_slot,
		)


def build_exam_world(
	*,
	location_required=True,
	exclusive_use=False,
	planned_size=1,
	writing_time=None,
	reading_time=None,
	row=10,
	column=10,
	is_partition=False,
	create_location=True,
	create_activity=True,
	create_session_start=True,
	**requirement_kwargs,
):
	if writing_time is None:
		writing_time = dt_time(1, 0)
	if reading_time is None:
		reading_time = dt_time(0, 0)

	period = EsExamPeriod.objects.create(
		code=unique_code("PER"),
		name=unique_code("Period"),
		start_date=PERIOD_START,
		end_date=PERIOD_END,
		start_time=dt_time(0, 0),
		end_time=dt_time(23, 59),
		status=EsExamPeriod.STATUS_TO_CODE["active"],
	)
	session_start = None
	if create_session_start:
		session_start = EsSessionStart.objects.create(
			exam_period=period,
			code=unique_code("SS"),
			start_time=SESSION_TIME,
			status=EsSessionStart.STATUS_TO_CODE["active"],
		)
		for day in WEEKDAYS:
			EsSessionStartDay.objects.create(session_start=session_start, day=day)

	location = None
	if create_location:
		location = EsLocation.objects.create(
			code=unique_code("LOC"),
			name=unique_code("Room"),
			seat_order=EsLocation.SEAT_ORDER_TO_CODE["column_first"],
			row=row,
			column=column,
			is_partition=is_partition,
			status=EsLocation.STATUS_TO_CODE["active"],
		)

	requirement = EsExamRequirement.objects.create(
		exam_period=period,
		code=unique_code("REQ"),
		name=unique_code("Requirement"),
		planned_size=planned_size,
		writing_time=writing_time,
		reading_time=reading_time,
		location_required=location_required,
		exclusive_use=exclusive_use,
		**requirement_kwargs,
	)
	activity = None
	if create_activity:
		activity = EsExamActivity.objects.create(
			exam_requirement=requirement,
			code=unique_code("ACT"),
			name=unique_code("Activity"),
			is_scheduled=False,
		)
	return ExamWorld(period, session_start, location, requirement, activity)


def activity_label(activity):
	return f"{activity.id} - {activity.name}"


def reload_activity(activity):
	return EsExamActivity.objects.get(pk=activity.pk)


class SchedulerTestCase(TestCase):
	def setUp(self):
		super().setUp()
		self.tt_student_patcher = patch(
			"api.services.exam_scheduler.constraints.tt_student_groups_conflict",
			return_value=False,
		)
		self.tt_location_patcher = patch(
			"api.services.exam_scheduler.constraints.tt_location_conflict",
			return_value=False,
		)
		self.tt_staff_patcher = patch(
			"api.views.admin.session_update_invigilator.tt_staff_conflict_at",
			return_value=False,
		)
		self.tt_staff_assign_patcher = patch(
			"api.services.invigilator_assign.tt_staff_conflict_at",
			return_value=False,
		)
		self.tt_student_groups_conflict = self.tt_student_patcher.start()
		self.tt_location_conflict = self.tt_location_patcher.start()
		self.tt_staff_conflict_at = self.tt_staff_patcher.start()
		self.tt_staff_assign_conflict_at = self.tt_staff_assign_patcher.start()
		self.addCleanup(self.tt_student_patcher.stop)
		self.addCleanup(self.tt_location_patcher.stop)
		self.addCleanup(self.tt_staff_patcher.stop)
		self.addCleanup(self.tt_staff_assign_patcher.stop)
		self.user, self.client, self.token = create_admin_client()
