"""Tests for the maintenance module."""

from __future__ import annotations

import threading
import time
from datetime import datetime, timedelta

import pytest

from oclawma.maintenance import (
    MaintenanceManager,
    MaintenanceNotFoundError,
    MaintenanceState,
    MaintenanceStatus,
    MaintenanceWindow,
    RecurrenceType,
    get_maintenance_manager,
    set_maintenance_manager,
)


class TestMaintenanceWindow:
    """Test MaintenanceWindow model."""

    def test_default_creation(self):
        """Test creating a maintenance window with defaults."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Test Window",
            start_time=now,
            end_time=now + timedelta(hours=1),
        )

        assert window.id is None
        assert window.name == "Test Window"
        assert window.description is None
        assert window.timezone == "UTC"
        assert window.recurring == RecurrenceType.NONE
        assert window.recurrence_end is None
        assert window.enabled is True
        assert window.status == MaintenanceStatus.SCHEDULED
        assert window.metadata == {}

    def test_custom_creation(self):
        """Test creating a maintenance window with custom values."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            id=1,
            name="Deploy Window",
            description="Weekly deploy maintenance",
            start_time=now,
            end_time=now + timedelta(minutes=30),
            timezone="America/New_York",
            recurring=RecurrenceType.WEEKLY,
            recurrence_end=now + timedelta(weeks=4),
            enabled=True,
            metadata={"team": "platform", "jira": "DEPLOY-123"},
        )

        assert window.id == 1
        assert window.name == "Deploy Window"
        assert window.description == "Weekly deploy maintenance"
        assert window.timezone == "America/New_York"
        assert window.recurring == RecurrenceType.WEEKLY
        assert window.metadata == {"team": "platform", "jira": "DEPLOY-123"}

    def test_end_time_validation(self):
        """Test that end_time must be after start_time."""
        now = datetime.utcnow()

        with pytest.raises(ValueError, match="end_time must be after start_time"):
            MaintenanceWindow(
                name="Invalid Window",
                start_time=now,
                end_time=now - timedelta(hours=1),
            )

        with pytest.raises(ValueError, match="end_time must be after start_time"):
            MaintenanceWindow(
                name="Invalid Window",
                start_time=now,
                end_time=now,
            )

    def test_is_active_current(self):
        """Test is_active for current window."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Current Window",
            start_time=now - timedelta(minutes=30),
            end_time=now + timedelta(minutes=30),
        )

        assert window.is_active(now) is True
        assert window.is_active() is True

    def test_is_active_past(self):
        """Test is_active for past window."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Past Window",
            start_time=now - timedelta(hours=2),
            end_time=now - timedelta(hours=1),
        )

        assert window.is_active(now) is False

    def test_is_active_future(self):
        """Test is_active for future window."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Future Window",
            start_time=now + timedelta(hours=1),
            end_time=now + timedelta(hours=2),
        )

        assert window.is_active(now) is False

    def test_is_active_disabled(self):
        """Test is_active returns False for disabled window."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Disabled Window",
            start_time=now - timedelta(minutes=30),
            end_time=now + timedelta(minutes=30),
            enabled=False,
        )

        assert window.is_active(now) is False

    def test_duration_seconds(self):
        """Test duration calculation."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Test Window",
            start_time=now,
            end_time=now + timedelta(minutes=45),
        )

        assert window.duration_seconds() == 45 * 60

    def test_get_next_occurrence_none(self):
        """Test get_next_occurrence for non-recurring window."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="One-time Window",
            start_time=now,
            end_time=now + timedelta(hours=1),
            recurring=RecurrenceType.NONE,
        )

        assert window.get_next_occurrence() is None

    def test_get_next_occurrence_daily(self):
        """Test get_next_occurrence for daily recurring window."""
        now = datetime.utcnow()
        # Create a window that started yesterday at a specific time
        yesterday_start = now - timedelta(days=1)
        window = MaintenanceWindow(
            name="Daily Window",
            start_time=yesterday_start,
            end_time=yesterday_start + timedelta(hours=1),
            recurring=RecurrenceType.DAILY,
        )

        next_occurrence = window.get_next_occurrence(now)

        # Should be tomorrow at the same time as original start
        assert next_occurrence is not None
        # Should be approximately 1 day from now (within a few hours tolerance for DST/timezone)
        time_diff = (next_occurrence - now).total_seconds()
        assert 20 * 3600 < time_diff < 28 * 3600  # Between 20 and 28 hours

    def test_get_next_occurrence_weekly(self):
        """Test get_next_occurrence for weekly recurring window."""
        now = datetime.utcnow()
        # Create a window that started last week
        last_week_start = now - timedelta(weeks=1)
        window = MaintenanceWindow(
            name="Weekly Window",
            start_time=last_week_start,
            end_time=last_week_start + timedelta(hours=1),
            recurring=RecurrenceType.WEEKLY,
        )

        next_occurrence = window.get_next_occurrence(now)

        # Should be approximately 1 week from original start time
        assert next_occurrence is not None
        # Should be approximately 1 week from now (with some tolerance)
        time_diff = abs((next_occurrence - now).total_seconds())
        assert 6 * 24 * 3600 < time_diff < 8 * 24 * 3600  # Between 6 and 8 days

    def test_get_next_occurrence_monthly(self):
        """Test get_next_occurrence for monthly recurring window."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Monthly Window",
            start_time=now - timedelta(hours=1),
            end_time=now + timedelta(hours=1),
            recurring=RecurrenceType.MONTHLY,
        )

        next_occurrence = window.get_next_occurrence(now)
        assert next_occurrence is not None
        # Should be approximately one month later
        assert next_occurrence > now

    def test_get_next_occurrence_past_recurrence_end(self):
        """Test get_next_occurrence respects recurrence_end."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Limited Window",
            start_time=now - timedelta(days=1),
            end_time=now - timedelta(days=1) + timedelta(hours=1),
            recurring=RecurrenceType.DAILY,
            recurrence_end=now - timedelta(hours=1),
        )

        assert window.get_next_occurrence(now) is None

    def test_to_from_db_dict_roundtrip(self):
        """Test database serialization round-trip."""
        now = datetime.utcnow()
        original = MaintenanceWindow(
            id=42,
            name="Test Window",
            description="Test description",
            start_time=now,
            end_time=now + timedelta(hours=1),
            timezone="America/Los_Angeles",
            recurring=RecurrenceType.WEEKLY,
            recurrence_end=now + timedelta(weeks=4),
            enabled=True,
            status=MaintenanceStatus.ACTIVE,
            metadata={"key": "value"},
        )

        db_dict = original.to_db_dict()

        assert db_dict["id"] == 42
        assert db_dict["name"] == "Test Window"
        assert db_dict["timezone"] == "America/Los_Angeles"
        assert db_dict["recurring"] == "weekly"
        assert db_dict["enabled"] == 1


