from datetime import datetime, time as dt_time

from api.models import EsExamRequirementStudent, EsSessionStart
from api.services.exam_scheduler import schedule_exam_requirements
from api.tests.helpers import (
	AFTERNOON_TIME,
	PERIOD_START,
	SchedulerTestCase,
	activity_label,
	build_exam_world,
	post_signed,
	reload_activity,
)

SCHEDULE_URL = "/api/admin/exam-activity/schedule"


class ExamActivityScheduleHttpTests(SchedulerTestCase):
	def test_schedule_assigns_legal_slot_and_location(self):
		world = build_exam_world()
		response = post_signed(self.client, SCHEDULE_URL, {"id": [world.activity.id]})
		self.assertEqual(response.status_code, 200)
		body = response.json()
		self.assertEqual(body["code"], 200)
		self.assertIn("activities", body["data"])
		self.assertIn("unscheduled_activities", body["data"])
		self.assertIn("skipped_activities", body["data"])
		self.assertEqual(body["data"]["unscheduled_activities"], [])
		self.assertEqual(body["data"]["skipped_activities"], [])

		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.monday_slot())
		self.assertEqual(activity.location_id, world.location.id)
		self.assertEqual(body["data"]["activities"][0]["id"], activity.id)
		self.assertEqual(body["data"]["activities"][0]["time_slot"], activity.time_slot)
		self.assertEqual(body["data"]["activities"][0]["location_id"], world.location.id)


