"""Tests covering the trickiest business rules.

Uses Django's TestCase (built on unittest) so it runs with
`python manage.py test` without extra test-runner dependencies.
"""
from __future__ import annotations

import datetime

from django.test import TestCase, override_settings
from django.utils import timezone

from application.use_cases.attendance import AdminEditAttendance, ApplyDefaultCheckoutForPastDays, CheckOut
from application.use_cases.auth import CheckIn
from application.use_cases.leaves import GrantDailyLeave, ReviewLeaveRequest, SubmitLeaveRequest
from application.use_cases.reports import BuildMonthlyReport
from application.use_cases.work_schedule import SetDefaultWorkSchedule, SetWorkScheduleForDate
from domain.exceptions import (
    AlreadyCheckedOutError,
    AttendanceNotFoundError,
    LeaveAlreadyGrantedError,
    LeaveRequestNotPendingError,
    UserNotFoundError,
)
from apps.organizations.models import Organization, OrganizationMembership
from domain.value_objects import JalaliDate
from infrastructure.models import Attendance, Holiday, LeaveRequest, User
from infrastructure.repositories import (
    DjangoAttendanceRepository,
    DjangoHolidayRepository,
    DjangoLeaveRequestRepository,
    DjangoMemberRepository,
    DjangoWorkScheduleRepository,
)