class TestMaintenanceManager:
    """Test MaintenanceManager."""

    @pytest.fixture
    def manager(self, tmp_path):
        """Create a temporary maintenance manager."""
        db_path = tmp_path / "maintenance.db"
        return MaintenanceManager(db_path)

    def test_init_creates_db(self, tmp_path):
        """Test that initialization creates database."""
        db_path = tmp_path / "maintenance.db"
        manager = MaintenanceManager(db_path)

        assert db_path.exists()
        manager.close()

    def test_schedule_window(self, manager):
        """Test scheduling a maintenance window."""
        now = datetime.utcnow()
        window = manager.schedule_window(
            name="Test Window",
            start=now,
            duration_minutes=30,
            description="Test description",
        )

        assert window.id is not None
        assert window.name == "Test Window"
        assert window.description == "Test description"
        assert window.start_time == now
        assert window.end_time == now + timedelta(minutes=30)

    def test_schedule_window_with_recurrence(self, manager):
        """Test scheduling a recurring maintenance window."""
        now = datetime.utcnow()
        window = manager.schedule_window(
            name="Weekly Deploy",
            start=now,
            duration_minutes=60,
            recurring=RecurrenceType.WEEKLY,
        )

        assert window.recurring == RecurrenceType.WEEKLY

    def test_get_window(self, manager):
        """Test getting a window by ID."""
        now = datetime.utcnow()
        window = manager.schedule_window(
            name="Test Window",
            start=now,
            duration_minutes=30,
        )

        retrieved = manager.get_window(window.id)
        assert retrieved.id == window.id
        assert retrieved.name == window.name

    def test_get_window_not_found(self, manager):
        """Test getting a non-existent window raises error."""
        with pytest.raises(MaintenanceNotFoundError):
            manager.get_window(999)

    def test_list_windows(self, manager):
        """Test listing all windows."""
        now = datetime.utcnow()

        # Create multiple windows
        for i in range(3):
            manager.schedule_window(
                name=f"Window {i}",
                start=now + timedelta(hours=i),
                duration_minutes=30,
            )

        windows = manager.list_windows()
        assert len(windows) == 3

    def test_list_windows_active_only(self, manager):
        """Test listing only active windows."""
        now = datetime.utcnow()

        # Create active window
        manager.schedule_window(
            name="Active Window",
            start=now - timedelta(minutes=30),
            duration_minutes=60,  # 30 mins ago + 60 mins duration = ends 30 mins from now
        )

        # Create future window
        manager.schedule_window(
            name="Future Window",
            start=now + timedelta(hours=1),
            duration_minutes=30,
        )

        active = manager.list_windows(active_only=True)
        assert len(active) == 1
        assert active[0].name == "Active Window"

    def test_list_windows_enabled_only(self, manager):
        """Test listing only enabled windows."""
        now = datetime.utcnow()

        # Create enabled window
        manager.schedule_window(
            name="Enabled Window",
            start=now,
            duration_minutes=30,
            enabled=True,
        )

        # Create disabled window
        manager.schedule_window(
            name="Disabled Window",
            start=now,
            duration_minutes=30,
            enabled=False,
        )

        enabled = manager.list_windows(enabled_only=True)
        assert len(enabled) == 1
        assert enabled[0].name == "Enabled Window"

    def test_update_window(self, manager):
        """Test updating a window."""
        now = datetime.utcnow()
        window = manager.schedule_window(
            name="Original Name",
            start=now,
            duration_minutes=30,
        )

        updated = manager.update_window(
            window.id,
            name="Updated Name",
            description="Updated description",
        )

        assert updated.name == "Updated Name"
        assert updated.description == "Updated description"

        # Verify in database
        retrieved = manager.get_window(window.id)
        assert retrieved.name == "Updated Name"

    def test_update_window_metadata_merge(self, manager):
        """Test that metadata is merged during update."""
        now = datetime.utcnow()
        window = manager.schedule_window(
            name="Test Window",
            start=now,
            duration_minutes=30,
            metadata={"key1": "value1"},
        )

        updated = manager.update_window(
            window.id,
            metadata={"key2": "value2"},
        )

        assert updated.metadata == {"key1": "value1", "key2": "value2"}

    def test_delete_window(self, manager):
        """Test deleting a window."""
        now = datetime.utcnow()
        window = manager.schedule_window(
            name="To Delete",
            start=now,
            duration_minutes=30,
        )

        manager.delete_window(window.id)

        with pytest.raises(MaintenanceNotFoundError):
            manager.get_window(window.id)

    def test_delete_window_not_found(self, manager):
        """Test deleting a non-existent window raises error."""
        with pytest.raises(MaintenanceNotFoundError):
            manager.delete_window(999)

    def test_is_in_maintenance_no_windows(self, manager):
        """Test is_in_maintenance returns False with no windows."""
        assert manager.is_in_maintenance() is False

    def test_is_in_maintenance_active_window(self, manager):
        """Test is_in_maintenance returns True when window is active."""
        now = datetime.utcnow()
        manager.schedule_window(
            name="Active Window",
            start=now - timedelta(minutes=30),
            duration_minutes=60,  # started 30 mins ago, lasts 60 mins = still active for 30 more mins
        )

        assert manager.is_in_maintenance() is True

    def test_is_in_maintenance_future_window(self, manager):
        """Test is_in_maintenance returns False for future window."""
        now = datetime.utcnow()
        manager.schedule_window(
            name="Future Window",
            start=now + timedelta(hours=1),
            duration_minutes=30,
        )

        assert manager.is_in_maintenance() is False

    def test_is_in_maintenance_disabled_window(self, manager):
        """Test is_in_maintenance returns False for disabled window."""
        now = datetime.utcnow()
        manager.schedule_window(
            name="Disabled Window",
            start=now - timedelta(minutes=30),
            duration_minutes=60,
            enabled=False,
        )

        assert manager.is_in_maintenance() is False

    def test_is_in_maintenance_past_window(self, manager):
        """Test is_in_maintenance returns False for past window."""
        now = datetime.utcnow()
        manager.schedule_window(
            name="Past Window",
            start=now - timedelta(hours=2),
            duration_minutes=60,  # Started 2 hours ago, lasted 60 mins = ended 1 hour ago
        )

        assert manager.is_in_maintenance() is False

    def test_get_maintenance_state(self, manager):
        """Test getting maintenance state."""
        now = datetime.utcnow()
        manager.schedule_window(
            name="Active Window",
            start=now - timedelta(minutes=30),
            duration_minutes=60,
        )

        state = manager.get_maintenance_state()
        assert isinstance(state, MaintenanceState)
        assert state.in_maintenance is True
        assert state.active_window is not None
        assert state.active_window.name == "Active Window"
        assert state.maintenance_start is not None
        assert state.maintenance_end is not None

    def test_get_maintenance_state_with_next_window(self, manager):
        """Test getting maintenance state shows next upcoming window."""
        now = datetime.utcnow()
        manager.schedule_window(
            name="Future Window",
            start=now + timedelta(hours=1),
            duration_minutes=30,
        )

        state = manager.get_maintenance_state()
        assert state.in_maintenance is False
        assert state.active_window is None
        assert state.next_window is not None
        assert state.next_window.name == "Future Window"

    def test_cleanup_expired(self, manager):
        """Test cleaning up expired windows."""
        now = datetime.utcnow()

        # Create expired disabled window
        manager.schedule_window(
            name="Expired",
            start=now - timedelta(days=7),
            duration_minutes=60,
            enabled=False,
        )

        # Create active window (should not be deleted)
        manager.schedule_window(
            name="Active",
            start=now - timedelta(minutes=30),
            duration_minutes=60,
        )

        deleted = manager.cleanup_expired()
        assert deleted == 1

        windows = manager.list_windows()
        assert len(windows) == 1
        assert windows[0].name == "Active"

    def test_get_stats(self, manager):
        """Test getting statistics."""
        now = datetime.utcnow()

        # Create windows
        manager.schedule_window(
            name="Active Window",
            start=now - timedelta(minutes=30),
            duration_minutes=60,
        )
        manager.schedule_window(
            name="Future Window",
            start=now + timedelta(hours=1),
            duration_minutes=30,
        )
        manager.schedule_window(
            name="Disabled Window",
            start=now + timedelta(hours=2),
            duration_minutes=30,
            enabled=False,
        )

        stats = manager.get_stats()
        assert stats["total_windows"] == 3
        assert stats["enabled_windows"] == 2
        assert stats["active_windows"] == 1
        assert stats["upcoming_windows"] == 1
        assert stats["in_maintenance"] is True

    def test_maintenance_callbacks(self, tmp_path):
        """Test maintenance start/end callbacks."""
        callbacks_triggered = {"start": False, "end": False}

        def on_start(window):
            callbacks_triggered["start"] = True

        def on_end(window):
            callbacks_triggered["end"] = True

        db_path = tmp_path / "maintenance.db"
        manager = MaintenanceManager(
            db_path,
            check_interval_seconds=0.1,
            on_maintenance_start=on_start,
            on_maintenance_end=on_end,
        )

        now = datetime.utcnow()
        manager.schedule_window(
            name="Test Window",
            start=now - timedelta(minutes=1),
            duration_minutes=2,  # Started 1 min ago, lasts 2 mins = ends 1 min from now
        )

        # Start monitoring
        manager.start_monitoring()

        # Wait for callback to trigger
        time.sleep(0.2)

        assert callbacks_triggered["start"] is True

        # Update window to end in the past
        manager.update_window(
            1,
            end_time=now - timedelta(seconds=1),
        )

        # Wait for end callback
        time.sleep(0.2)

        # Force state refresh
        manager.is_in_maintenance()

        manager.stop_monitoring()

    def test_start_stop_monitoring(self, manager):
        """Test starting and stopping the monitoring thread."""
        manager.start_monitoring()
        assert manager._monitor_thread is not None
        assert manager._monitor_thread.is_alive()

        manager.stop_monitoring()
        assert not manager._monitor_thread.is_alive()

    def test_concurrent_access(self, manager):
        """Test thread-safe concurrent access."""
        now = datetime.utcnow()
        errors = []

        def create_windows():
            try:
                for i in range(10):
                    manager.schedule_window(
                        name=f"Window {i}",
                        start=now + timedelta(minutes=i),
                        duration_minutes=30,
                    )
            except Exception as e:
                errors.append(e)

        # Create multiple threads
        threads = [threading.Thread(target=create_windows) for _ in range(3)]

        for t in threads:
            t.start()

        for t in threads:
            t.join()

        assert len(errors) == 0
        assert len(manager.list_windows()) == 30


