from datetime import time

from api.models import EsSessionInvigilator
from api.tests.helpers import SchedulerTestCase, build_exam_world, post_signed

UPDATE_INVIGILATOR_URL = "/api/admin/session/update-invigilator"


class SessionUpdateInvigilatorTests(SchedulerTestCase):
	def _ids(self, session, is_floating):
		return EsSessionInvigilator.objects.filter(
			session=session,
			invigilator_id__isnull=False,
			is_floating=is_floating,
		).values_list("invigilator_id", flat=True)

	def test_assigns_and_replaces_invigilators(self):
		world = build_exam_world()
		session = world.add_session()
		world.mark_scheduled(world.activity, session=session)
		first = world.add_invigilator()
		second = world.add_invigilator()

		assign = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": [first.id]},
		)
		self.assertEqual(assign.status_code, 200)
		self.assertEqual(assign.json()["data"], {})
		self.assertEqual(
			list(
				EsSessionInvigilator.objects.filter(session=session, invigilator_id__isnull=False)
				.values_list("invigilator_id", flat=True)
			),
			[first.id],
		)

		replace = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": [second.id]},
		)
		self.assertEqual(replace.status_code, 200)
		self.assertEqual(
			list(
				EsSessionInvigilator.objects.filter(session=session, invigilator_id__isnull=False)
				.values_list("invigilator_id", flat=True)
			),
			[second.id],
		)

		clear = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": []},
		)
		self.assertEqual(clear.status_code, 200)
		self.assertFalse(
			EsSessionInvigilator.objects.filter(session=session, invigilator_id__isnull=False).exists()
		)

	def test_tt_busy_staff_is_rejected(self):
		world = build_exam_world()
		session = world.add_session()
		world.mark_scheduled(world.activity, session=session)
		invigilator = world.add_invigilator(with_staff=True)
		self.tt_staff_conflict_at.return_value = True

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": [invigilator.id]},
		)
		self.assertEqual(response.status_code, 400)
		self.assertEqual(response.json()["code"], 400)
		self.assertFalse(
			EsSessionInvigilator.objects.filter(session=session, invigilator_id=invigilator.id).exists()
		)
		self.tt_staff_conflict_at.assert_called()

	def test_assigns_floating_invigilators_on_requested_sessions_only(self):
		world = build_exam_world()
		session = world.add_session()
		other = world.add_session()
		world.mark_scheduled(world.activity, session=session)
		session_invigilator = world.add_invigilator()
		floating = world.add_invigilator()
		replacement = world.add_invigilator()

		assign = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{
				"id": [session.id],
				"invigilator_id": [session_invigilator.id],
				"floating_invigilator_id": [floating.id],
			},
		)
		self.assertEqual(assign.status_code, 200)
		self.assertEqual(
			set(self._ids(session, is_floating=0)),
			{session_invigilator.id},
		)
		self.assertEqual(set(self._ids(session, is_floating=1)), {floating.id})
		self.assertFalse(
			EsSessionInvigilator.objects.filter(session=other, invigilator_id__isnull=False).exists()
		)

		replace = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{
				"id": [session.id],
				"invigilator_id": [session_invigilator.id],
				"floating_invigilator_id": [replacement.id],
			},
		)
		self.assertEqual(replace.status_code, 200)
		self.assertEqual(set(self._ids(session, is_floating=1)), {replacement.id})

	def test_omitted_floating_invigilator_id_is_left_unchanged(self):
		world = build_exam_world()
		session = world.add_session()
		world.mark_scheduled(world.activity, session=session)
		session_invigilator = world.add_invigilator()
		replacement = world.add_invigilator()
		floating = world.add_invigilator()
		EsSessionInvigilator.objects.create(
			session=session,
			invigilator=floating,
			is_floating=1,
		)

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": [session_invigilator.id]},
		)
		self.assertEqual(response.status_code, 200)
		self.assertEqual(set(self._ids(session, is_floating=0)), {session_invigilator.id})
		self.assertEqual(set(self._ids(session, is_floating=1)), {floating.id})

		replaced = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": [replacement.id]},
		)
		self.assertEqual(replaced.status_code, 200)
		self.assertEqual(set(self._ids(session, is_floating=1)), {floating.id})

	def test_empty_floating_invigilator_id_clears_floating_rows(self):
		world = build_exam_world()
		session = world.add_session()
		world.mark_scheduled(world.activity, session=session)
		session_invigilator = world.add_invigilator()
		floating = world.add_invigilator()
		EsSessionInvigilator.objects.create(session=session, invigilator=session_invigilator, is_floating=0)
		EsSessionInvigilator.objects.create(session=session, invigilator=floating, is_floating=1)

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": [session_invigilator.id], "floating_invigilator_id": []},
		)
		self.assertEqual(response.status_code, 200)
		self.assertEqual(set(self._ids(session, is_floating=0)), {session_invigilator.id})
		self.assertFalse(EsSessionInvigilator.objects.filter(session=session, is_floating=1).exists())

	def test_same_invigilator_in_both_lists_is_rejected(self):
		world = build_exam_world()
		session = world.add_session()
		world.mark_scheduled(world.activity, session=session)
		invigilator = world.add_invigilator()

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{
				"id": [session.id],
				"invigilator_id": [invigilator.id],
				"floating_invigilator_id": [invigilator.id],
			},
		)
		self.assertEqual(response.status_code, 400)
		self.assertIn("floating_invigilator_id", response.json()["errors"])
		self.assertFalse(
			EsSessionInvigilator.objects.filter(session=session, invigilator_id=invigilator.id).exists()
		)

	def test_tt_busy_floating_invigilator_is_rejected(self):
		world = build_exam_world()
		session = world.add_session()
		world.mark_scheduled(world.activity, session=session)
		invigilator = world.add_invigilator(with_staff=True)
		self.tt_staff_conflict_at.return_value = True

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": [], "floating_invigilator_id": [invigilator.id]},
		)
		self.assertEqual(response.status_code, 400)
		self.assertIn("floating_invigilator_id", response.json()["errors"])
		self.assertFalse(
			EsSessionInvigilator.objects.filter(session=session, invigilator_id=invigilator.id).exists()
		)

	def test_same_time_non_floating_assignment_is_rejected(self):
		world = build_exam_world()
		first = world.add_session()
		second = world.add_session()
		invigilator = world.add_invigilator()

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [first.id, second.id], "invigilator_id": [invigilator.id]},
		)
		self.assertEqual(response.status_code, 400)
		self.assertIn("invigilator_id", response.json()["errors"])
		self.assertFalse(EsSessionInvigilator.objects.filter(invigilator=invigilator).exists())

	def test_non_overlapping_sessions_can_share_a_non_floating_invigilator(self):
		world = build_exam_world()
		first = world.add_session()
		second = world.add_session(start_time=world.datetime_for(0, time(10, 0)))
		invigilator = world.add_invigilator()

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [first.id, second.id], "invigilator_id": [invigilator.id]},
		)
		self.assertEqual(response.status_code, 200)
		self.assertEqual(set(self._ids(first, is_floating=0)), {invigilator.id})
		self.assertEqual(set(self._ids(second, is_floating=0)), {invigilator.id})

	def test_existing_overlapping_assignment_is_rejected(self):
		world = build_exam_world()
		existing = world.add_session()
		session = world.add_session()
		invigilator = world.add_invigilator()
		EsSessionInvigilator.objects.create(session=existing, invigilator=invigilator, is_floating=0)

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [session.id], "invigilator_id": [invigilator.id]},
		)
		self.assertEqual(response.status_code, 400)
		self.assertIn("invigilator_id", response.json()["errors"])
		self.assertFalse(
			EsSessionInvigilator.objects.filter(session=session, invigilator=invigilator).exists()
		)

	def test_floating_invigilator_can_cover_overlapping_sessions(self):
		world = build_exam_world()
		first = world.add_session()
		second = world.add_session()
		invigilator = world.add_invigilator()

		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{
				"id": [first.id, second.id],
				"invigilator_id": [],
				"floating_invigilator_id": [invigilator.id],
			},
		)
		self.assertEqual(response.status_code, 200)
		self.assertEqual(set(self._ids(first, is_floating=1)), {invigilator.id})
		self.assertEqual(set(self._ids(second, is_floating=1)), {invigilator.id})

	def test_invalid_session_id_returns_400(self):
		response = post_signed(
			self.client,
			UPDATE_INVIGILATOR_URL,
			{"id": [999999999], "invigilator_id": []},
		)
		self.assertEqual(response.status_code, 400)