@override_settings(PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"])
class OrgTestCase(TestCase):
    """Timesheet logic always runs inside one organization."""

    @classmethod
    def setUpTestData(cls):
        cls.org = Organization.objects.create(name="Test Co", slug="test-co")

    def new_user(self, **kwargs):
        user = User.objects.create_user(**kwargs)
        OrganizationMembership.objects.create(user=user, organization=self.org)
        return user


class LoginCheckInTests(OrgTestCase):
    def setUp(self):
        self.user = self.new_user(personnel_code="1001", password="pass1234", name="Ali")
        self.repo = DjangoAttendanceRepository(self.org)

    def test_first_login_of_day_creates_check_in(self):
        CheckIn(self.repo).execute(self.user)
        today = datetime.date.today()
        attendance = Attendance.objects.get(user=self.user, date=today)
        self.assertIsNotNone(attendance.check_in)
        self.assertIsNone(attendance.check_out)

    def test_second_login_same_day_does_not_overwrite_check_in(self):
        CheckIn(self.repo).execute(self.user)
        today = datetime.date.today()
        first = Attendance.objects.get(user=self.user, date=today)
        original_check_in = first.check_in

        CheckIn(self.repo).execute(self.user)

        refreshed = Attendance.objects.get(user=self.user, date=today)
        self.assertEqual(refreshed.check_in, original_check_in)
        self.assertEqual(Attendance.objects.filter(user=self.user, date=today).count(), 1)


class CheckOutIdempotencyTests(OrgTestCase):
    def setUp(self):
        self.user = self.new_user(personnel_code="1002", password="pass1234", name="Sara")
        self.repo = DjangoAttendanceRepository(self.org)

    def test_checkout_fails_without_check_in(self):
        with self.assertRaises(AttendanceNotFoundError):
            CheckOut(self.repo).execute(self.user, "did some work")

    def test_checkout_succeeds_after_check_in(self):
        Attendance.objects.create(organization=self.org, user=self.user, date=datetime.date.today(), check_in=datetime.time(9, 0))
        attendance = CheckOut(self.repo).execute(self.user, "finished tasks")
        self.assertIsNotNone(attendance.check_out)
        self.assertFalse(attendance.is_default_checkout)
        self.assertEqual(attendance.tasks_note, "finished tasks")

    def test_double_checkout_raises(self):
        Attendance.objects.create(organization=self.org, 
            user=self.user,
            date=datetime.date.today(),
            check_in=datetime.time(9, 0),
            check_out=datetime.time(17, 0),
        )
        with self.assertRaises(AlreadyCheckedOutError):
            CheckOut(self.repo).execute(self.user, "again")


class DefaultCheckoutTests(OrgTestCase):
    def test_applies_default_checkout_to_past_open_days_only(self):
        user = self.new_user(personnel_code="1003", password="pass1234")
        yesterday = datetime.date.today() - datetime.timedelta(days=1)
        today = datetime.date.today()

        past_open = Attendance.objects.create(organization=self.org, user=user, date=yesterday, check_in=datetime.time(9, 0))
        today_open = Attendance.objects.create(organization=self.org, user=user, date=today, check_in=datetime.time(9, 0))

        count = ApplyDefaultCheckoutForPastDays(DjangoAttendanceRepository(self.org)).execute()

        past_open.refresh_from_db()
        today_open.refresh_from_db()

        self.assertEqual(count, 1)
        self.assertEqual(past_open.check_out, datetime.time(17, 30, 0))
        self.assertTrue(past_open.is_default_checkout)
        self.assertIsNone(today_open.check_out)


class MonthlyReportMissingFlagTests(OrgTestCase):
    def setUp(self):
        self.user = self.new_user(personnel_code="1004", password="pass1234")
        self.attendance_repo = DjangoAttendanceRepository(self.org)
        self.holiday_repo = DjangoHolidayRepository(self.org)
        self.leave_repo = DjangoLeaveRequestRepository(self.org)
        self.use_case = BuildMonthlyReport(self.attendance_repo, self.holiday_repo, self.leave_repo)

    def test_missing_true_when_no_attendance_no_holiday_no_daily_leave(self):
        jd = JalaliDate.from_gregorian(datetime.date.today())
        report = self.use_case.execute(self.user.id, jd.year, jd.month)
        today_row = next(r for r in report.rows if r.date == datetime.date.today())
        self.assertTrue(today_row.missing)

    def test_missing_false_on_holiday(self):
        today = datetime.date.today()
        Holiday.objects.create(organization=self.org, date=today, title="Test Holiday")
        jd = JalaliDate.from_gregorian(today)
        report = self.use_case.execute(self.user.id, jd.year, jd.month)
        today_row = next(r for r in report.rows if r.date == today)
        self.assertFalse(today_row.missing)
        self.assertTrue(today_row.is_holiday)

    def test_missing_false_with_approved_daily_leave(self):
        today = datetime.date.today()
        LeaveRequest.objects.create(organization=self.org, 
            user=self.user, date=today, type="daily", category="annual", status="approved"
        )
        jd = JalaliDate.from_gregorian(today)
        report = self.use_case.execute(self.user.id, jd.year, jd.month)
        today_row = next(r for r in report.rows if r.date == today)
        self.assertFalse(today_row.missing)

    def test_missing_true_with_only_hourly_leave(self):
        today = datetime.date.today()
        LeaveRequest.objects.create(organization=self.org, 
            user=self.user,
            date=today,
            type="hourly",
            category="annual",
            status="approved",
            start_time=datetime.time(9, 0),
            end_time=datetime.time(10, 0),
        )
        jd = JalaliDate.from_gregorian(today)
        report = self.use_case.execute(self.user.id, jd.year, jd.month)
        today_row = next(r for r in report.rows if r.date == today)
        self.assertTrue(today_row.missing)

    def test_missing_false_when_attendance_exists(self):
        today = datetime.date.today()
        Attendance.objects.create(organization=self.org, 
            user=self.user, date=today, check_in=datetime.time(9, 0), check_out=datetime.time(17, 0)
        )
        jd = JalaliDate.from_gregorian(today)
        report = self.use_case.execute(self.user.id, jd.year, jd.month)
        today_row = next(r for r in report.rows if r.date == today)
        self.assertFalse(today_row.missing)
        self.assertEqual(today_row.worked, "8:00")


class LeaveRequestApprovalTests(OrgTestCase):
    def setUp(self):
        self.employee = self.new_user(personnel_code="1005", password="pass1234")
        self.admin = self.new_user(personnel_code="admin2", password="pass1234", is_admin=True)
        self.repo = DjangoLeaveRequestRepository(self.org)

    def test_submit_hourly_requires_times(self):
        from domain.exceptions import InvalidLeaveRequestError

        with self.assertRaises(InvalidLeaveRequestError):
            SubmitLeaveRequest(self.repo).execute(
                user=self.employee, date=datetime.date.today(), type="hourly", category="sick"
            )

    def test_approve_pending_request(self):
        leave = LeaveRequest.objects.create(organization=self.org, 
            user=self.employee, date=datetime.date.today(), type="daily", category="annual", status="pending"
        )
        approved = ReviewLeaveRequest(self.repo).approve(leave, self.admin)
        self.assertEqual(approved.status, "approved")
        self.assertEqual(approved.reviewed_by, self.admin)
        self.assertIsNotNone(approved.reviewed_at)

    def test_double_approve_raises(self):
        leave = LeaveRequest.objects.create(organization=self.org, 
            user=self.employee, date=datetime.date.today(), type="daily", category="annual", status="approved"
        )
        with self.assertRaises(LeaveRequestNotPendingError):
            ReviewLeaveRequest(self.repo).approve(leave, self.admin)

    def test_reject_pending_request(self):
        leave = LeaveRequest.objects.create(organization=self.org, 
            user=self.employee, date=datetime.date.today(), type="daily", category="unpaid", status="pending"
        )
        rejected = ReviewLeaveRequest(self.repo).reject(leave, self.admin, "no cover that day")
        self.assertEqual(rejected.status, "rejected")


class AdminAttendanceEditTests(OrgTestCase):
    def test_backfills_missing_day_and_clears_default_flag(self):
        user = self.new_user(personnel_code="1006", password="pass1234")
        repo = DjangoAttendanceRepository(self.org)
        attendance = AdminEditAttendance(repo).execute(
            user_id=user.id,
            date=datetime.date.today(),
            check_in=datetime.time(9, 0),
            check_out=datetime.time(17, 0),
            tasks_note="manual backfill",
        )
        self.assertFalse(attendance.is_default_checkout)
        self.assertEqual(attendance.tasks_note, "manual backfill")


class GrantDailyLeaveTests(OrgTestCase):
    def setUp(self):
        self.admin = self.new_user(personnel_code="admin5", password="pass1234", is_admin=True)
        self.employee = self.new_user(personnel_code="1009", password="pass1234")
        self.leave_repo = DjangoLeaveRequestRepository(self.org)
        self.user_repo = DjangoMemberRepository(self.org)
        self.use_case = GrantDailyLeave(self.leave_repo, self.user_repo)
        self.today = datetime.date.today()

    def test_creates_approved_daily_leave_and_clears_missing_flag(self):
        leave = self.use_case.execute(self.employee.id, self.today, "annual", self.admin)
        self.assertEqual(leave.status, "approved")
        self.assertEqual(leave.type, "daily")
        self.assertEqual(leave.reviewed_by, self.admin)

        jalali = JalaliDate.from_gregorian(self.today)
        report = BuildMonthlyReport(
            DjangoAttendanceRepository(self.org), DjangoHolidayRepository(self.org), self.leave_repo
        ).execute(self.employee.id, jalali.year, jalali.month)
        row = next(r for r in report.rows if r.date == self.today)
        self.assertFalse(row.missing)

    def test_second_grant_same_day_rejected(self):
        self.use_case.execute(self.employee.id, self.today, "annual", self.admin)
        with self.assertRaises(LeaveAlreadyGrantedError):
            self.use_case.execute(self.employee.id, self.today, "sick", self.admin)

    def test_pending_daily_request_is_approved_not_duplicated(self):
        pending = LeaveRequest.objects.create(organization=self.org, 
            user=self.employee, date=self.today, type="daily", category="sick", status="pending"
        )
        leave = self.use_case.execute(self.employee.id, self.today, "annual", self.admin)
        self.assertEqual(leave.id, pending.id)
        self.assertEqual(leave.status, "approved")
        self.assertEqual(LeaveRequest.objects.filter(user=self.employee, date=self.today).count(), 1)

    def test_cannot_grant_leave_to_non_members_or_unknown_users(self):
        outsider = User.objects.create_user(personnel_code="outsider", password="pass1234")  # no membership
        elsewhere = Organization.objects.create(name="Other Co", slug="other-co")
        OrganizationMembership.objects.create(user=outsider, organization=elsewhere)
        with self.assertRaises(UserNotFoundError):
            self.use_case.execute(outsider.id, self.today, "annual", self.admin)
        with self.assertRaises(UserNotFoundError):
            self.use_case.execute(999999, self.today, "annual", self.admin)

    def test_suspended_members_cannot_be_granted_leave(self):
        OrganizationMembership.objects.filter(user=self.employee).update(status="suspended")
        with self.assertRaises(UserNotFoundError):
            self.use_case.execute(self.employee.id, self.today, "annual", self.admin)


class WorkScheduleReportTests(OrgTestCase):
    def setUp(self):
        self.employee = self.new_user(personnel_code="1008", password="pass1234")
        self.attendance_repo = DjangoAttendanceRepository(self.org)
        self.holiday_repo = DjangoHolidayRepository(self.org)
        self.leave_repo = DjangoLeaveRequestRepository(self.org)
        self.schedule_repo = DjangoWorkScheduleRepository(self.org)
        self.use_case = BuildMonthlyReport(
            self.attendance_repo, self.holiday_repo, self.leave_repo, self.schedule_repo
        )

    def _build_today_row(self):
        today = datetime.date.today()
        jalali = JalaliDate.from_gregorian(today)
        report = self.use_case.execute(self.employee.id, jalali.year, jalali.month)
        return next(r for r in report.rows if r.date == today)

    def test_late_check_in_flagged(self):
        SetDefaultWorkSchedule(self.schedule_repo).execute(
            datetime.time(8, 0), datetime.time(16, 0)
        )
        today = datetime.date.today()
        Attendance.objects.create(organization=self.org, 
            user=self.employee, date=today, check_in=datetime.time(8, 20), check_out=datetime.time(16, 0)
        )
        row = self._build_today_row()
        self.assertEqual(row.late, "0:20")
        self.assertIsNone(row.overtime)

    def test_overtime_check_out_flagged(self):
        SetDefaultWorkSchedule(self.schedule_repo).execute(
            datetime.time(8, 0), datetime.time(16, 0)
        )
        today = datetime.date.today()
        Attendance.objects.create(organization=self.org, 
            user=self.employee, date=today, check_in=datetime.time(8, 0), check_out=datetime.time(17, 15)
        )
        row = self._build_today_row()
        self.assertIsNone(row.late)
        self.assertEqual(row.overtime, "1:15")

    def test_default_checkout_never_counts_as_overtime(self):
        SetDefaultWorkSchedule(self.schedule_repo).execute(
            datetime.time(8, 0), datetime.time(16, 0)
        )
        today = datetime.date.today()
        Attendance.objects.create(organization=self.org, 
            user=self.employee,
            date=today,
            check_in=datetime.time(8, 0),
            check_out=datetime.time(17, 30),
            is_default_checkout=True,
        )
        row = self._build_today_row()
        self.assertIsNone(row.overtime)

    def test_per_day_override_wins_over_default(self):
        SetDefaultWorkSchedule(self.schedule_repo).execute(
            datetime.time(8, 0), datetime.time(16, 0)
        )
        today = datetime.date.today()
        SetWorkScheduleForDate(self.schedule_repo).execute(
            today, datetime.time(9, 0), datetime.time(17, 0)
        )
        Attendance.objects.create(organization=self.org, 
            user=self.employee, date=today, check_in=datetime.time(8, 30), check_out=datetime.time(17, 0)
        )
        row = self._build_today_row()
        self.assertIsNone(row.late)
        self.assertEqual(row.standard_check_in, datetime.time(9, 0))