class TestGlobalManager:
    """Test global maintenance manager functions."""

    def test_get_maintenance_manager_creates_default(self, tmp_path):
        """Test that get_maintenance_manager creates default instance."""
        # Reset global state
        set_maintenance_manager(None)

        # Override home directory for testing
        import os

        original_home = os.environ.get("HOME")
        os.environ["HOME"] = str(tmp_path)

        try:
            manager = get_maintenance_manager()
            assert manager is not None
            assert isinstance(manager, MaintenanceManager)
        finally:
            if original_home:
                os.environ["HOME"] = original_home

        # Cleanup
        set_maintenance_manager(None)

    def test_get_maintenance_manager_with_custom_path(self, tmp_path):
        """Test get_maintenance_manager with custom path."""
        set_maintenance_manager(None)
        db_path = tmp_path / "custom.db"

        manager = get_maintenance_manager(db_path)
        assert manager is not None
        assert db_path.exists()

        # Cleanup
        set_maintenance_manager(None)

    def test_set_maintenance_manager(self, tmp_path):
        """Test setting the global maintenance manager."""
        db_path = tmp_path / "test.db"
        custom_manager = MaintenanceManager(db_path)

        set_maintenance_manager(custom_manager)
        retrieved = get_maintenance_manager()

        assert retrieved is custom_manager

        # Cleanup
        set_maintenance_manager(None)


