# Copyright (c) 2025 Thomas Goirand <zigo@debian.org>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Unit tests for VMMS worker migration window logic."""

import datetime
from unittest import mock
from unittest import TestCase


class FakeConfGroup:
    """Fake CONF group that supports attribute and subscript access."""
    def __init__(self, **kwargs):
        self._attrs = kwargs
    
    def __getattr__(self, name):
        if name in self._attrs:
            return self._attrs[name]
        return None
    
    def __getitem__(self, key):
        if key in self._attrs:
            return self._attrs[key]
        return FakeConfGroup()


class FakeConf:
    """Minimal fake CONF object for testing."""
    def __init__(self, start_time='22:00', end_time='05:00'):
        self.worker = FakeConfGroup(
            migration_start_time=start_time,
            migration_end_time=end_time,
            my_hostname='test-host',
            coordination_url=None,
            check_interval=60,
            parallel_jobs=5,
            mistral_use_keystone_auth=False,
            mistral_auth_section='mistral',
        )
        self.mistral = FakeConfGroup()
    
    def __getitem__(self, key):
        if key == 'worker':
            return self.worker
        if key == 'mistral':
            return self.mistral
        return FakeConfGroup()


class TestMigrationWindow(TestCase):
    """Test cases for is_within_migration_window() method."""

    def setUp(self):
        """Set up test fixtures with mocked MistralClient."""
        # Mock MistralClient before importing WorkerService
        self.mistral_patcher = mock.patch('vmms.worker.MistralClient')
        self.mock_mistral_class = self.mistral_patcher.start()
        
        # Create a mock instance that won't try to initialize
        self.mock_mistral_instance = mock.Mock()
        self.mock_mistral_instance.client = None  # Simulate failed init (doesn't matter for our tests)
        self.mock_mistral_class.return_value = self.mock_mistral_instance
        
        self.addCleanup(self.mistral_patcher.stop)

    def _create_worker(self, start_time='22:00', end_time='05:00'):
        """Create a WorkerService instance with fake config."""
        from vmms.worker import WorkerService
        conf = FakeConf(start_time, end_time)
        return WorkerService(conf)

    def _create_mock_datetime(self, year, month, day, hour, minute=0):
        """Create a datetime object for testing."""
        return datetime.datetime(year, month, day, hour, minute)

    def _test_window_with_datetime(
        self,
        test_datetime,
        expected_result,
        start_time='22:00',
        end_time='05:00',
        test_name=''
    ):
        """Helper method to test migration window with specific datetime."""
        worker = self._create_worker(start_time, end_time)
        
        mock_datetime = mock.Mock()
        mock_datetime.now.return_value = test_datetime
        mock_datetime.strptime = datetime.datetime.strptime
        mock_datetime.time = datetime.time
        
        with mock.patch('vmms.worker.datetime.datetime', mock_datetime):
            result = worker.is_within_migration_window()
            
            assertion_msg = (
                f"{test_name} - "
                f"{test_datetime.strftime('%A %Y-%m-%d %H:%M')} "
                f"(start={start_time}, end={end_time}): "
                f"expected {expected_result}, got {result}"
            )
            self.assertEqual(result, expected_result, assertion_msg)

    #######################
    ### Overnight tests ###
    #######################
    def test_overnight_monday_evening_should_allow(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 23, 0)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'Monday evening')

    def test_overnight_friday_morning_should_allow(self):
        test_dt = self._create_mock_datetime(2026, 1, 9, 3, 0)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'Friday morning')

    def test_overnight_friday_evening_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 9, 23, 0)
        self._test_window_with_datetime(test_dt, False, '22:00', '05:00', 'Friday evening')

    def test_overnight_saturday_morning_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 10, 3, 0)
        self._test_window_with_datetime(test_dt, False, '22:00', '05:00', 'Saturday morning')

    def test_overnight_saturday_evening_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 10, 23, 0)
        self._test_window_with_datetime(test_dt, False, '22:00', '05:00', 'Saturday evening')

    def test_overnight_sunday_morning_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 11, 3, 0)
        self._test_window_with_datetime(test_dt, False, '22:00', '05:00', 'Sunday morning')

    def test_overnight_sunday_evening_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 11, 23, 0)
        self._test_window_with_datetime(test_dt, False, '22:00', '05:00', 'Sunday evening')

    def test_overnight_monday_morning_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 3, 0)
        self._test_window_with_datetime(test_dt, False, '22:00', '05:00', 'Monday morning')

    def test_overnight_thursday_evening_should_allow(self):
        test_dt = self._create_mock_datetime(2026, 1, 8, 23, 0)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'Thursday evening')

    def test_overnight_tuesday_morning_should_allow(self):
        test_dt = self._create_mock_datetime(2026, 1, 6, 3, 0)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'Tuesday morning')

    ######################
    ### Same day tests ###
    ######################
    def test_sameday_monday_morning_should_allow(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 10, 0)
        self._test_window_with_datetime(test_dt, True, '09:00', '18:00', 'Monday morning')

    def test_sameday_monday_evening_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 19, 0)
        self._test_window_with_datetime(test_dt, False, '09:00', '18:00', 'Monday evening')

    def test_sameday_monday_early_morning_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 8, 0)
        self._test_window_with_datetime(test_dt, False, '09:00', '18:00', 'Monday early morning')

    def test_sameday_friday_morning_should_allow(self):
        test_dt = self._create_mock_datetime(2026, 1, 9, 14, 0)
        self._test_window_with_datetime(test_dt, True, '09:00', '18:00', 'Friday morning')

    def test_sameday_friday_evening_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 9, 19, 0)
        self._test_window_with_datetime(test_dt, False, '09:00', '18:00', 'Friday evening')

    def test_sameday_saturday_morning_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 10, 10, 0)
        self._test_window_with_datetime(test_dt, False, '09:00', '18:00', 'Saturday morning')

    def test_sameday_saturday_evening_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 10, 15, 0)
        self._test_window_with_datetime(test_dt, False, '09:00', '18:00', 'Saturday evening')

    def test_sameday_sunday_morning_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 11, 10, 0)
        self._test_window_with_datetime(test_dt, False, '09:00', '18:00', 'Sunday morning')

    def test_sameday_sunday_evening_should_block(self):
        test_dt = self._create_mock_datetime(2026, 1, 11, 15, 0)
        self._test_window_with_datetime(test_dt, False, '09:00', '18:00', 'Sunday evening')

    def test_sameday_wednesday_midday_should_allow(self):
        test_dt = self._create_mock_datetime(2026, 1, 7, 12, 0)
        self._test_window_with_datetime(test_dt, True, '09:00', '18:00', 'Wednesday midday')

    def test_sameday_thursday_morning_should_allow(self):
        test_dt = self._create_mock_datetime(2026, 1, 8, 9, 0)
        self._test_window_with_datetime(test_dt, True, '09:00', '18:00', 'Thursday morning')

    ############################################
    ### Overnight exact start time edge case ###
    ############################################
    def test_overnight_exact_start_time(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 22, 0)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'Exactly at start time')

    def test_overnight_exact_end_time(self):
        test_dt = self._create_mock_datetime(2026, 1, 9, 5, 0)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'Exactly at end time')

    def test_overnight_one_minute_after_start(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 22, 1)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'One minute after start')

    def test_overnight_one_minute_before_end(self):
        test_dt = self._create_mock_datetime(2026, 1, 9, 4, 59)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'One minute before end')

    def test_overnight_midnight(self):
        test_dt = self._create_mock_datetime(2026, 1, 6, 0, 0)
        self._test_window_with_datetime(test_dt, True, '22:00', '05:00', 'Midnight Tuesday')

    ######################################
    ### Overnight exact time edge case ###
    ######################################
    def test_sameday_exact_start_time(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 9, 0)
        self._test_window_with_datetime(test_dt, True, '09:00', '18:00', 'Exactly at start time')

    def test_sameday_exact_end_time(self):
        test_dt = self._create_mock_datetime(2026, 1, 5, 18, 0)
        self._test_window_with_datetime(test_dt, True, '09:00', '18:00', 'Exactly at end time')

    #########################################################
    ### FULL DAY WINDOW TESTS (00:00-23:59, weekday only) ###
    #########################################################
    def test_fullday_monday_morning_should_allow(self):
        """Monday 06:00 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 5, 6, 0)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Monday morning full-day'
        )

    def test_fullday_monday_midday_should_allow(self):
        """Monday 12:00 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 5, 12, 0)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Monday midday full-day'
        )

    def test_fullday_monday_evening_should_allow(self):
        """Monday 20:00 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 5, 20, 0)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Monday evening full-day'
        )

    def test_fullday_monday_night_should_allow(self):
        """Monday 23:59 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 5, 23, 59)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Monday night full-day'
        )

    def test_fullday_tuesday_morning_should_allow(self):
        """Tuesday 03:00 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 6, 3, 0)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Tuesday morning full-day'
        )

    def test_fullday_wednesday_afternoon_should_allow(self):
        """Wednesday 15:00 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 7, 15, 0)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Wednesday afternoon full-day'
        )

    def test_fullday_thursday_night_should_allow(self):
        """Thursday 22:00 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 8, 22, 0)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Thursday night full-day'
        )

    def test_fullday_friday_morning_should_allow(self):
        """Friday 08:00 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 9, 8, 0)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Friday morning full-day'
        )

    def test_fullday_friday_evening_should_allow(self):
        """Friday 18:00 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 9, 18, 0)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Friday evening full-day'
        )

    def test_fullday_friday_night_should_allow(self):
        """Friday 23:59 with full-day window (00:00-23:59) should be allowed."""
        test_dt = self._create_mock_datetime(2026, 1, 9, 23, 59)
        self._test_window_with_datetime(
            test_dt, True, '00:00', '23:59', 'Friday night full-day'
        )

    def test_fullday_saturday_morning_should_block(self):
        """Saturday 06:00 with full-day window (00:00-23:59) should be blocked (weekend)."""
        test_dt = self._create_mock_datetime(2026, 1, 10, 6, 0)
        self._test_window_with_datetime(
            test_dt, False, '00:00', '23:59', 'Saturday morning full-day'
        )

    def test_fullday_saturday_midday_should_block(self):
        """Saturday 12:00 with full-day window (00:00-23:59) should be blocked (weekend)."""
        test_dt = self._create_mock_datetime(2026, 1, 10, 12, 0)
        self._test_window_with_datetime(
            test_dt, False, '00:00', '23:59', 'Saturday midday full-day'
        )

    def test_fullday_saturday_evening_should_block(self):
        """Saturday 20:00 with full-day window (00:00-23:59) should be blocked (weekend)."""
        test_dt = self._create_mock_datetime(2026, 1, 10, 20, 0)
        self._test_window_with_datetime(
            test_dt, False, '00:00', '23:59', 'Saturday evening full-day'
        )

    def test_fullday_sunday_morning_should_block(self):
        """Sunday 06:00 with full-day window (00:00-23:59) should be blocked (weekend)."""
        test_dt = self._create_mock_datetime(2026, 1, 11, 6, 0)
        self._test_window_with_datetime(
            test_dt, False, '00:00', '23:59', 'Sunday morning full-day'
        )

    def test_fullday_sunday_midday_should_block(self):
        """Sunday 12:00 with full-day window (00:00-23:59) should be blocked (weekend)."""
        test_dt = self._create_mock_datetime(2026, 1, 11, 12, 0)
        self._test_window_with_datetime(
            test_dt, False, '00:00', '23:59', 'Sunday midday full-day'
        )

    def test_fullday_sunday_evening_should_block(self):
        """Sunday 20:00 with full-day window (00:00-23:59) should be blocked (weekend)."""
        test_dt = self._create_mock_datetime(2026, 1, 11, 20, 0)
        self._test_window_with_datetime(
            test_dt, False, '00:00', '23:59', 'Sunday evening full-day'
        )

    def test_fullday_sunday_night_should_block(self):
        """Sunday 23:59 with full-day window (00:00-23:59) should be blocked (weekend)."""
        test_dt = self._create_mock_datetime(2026, 1, 11, 23, 59)
        self._test_window_with_datetime(
            test_dt, False, '00:00', '23:59', 'Sunday night full-day'
        )
