# Part of Odoo. See LICENSE file for full copyright and licensing details.

from datetime import date, datetime, UTC
from zoneinfo import ZoneInfo

from dateutil.relativedelta import relativedelta

from odoo.tests.common import tagged
from odoo.addons.hr_work_entry.tests.common import TestWorkEntryBase


@tagged('work_entry')
@tagged('at_install', '-post_install')  # LEGACY at_install
class TestWorkEntry(TestWorkEntryBase):

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.tz = ZoneInfo(cls.richard_emp.tz)
        cls.start = datetime(2015, 11, 1, 1, 0, 0)
        cls.end = datetime(2015, 11, 30, 23, 59, 59)
        cls.resource_calendar_id = cls.env['resource.calendar'].create({
            'attendance_ids': [
                (0, 0,
                    {
                        'dayofweek': weekday,
                        'hour_from': hour,
                        'hour_to': hour + 4,
                    })
                for weekday in ['0', '1', '2', '3', '4']
                for hour in [8, 13]
            ],
            'name': 'Standard 40h/week',
        })
        cls.richard_emp.create_version({
            'date_version': cls.start.date() - relativedelta(days=5),
            'contract_date_start': cls.start.date() - relativedelta(days=5),
            'contract_date_end': cls.end.date() + relativedelta(days=5),
            'name': 'dodo',
            'resource_calendar_id': cls.resource_calendar_id.id,
            'wage': 1000,
        })

    def test_work_entry_timezone(self):
        """ Test work entries with different timezone """
        hk_resource_calendar_id = self.env['resource.calendar'].create({
            'name': 'HK Calendar',
            'hours_per_day': 8,
            'attendance_ids': [(5, 0, 0),
                (0, 0, {'dayofweek': '0', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '0', 'hour_from': 13, 'hour_to': 17}),
                (0, 0, {'dayofweek': '1', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '1', 'hour_from': 13, 'hour_to': 17}),
                (0, 0, {'dayofweek': '2', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '2', 'hour_from': 13, 'hour_to': 17}),
                (0, 0, {'dayofweek': '3', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '3', 'hour_from': 13, 'hour_to': 17}),
                (0, 0, {'dayofweek': '4', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '4', 'hour_from': 13, 'hour_to': 17})
            ]
        })
        hk_employee = self.env['hr.employee'].create({
            'name': 'HK Employee',
            'resource_calendar_id': hk_resource_calendar_id.id,
            'date_version': datetime(2023, 8, 1),
            'contract_date_start': datetime(2023, 8, 1),
            'contract_date_end': False,
            'wage': 1000,
            'tz': 'Asia/Hong_Kong',
        })
        self.env['resource.calendar.leaves'].create({
            'date_from': datetime(2023, 8, 2, 0, 0, 0).replace(tzinfo=ZoneInfo('Asia/Hong_Kong')).astimezone(UTC).replace(tzinfo=None),
            'date_to': datetime(2023, 8, 2, 23, 59, 59).replace(tzinfo=ZoneInfo('Asia/Hong_Kong')).astimezone(UTC).replace(tzinfo=None),
            'calendar_id': hk_resource_calendar_id.id,
            'work_entry_type_id': self.work_entry_type_leave.id,
        })
        self.env.company.resource_calendar_id = hk_resource_calendar_id
        work_entries_vals = hk_employee.generate_work_entries(datetime(2023, 8, 1), datetime(2023, 8, 2))
        self.assertEqual(len(work_entries_vals), 2)
        self.assertEqual(work_entries_vals[0]['date'], date(2023, 8, 1))
        self.assertEqual(work_entries_vals[0]['duration'], 8)
        self.assertEqual(work_entries_vals[1]['date'], date(2023, 8, 2))
        self.assertEqual(work_entries_vals[1]['duration'], 8)

    def test_separate_overlapping_work_entries_by_type(self):
        calendar = self.env['resource.calendar'].create({'name': 'Calendar'})
        employee = self.env['hr.employee'].create({
            'name': 'Test',
            'resource_calendar_id': calendar.id,
            'date_version': datetime(2024, 9, 1),
            'contract_date_start': datetime(2024, 9, 1),
            'contract_date_end': datetime(2024, 9, 30),
            'wage': 5000.0,
            'tz': 'Europe/Brussels',
        })
        calendar.attendance_ids -= calendar.attendance_ids.filtered(lambda attendance: attendance.dayofweek == '0')

        entry_type_1, entry_type_2 = self.env['hr.work.entry.type'].create([
            {'name': 'Work type 1', 'count_as': 'working_time', 'code': 'ENTRY_TYPE1'},
            {'name': 'Work type 2', 'count_as': 'working_time', 'code': 'ENTRY_TYPE2'},
        ])

        self.env['resource.calendar.attendance'].create([
            {
                'calendar_id': calendar.id,
                'dayofweek': '0',
                'hour_from': 8,
                'hour_to': 11,
                'work_entry_type_id': entry_type_1.id,
            },
            {
                'calendar_id': calendar.id,
                'dayofweek': '0',
                'hour_from': 11,
                'hour_to': 12,
                'work_entry_type_id': entry_type_1.id,
            },
            {
                'calendar_id': calendar.id,
                'dayofweek': '0',
                'hour_from': 13,
                'hour_to': 16,
                'work_entry_type_id': entry_type_1.id,
            },
            {
                'calendar_id': calendar.id,
                'dayofweek': '0',
                'hour_from': 16,
                'hour_to': 17,
                'work_entry_type_id': entry_type_2.id,
            },
        ])

        work_entries_vals = employee.generate_work_entries(datetime(2024, 9, 2), datetime(2024, 9, 2))
        work_entry_types = [entry['work_entry_type_id'] for entry in work_entries_vals]
        self.assertEqual(len(work_entries_vals), 2, 'A shift should be created for each pair of attendance by day')
        self.assertEqual((work_entry_types[0] + work_entry_types[1]), (entry_type_1 + entry_type_2))

    def test_work_entry_duration(self):
        """ Test the duration of a work entry is rounded to the nearest minute and correctly calculated """
        vals_list = [{
            'name': 'Test Work Entry',
            'employee_id': self.richard_emp,
            'version_id': self.richard_emp.version_id,
            'date_start': datetime(2023, 10, 1, 9, 0, 0),
            'date_stop': datetime(2023, 10, 1, 9, 59, 59, 999999),
            'work_entry_type_id': self.work_entry_type,
        }]
        vals_list = self.env['hr.version']._generate_work_entries_postprocess(vals_list)
        self.assertEqual(vals_list[0]['duration'], 1, "The duration should be 1 hour")

    def test_work_entry_different_calendars(self):
        """ Test work entries are correctly created for employees with versions that have different calendar types. """
        # create 4 employees that have versions corresponding to these 4 cases:
        # flexible calendar then standard calendar
        # standard calendar then flexible calendar
        # no calendar (fully flexible) then standard calendar
        # standard calendar then no calendar (fully flexible)
        # the cases of flexible then fully flexible and fully flexible then flexible are similar in logic to the last 2
        # so they should work if last 2 are working properly
        emp_flex_std, emp_std_flex, emp_fullyflex_std, emp_std_fullyflex = self.env['hr.employee'].create([
            {'name': 'emp flex std'},
            {'name': 'emp std flex'},
            {'name': 'emp fullyflex std'},
            {'name': 'emp fullyflex std'},
        ])
        self.env['hr.version'].create([{
            'employee_id': emp_flex_std.id,
            'resource_calendar_id': False,
            'hours_per_week': 21,
            'hours_per_day': 3,
            'date_version': datetime(2025, 9, 1),
            'contract_date_start': datetime(2025, 9, 1),
            'contract_date_end': datetime(2025, 9, 15),
            'name': 'Flex Contract',
            'wage': 5000.0,
            'active': True,
        },
        {
            'employee_id': emp_flex_std.id,
            'resource_calendar_id': self.resource_calendar_id.id,
            'date_version': datetime(2025, 9, 16),
            'contract_date_start': datetime(2025, 9, 16),
            'contract_date_end': datetime(2025, 9, 30),
            'name': 'Std Contract',
            'wage': 5000.0,
            'active': True,
        },
        {
            'employee_id': emp_std_flex.id,
            'resource_calendar_id': self.resource_calendar_id.id,
            'date_version': datetime(2025, 9, 1),
            'contract_date_start': datetime(2025, 9, 1),
            'contract_date_end': datetime(2025, 9, 15),
            'name': 'Std Contract',
            'wage': 5000.0,
            'active': True,
        },
        {
            'employee_id': emp_std_flex.id,
            'resource_calendar_id': False,
            'hours_per_week': 21,
            'hours_per_day': 3,
            'date_version': datetime(2025, 9, 16),
            'contract_date_start': datetime(2025, 9, 16),
            'contract_date_end': datetime(2025, 9, 30),
            'name': 'Flex Contract',
            'wage': 5000.0,
            'active': True,
        },
        {
            'employee_id': emp_fullyflex_std.id,
            'resource_calendar_id': False,
            'hours_per_week': 0,
            'hours_per_day': 0,
            'date_version': datetime(2025, 9, 1),
            'contract_date_start': datetime(2025, 9, 1),
            'contract_date_end': datetime(2025, 9, 15),
            'name': 'FullyFlex Contract',
            'wage': 5000.0,
            'active': True,
        },
        {
            'employee_id': emp_fullyflex_std.id,
            'resource_calendar_id': self.resource_calendar_id.id,
            'date_version': datetime(2025, 9, 16),
            'contract_date_start': datetime(2025, 9, 16),
            'contract_date_end': datetime(2025, 9, 30),
            'name': 'Std Contract',
            'wage': 5000.0,
            'active': True,
        },
        {
            'employee_id': emp_std_fullyflex.id,
            'resource_calendar_id': self.resource_calendar_id.id,
            'date_version': datetime(2025, 9, 1),
            'contract_date_start': datetime(2025, 9, 1),
            'contract_date_end': datetime(2025, 9, 15),
            'name': 'Std Contract',
            'wage': 5000.0,
            'active': True,
        },
        {
            'employee_id': emp_std_fullyflex.id,
            'resource_calendar_id': False,
            'hours_per_week': 0,
            'hours_per_day': 0,
            'date_version': datetime(2025, 9, 16),
            'contract_date_start': datetime(2025, 9, 16),
            'contract_date_end': datetime(2025, 9, 30),
            'name': 'FullyFlex Contract',
            'wage': 5000.0,
            'active': True,
        }])

        half1_date_start = date(2025, 9, 1)
        half1_date_end = date(2025, 9, 15)
        half2_date_start = date(2025, 9, 16)
        half2_date_end = date(2025, 9, 30)

        # generate work entries for the 4 employees between (2025, 9, 1) and (2025, 9, 30)
        # then split them into 2 halves (each half corresponding to the work entries generated by 1 contract)
        # the timezones are considered in the split
        all_work_entries_vals = (emp_flex_std + emp_std_flex + emp_fullyflex_std + emp_std_fullyflex).generate_work_entries(date(2025, 9, 1), date(2025, 9, 30))
        flex_std_work_entries_vals = [vals for vals in all_work_entries_vals if vals['employee_id'] == emp_flex_std]
        self.assertEqual(len(flex_std_work_entries_vals), 26)
        half1_entries = [vals for vals in flex_std_work_entries_vals if vals['date'] >= half1_date_start and vals['date'] <= half1_date_end]
        half2_entries = [vals for vals in flex_std_work_entries_vals if vals['date'] >= half2_date_start and vals['date'] <= half2_date_end]
        self.assertEqual(len(half1_entries), 15)  # 1 work entry per day (including weekend)
        self.assertTrue(all(entry['duration'] == 3 for entry in half1_entries))
        self.assertTrue(all(entry['version_id'].name == 'Flex Contract' for entry in half1_entries))
        self.assertEqual(len(half2_entries), 11)  # 1 work entries per day, no work entries for weekend
        self.assertTrue(all(entry['duration'] == 8 for entry in half2_entries))
        self.assertTrue(all(entry['version_id'].name == 'Std Contract' for entry in half2_entries))

        std_flex_work_entries = [vals for vals in all_work_entries_vals if vals['employee_id'] == emp_std_flex]
        self.assertEqual(len(std_flex_work_entries), 26)
        half1_entries = [vals for vals in std_flex_work_entries if vals['date'] >= half1_date_start and vals['date'] <= half1_date_end]
        half2_entries = [vals for vals in std_flex_work_entries if vals['date'] >= half2_date_start and vals['date'] <= half2_date_end]
        self.assertEqual(len(half1_entries), 11)  # 1 work entries per day, no work entries for weekend
        self.assertTrue(all(entry['duration'] == 8 for entry in half1_entries))
        self.assertTrue(all(entry['version_id'].name == 'Std Contract' for entry in half1_entries))
        self.assertEqual(len(half2_entries), 15)  # 1 work entry per day (including weekend)
        self.assertTrue(all(entry['duration'] == 3 for entry in half2_entries))
        self.assertTrue(all(entry['version_id'].name == 'Flex Contract' for entry in half2_entries))

        fullyflex_std_work_entries_vals = [vals for vals in all_work_entries_vals if vals['employee_id'] == emp_fullyflex_std]
        self.assertEqual(len(fullyflex_std_work_entries_vals), 26)
        half1_entries = [vals for vals in fullyflex_std_work_entries_vals if vals['date'] >= half1_date_start and vals['date'] <= half1_date_end]
        half2_entries = [vals for vals in fullyflex_std_work_entries_vals if vals['date'] >= half2_date_start and vals['date'] <= half2_date_end]
        self.assertEqual(len(half1_entries), 15)  # work entries cover the entire duration
        self.assertTrue(all(entry['duration'] == 24 for entry in half1_entries))
        self.assertTrue(all(entry['version_id'].name == 'FullyFlex Contract' for entry in half1_entries))
        self.assertEqual(len(half2_entries), 11)  # 1 work entries per day, no work entries for weekend
        self.assertTrue(all(entry['duration'] == 8 for entry in half2_entries))
        self.assertTrue(all(entry['version_id'].name == 'Std Contract' for entry in half2_entries))

        std_fullyflex_work_entries = [vals for vals in all_work_entries_vals if vals['employee_id'] == emp_std_fullyflex]
        self.assertEqual(len(std_fullyflex_work_entries), 26)
        half1_entries = [vals for vals in std_fullyflex_work_entries if vals['date'] >= half1_date_start and vals['date'] <= half1_date_end]
        half2_entries = [vals for vals in std_fullyflex_work_entries if vals['date'] >= half2_date_start and vals['date'] <= half2_date_end]
        self.assertEqual(len(half1_entries), 11)  # 1 work entries per day, no work entries for weekend
        self.assertTrue(all(entry['duration'] == 8 for entry in half1_entries))
        self.assertTrue(all(entry['version_id'].name == 'Std Contract' for entry in half1_entries))
        self.assertEqual(len(half2_entries), 15)  # work entries cover the entire duration
        self.assertTrue(all(entry['duration'] == 24 for entry in half2_entries))
        self.assertTrue(all(entry['version_id'].name == 'FullyFlex Contract' for entry in half2_entries))

    def test_work_entry_version_changed_after_generation(self):
        """
        When you generate work entries for a version, and you add a new version to the employee starting during the period of already generated work entries,
        The previous version work entries date generation to should be updated to the new version date and new work entries should be generated.
        Previous ones should be deleted
        """
        calendar_40h = self.env['resource.calendar'].create({
            'name': '40h Calendar',
            'hours_per_day': 8,
            'attendance_ids': [(5, 0, 0),
               (0, 0, {'dayofweek': '0', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '0', 'hour_from': 13, 'hour_to': 17}),
               (0, 0, {'dayofweek': '1', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '1', 'hour_from': 13, 'hour_to': 17}),
               (0, 0, {'dayofweek': '2', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '2', 'hour_from': 13, 'hour_to': 17}),
               (0, 0, {'dayofweek': '3', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '3', 'hour_from': 13, 'hour_to': 17}),
               (0, 0, {'dayofweek': '4', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '4', 'hour_from': 13, 'hour_to': 17})
            ]
        })
        calendar_35h = self.env['resource.calendar'].create({
            'name': '35h Calendar',
            'hours_per_day': 7,
            'attendance_ids': [(5, 0, 0),
                (0, 0, {'dayofweek': '0', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '0', 'hour_from': 13, 'hour_to': 16}),
                (0, 0, {'dayofweek': '1', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '1', 'hour_from': 13, 'hour_to': 16}),
                (0, 0, {'dayofweek': '2', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '2', 'hour_from': 13, 'hour_to': 16}),
                (0, 0, {'dayofweek': '3', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '3', 'hour_from': 13, 'hour_to': 16}),
                (0, 0, {'dayofweek': '4', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '4', 'hour_from': 13, 'hour_to': 16})
            ]
        })

        # first version with a 40h calendar
        employee = self.env['hr.employee'].create({
            'name': 'Test',
            'resource_calendar_id': calendar_40h.id,
            'date_version': datetime(2025, 1, 1),
            'contract_date_start': datetime(2025, 1, 1),
        })
        work_entries_vals = employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
        self.assertEqual(len(work_entries_vals), 23, "23 attendance")
        self.assertEqual(sum(vals['duration'] for vals in work_entries_vals), 184, "23 * 8h")

        # new version with a different calendar (35h) set in the tier of the month
        employee.create_version({
            'resource_calendar_id': calendar_35h.id,
            'date_version': datetime(2025, 1, 10),
        })
        work_entries_vals = employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
        self.assertEqual(len(work_entries_vals), 23, "23 attendance")
        self.assertEqual(sum(vals['duration'] for vals in work_entries_vals), 168, "7 * 8h + 16 * 7h")

        # new version with a different calendar (40h) set in the second tier of the month
        employee.create_version({
            'resource_calendar_id': calendar_40h.id,
            'date_version': datetime(2025, 1, 20),
        })
        work_entries_vals = employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
        self.assertEqual(len(work_entries_vals), 23, "23 attendance")
        self.assertEqual(sum(vals['duration'] for vals in work_entries_vals), 178, "7 * 8h + 6 * 7h + 10 * 8h")

    def test_work_entry_version_changed_after_generation2(self):
        """
        When you generate work entries for a version, and you add a new version to the employee starting during the period of already generated work entries,
        The previous version work entries date generation to should be updated to the new version date and new work entries should be generated.
        Previous ones should be deleted
        """
        calendar_40h = self.env['resource.calendar'].create({
            'name': '40h Calendar',
            'hours_per_day': 8,
            'attendance_ids': [(5, 0, 0),
               (0, 0, {'dayofweek': '0', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '0', 'hour_from': 13, 'hour_to': 17}),
               (0, 0, {'dayofweek': '1', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '1', 'hour_from': 13, 'hour_to': 17}),
               (0, 0, {'dayofweek': '2', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '2', 'hour_from': 13, 'hour_to': 17}),
               (0, 0, {'dayofweek': '3', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '3', 'hour_from': 13, 'hour_to': 17}),
               (0, 0, {'dayofweek': '4', 'hour_from': 7, 'hour_to': 11}),
               (0, 0, {'dayofweek': '4', 'hour_from': 13, 'hour_to': 17})
            ]
        })
        calendar_35h = self.env['resource.calendar'].create({
            'name': '35h Calendar',
            'hours_per_day': 7,
            'attendance_ids': [(5, 0, 0),
                (0, 0, {'dayofweek': '0', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '0', 'hour_from': 13, 'hour_to': 16}),
                (0, 0, {'dayofweek': '1', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '1', 'hour_from': 13, 'hour_to': 16}),
                (0, 0, {'dayofweek': '2', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '2', 'hour_from': 13, 'hour_to': 16}),
                (0, 0, {'dayofweek': '3', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '3', 'hour_from': 13, 'hour_to': 16}),
                (0, 0, {'dayofweek': '4', 'hour_from': 7, 'hour_to': 11}),
                (0, 0, {'dayofweek': '4', 'hour_from': 13, 'hour_to': 16})
            ]
        })

        # first version with a 40h calendar
        employee = self.env['hr.employee'].create({
            'name': 'Test',
            'resource_calendar_id': calendar_40h.id,
            'date_version': datetime(2025, 1, 1),
            'contract_date_start': datetime(2025, 1, 1),
        })
        work_entries_vals = employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
        self.assertEqual(len(work_entries_vals), 23, "23 attendance")
        self.assertEqual(sum(vals['duration'] for vals in work_entries_vals), 184, "23 * 8h")

        # new version with a different calendar (40h) set in the tier of the month
        employee.create_version({
            'resource_calendar_id': calendar_40h.id,
            'date_version': datetime(2025, 1, 20),
        })
        work_entries_vals = employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
        self.assertEqual(len(work_entries_vals), 23, "23 attendance")
        self.assertEqual(sum(vals['duration'] for vals in work_entries_vals), 184, "13 * 8h + 10 * 8h")

        # new version with a different calendar (35h) set in the second tier of the month
        employee.create_version({
            'resource_calendar_id': calendar_35h.id,
            'date_version': datetime(2025, 1, 10),
        })
        work_entries_vals = employee.generate_work_entries(datetime(2025, 1, 1), datetime(2025, 1, 31))
        self.assertEqual(len(work_entries_vals), 23, "23 attendance")
        self.assertEqual(sum(vals['duration'] for vals in work_entries_vals), 178, "7 * 8h + 6 * 7h + 10 * 8h")