class TestMaintenanceWindowEdgeCases:
    """Test edge cases for MaintenanceWindow."""

    def test_timezone_validation(self):
        """Test timezone validation."""
        now = datetime.utcnow()

        # Valid timezone should work
        window = MaintenanceWindow(
            name="Valid TZ",
            start_time=now,
            end_time=now + timedelta(hours=1),
            timezone="UTC",
        )
        assert window.timezone == "UTC"

        # Another valid timezone
        window2 = MaintenanceWindow(
            name="Valid TZ 2",
            start_time=now,
            end_time=now + timedelta(hours=1),
            timezone="America/New_York",
        )
        assert window2.timezone == "America/New_York"

    def test_timezone_validation_invalid(self):
        """Test that invalid timezone raises error."""
        now = datetime.utcnow()

        with pytest.raises(ValueError):
            MaintenanceWindow(
                name="Invalid TZ",
                start_time=now,
                end_time=now + timedelta(hours=1),
                timezone="Invalid/Timezone",
            )

    def test_naive_datetime_handling(self):
        """Test handling of naive datetime objects."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Test",
            start_time=now,
            end_time=now + timedelta(hours=1),
        )

        # Should handle naive datetimes
        assert window.is_active(now) is True
        assert window.is_active(now + timedelta(hours=2)) is False

    def test_empty_metadata(self):
        """Test window with empty metadata."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Test",
            start_time=now,
            end_time=now + timedelta(hours=1),
            metadata={},
        )

        db_dict = window.to_db_dict()
        assert db_dict["metadata"] == "{}"

    def test_metadata_with_special_characters(self):
        """Test metadata with special characters."""
        now = datetime.utcnow()
        window = MaintenanceWindow(
            name="Test",
            start_time=now,
            end_time=now + timedelta(hours=1),
            metadata={"key": 'value with "quotes" and unicode: 你好'},
        )

        db_dict = window.to_db_dict()
        # Should be valid JSON
        import json

        parsed = json.loads(db_dict["metadata"])
        assert parsed["key"] == 'value with "quotes" and unicode: 你好'