class ExamActivityScheduleServiceTests(SchedulerTestCase):
	def _schedule(self, *activities):
		return schedule_exam_requirements([activity.id for activity in activities], self.user.id)

	def test_first_legal_session_start_and_room(self):
		world = build_exam_world()
		result = self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.monday_slot())
		self.assertEqual(activity.location_id, world.location.id)
		self.assertEqual(result["data"]["unscheduled_activities"], [])
		self.assertEqual(result["data"]["skipped_activities"], [])

	def test_already_scheduled_is_skipped_and_not_moved(self):
		world = build_exam_world()
		original_slot = world.slot_for(2)
		world.mark_scheduled(world.activity, time_slot=original_slot)
		result = self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertEqual(activity.time_slot, original_slot)
		self.assertEqual(result["data"]["skipped_activities"], [activity_label(activity)])
		self.assertEqual(result["data"]["unscheduled_activities"], [])

	def test_same_time_group_shares_one_slot(self):
		world = build_exam_world(is_partition=True)
		_req2, activity2 = world.add_exam()
		world.link_same_time(world.requirement, activity2.exam_requirement)
		result = self._schedule(world.activity, activity2)
		first = reload_activity(world.activity)
		second = reload_activity(activity2)
		self.assertTrue(first.is_scheduled)
		self.assertTrue(second.is_scheduled)
		self.assertEqual(first.time_slot, second.time_slot)
		self.assertEqual(first.time_slot, world.monday_slot())
		self.assertEqual(result["data"]["unscheduled_activities"], [])

	def test_same_time_shared_students_roll_back(self):
		world = build_exam_world(is_partition=True)
		student = world.add_student()
		_req2, activity2 = world.add_exam()
		EsExamRequirementStudent.objects.create(exam_requirement=activity2.exam_requirement, student=student)
		world.link_same_time(world.requirement, activity2.exam_requirement)
		result = self._schedule(world.activity, activity2)
		self.assertFalse(reload_activity(world.activity).is_scheduled)
		self.assertFalse(reload_activity(activity2).is_scheduled)
		labels = set(result["data"]["unscheduled_activities"])
		self.assertEqual(labels, {activity_label(world.activity), activity_label(activity2)})

	def test_same_location_partition_can_share_room(self):
		world = build_exam_world(is_partition=True, planned_size=10)
		_req2, activity2 = world.add_exam(planned_size=10)
		world.link_same_location(world.requirement, activity2.exam_requirement)
		self._schedule(world.activity, activity2)
		first = reload_activity(world.activity)
		second = reload_activity(activity2)
		self.assertTrue(first.is_scheduled)
		self.assertTrue(second.is_scheduled)
		self.assertEqual(first.location_id, second.location_id)
		self.assertEqual(first.location_id, world.location.id)

	def test_non_partition_room_rejects_overlapping_occupancy(self):
		world = build_exam_world(is_partition=False, planned_size=1)
		_req2, activity2 = world.add_exam(planned_size=1)
		self._schedule(world.activity, activity2)
		first = reload_activity(world.activity)
		second = reload_activity(activity2)
		self.assertTrue(first.is_scheduled)
		self.assertTrue(second.is_scheduled)
		self.assertEqual(first.time_slot, world.monday_slot())
		self.assertEqual(second.time_slot, world.slot_for(1))
		self.assertEqual(first.location_id, world.location.id)
		self.assertEqual(second.location_id, world.location.id)

	def test_exclusive_use_blocks_sharing(self):
		world = build_exam_world(is_partition=True, exclusive_use=True, planned_size=1)
		_req2, activity2 = world.add_exam(planned_size=1, exclusive_use=False)
		self._schedule(world.activity, activity2)
		first = reload_activity(world.activity)
		second = reload_activity(activity2)
		self.assertTrue(first.is_scheduled)
		self.assertTrue(second.is_scheduled)
		self.assertNotEqual(first.time_slot, second.time_slot)

	def test_insufficient_capacity_is_unscheduled(self):
		world = build_exam_world(row=2, column=2, planned_size=10)
		result = self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertFalse(activity.is_scheduled)
		self.assertIsNone(activity.time_slot)
		self.assertEqual(result["data"]["unscheduled_activities"], [activity_label(activity)])

	def test_location_not_required_schedules_without_room(self):
		world = build_exam_world(location_required=False, create_location=False)
		result = self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.monday_slot())
		self.assertIsNone(activity.location_id)
		self.assertEqual(result["data"]["unscheduled_activities"], [])

	def test_shared_students_block_overlapping_slots(self):
		world = build_exam_world()
		student = world.add_student()
		_req2, activity2 = world.add_exam()
		EsExamRequirementStudent.objects.create(exam_requirement=activity2.exam_requirement, student=student)
		self._schedule(world.activity, activity2)
		first = reload_activity(world.activity)
		second = reload_activity(activity2)
		self.assertTrue(first.is_scheduled)
		self.assertTrue(second.is_scheduled)
		self.assertNotEqual(first.time_slot, second.time_slot)

	def test_preceding_exam_starts_after_predecessor_end(self):
		world = build_exam_world()
		successor_req, successor = world.add_exam()
		world.link_preceding(world.requirement, successor_req)
		self._schedule(world.activity, successor)
		predecessor = reload_activity(world.activity)
		successor = reload_activity(successor)
		self.assertTrue(predecessor.is_scheduled)
		self.assertTrue(successor.is_scheduled)
		self.assertEqual(predecessor.time_slot, world.monday_slot())
		self.assertGreaterEqual(successor.time_slot, predecessor.time_slot + 2)

	def test_fixed_start_date(self):
		world = build_exam_world(fixed_start_date=PERIOD_START.replace(day=4))
		self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.slot_for(2))

	def test_fixed_start_time(self):
		world = build_exam_world(fixed_start_time=AFTERNOON_TIME)
		world.add_session_start(AFTERNOON_TIME)
		self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.slot_for(0, AFTERNOON_TIME))

	def test_earliest_start(self):
		world = build_exam_world(earliest_start=datetime.combine(PERIOD_START.replace(day=4), dt_time(0, 0)))
		self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.slot_for(2))

	def test_latest_end_keeps_activity_on_monday(self):
		world = build_exam_world(latest_end=datetime.combine(PERIOD_START, dt_time(10, 0)))
		self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.monday_slot())

	def test_latest_end_too_tight_is_unscheduled(self):
		world = build_exam_world(latest_end=datetime.combine(PERIOD_START, dt_time(9, 30)))
		result = self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertFalse(activity.is_scheduled)
		self.assertEqual(result["data"]["unscheduled_activities"], [activity_label(activity)])

	def test_requirement_unavailability_skips_blocked_slot(self):
		world = build_exam_world()
		world.add_requirement_unavailability(world.requirement, world.monday_slot())
		self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.slot_for(1))

	def test_period_unavailability_skips_date(self):
		world = build_exam_world()
		world.add_period_unavailability(PERIOD_START)
		self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.slot_for(1))

	def test_no_active_session_starts_conflicts(self):
		world = build_exam_world()
		EsSessionStart.objects.filter(pk=world.session_start.pk).update(
			status=EsSessionStart.STATUS_TO_CODE["inactive"]
		)
		result = self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertFalse(activity.is_scheduled)
		self.assertEqual(result["data"]["unscheduled_activities"], [activity_label(activity)])

	def test_matching_session_is_linked(self):
		world = build_exam_world(is_partition=True)
		session = world.add_session(start_time=world.datetime_for(0), location=world.location)
		_other_req, other = world.add_exam()
		world.mark_scheduled(other, time_slot=world.monday_slot(), location=world.location, session=session)
		self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertTrue(activity.is_scheduled)
		self.assertEqual(activity.time_slot, world.monday_slot())
		self.assertEqual(activity.session_id, session.id)

	def test_tt_student_group_conflict_blocks_slot(self):
		world = build_exam_world()
		student = world.add_student()
		world.add_student_group(student, tt_id=202)
		self.tt_student_groups_conflict.return_value = True
		result = self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertFalse(activity.is_scheduled)
		self.assertEqual(result["data"]["unscheduled_activities"], [activity_label(activity)])

	def test_tt_location_conflict_skips_busy_room(self):
		world = build_exam_world()
		world.location.tt_id = 303
		world.location.save(update_fields=["tt_id"])
		self.tt_location_conflict.return_value = True
		result = self._schedule(world.activity)
		activity = reload_activity(world.activity)
		self.assertFalse(activity.is_scheduled)
		self.assertEqual(result["data"]["unscheduled_activities"], [activity_label(activity)])