class TestMaintenanceManagerEdgeCases:
    """Test edge cases for MaintenanceManager."""

    @pytest.fixture
    def manager(self, tmp_path):
        """Create a temporary maintenance manager."""
        db_path = tmp_path / "maintenance.db"
        return MaintenanceManager(db_path)

    def test_in_memory_database(self):
        """Test using in-memory database.

        Note: In-memory SQLite creates a new database per connection,
        so we can only test that initialization and scheduling work.
        """
        manager = MaintenanceManager(":memory:")
        now = datetime.utcnow()

        # Just verify the database is initialized and we can schedule
        # (in-memory databases don't persist across connections)
        window = manager.schedule_window(
            name="In-Memory Window",
            start=now,
            duration_minutes=30,
        )

        assert window.id is not None
        # For in-memory, we can retrieve within the same connection lifecycle
        # (The manager keeps a single connection internally)
        manager.close()

    def test_multiple_windows_same_time(self, manager):
        """Test multiple windows scheduled at the same time."""
        now = datetime.utcnow()

        for i in range(5):
            manager.schedule_window(
                name=f"Window {i}",
                start=now - timedelta(minutes=30),
                duration_minutes=60,  # Active for 60 mins from 30 mins ago = still active
            )

        # All should be returned as active
        active = manager.list_windows(active_only=True)
        assert len(active) == 5

        # is_in_maintenance should return True
        assert manager.is_in_maintenance() is True

    def test_very_long_duration(self, manager):
        """Test window with very long duration."""
        now = datetime.utcnow()

        window = manager.schedule_window(
            name="Long Window",
            start=now - timedelta(days=365),
            duration_minutes=365 * 24 * 60 * 2,  # 2 years
        )

        assert manager.is_in_maintenance() is True
        assert window.duration_seconds() == 365 * 24 * 60 * 2 * 60

    def test_very_short_duration(self, manager):
        """Test window with very short duration."""
        now = datetime.utcnow()

        window = manager.schedule_window(
            name="Short Window",
            start=now - timedelta(seconds=1),
            duration_minutes=1 / 60,  # 1 second
        )

        # Depending on timing, might or might not be active
        duration = window.duration_seconds()
        assert duration == 1.0

    def test_window_with_special_characters_in_name(self, manager):
        """Test window name with special characters."""
        now = datetime.utcnow()

        window = manager.schedule_window(
            name="Window with 'quotes' and \"double quotes\" and emoji 🚀",
            start=now,
            duration_minutes=30,
        )

        retrieved = manager.get_window(window.id)
        assert retrieved.name == "Window with 'quotes' and \"double quotes\" and emoji 🚀"

    def test_pagination(self, manager):
        """Test listing with limit and offset."""
        now = datetime.utcnow()

        # Create 10 windows
        for i in range(10):
            manager.schedule_window(
                name=f"Window {i}",
                start=now + timedelta(hours=i),
                duration_minutes=30,
            )

        # Get first 5
        first_5 = manager.list_windows(limit=5, offset=0)
        assert len(first_5) == 5

        # Get next 5
        next_5 = manager.list_windows(limit=5, offset=5)
        assert len(next_5) == 5

        # Verify they're different
        first_ids = {w.id for w in first_5}
        next_ids = {w.id for w in next_5}
        assert not first_ids.intersection(next_ids)
