"""
Extended Tests for Status Tracker

Phase 16: Download Management Deep Coverage - Status Tracker Tests
Tests status tracking, cleanup operations, and database interactions.
"""

import pytest
from datetime import datetime, timedelta, UTC
from unittest.mock import patch, MagicMock
import threading

from local_deep_research.library.download_management.status_tracker import (
    ResourceStatusTracker,
)
from local_deep_research.library.download_management.failure_classifier import (
    PermanentFailure,
    TemporaryFailure,
)


class TestStatusTracking:
    """Tests for status tracking functionality"""

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_can_retry_eligible_resource(self, mock_base, mock_db_manager):
        """Test can_retry for an eligible resource"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Mock the session and query to return no status
        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        with patch.object(tracker, "_get_session", return_value=mock_session):
            can_retry, reason = tracker.can_retry(1)

        assert can_retry is True
        assert reason is None

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_can_retry_permanent_failure(self, mock_base, mock_db_manager):
        """Test can_retry for permanently failed resource"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Mock status record with permanent failure
        mock_status = MagicMock()
        mock_status.status = "permanently_failed"
        mock_status.failure_message = "File not found"
        mock_status.failure_type = "not_found"

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            can_retry, reason = tracker.can_retry(1)

        assert can_retry is False
        assert "Permanently failed" in reason

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_can_retry_cooldown_active(self, mock_base, mock_db_manager):
        """Test can_retry with active cooldown"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Mock status record with temporary failure and future retry time
        future_time = datetime.now(UTC) + timedelta(hours=2)
        mock_status = MagicMock()
        mock_status.status = "temporarily_failed"
        mock_status.retry_after_timestamp = future_time
        mock_status.today_retry_count = 0

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            can_retry, reason = tracker.can_retry(1)

        assert can_retry is False
        assert "Cooldown active" in reason

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_can_retry_daily_limit_exceeded(self, mock_base, mock_db_manager):
        """Test can_retry when daily limit is exceeded"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Mock status with exceeded daily limit
        mock_status = MagicMock()
        mock_status.status = "available"
        mock_status.retry_after_timestamp = None
        mock_status.today_retry_count = 3
        mock_status.total_retry_count = 3
        mock_status.last_attempt_at = datetime.now(UTC)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            can_retry, reason = tracker.can_retry(1)

        assert can_retry is False
        assert "Daily retry limit exceeded" in reason

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_can_retry_retry_count_exceeded(self, mock_base, mock_db_manager):
        """Test can_retry for exceeded retry count"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Mock status with exceeded daily count
        mock_status = MagicMock()
        mock_status.status = "temporarily_failed"
        mock_status.retry_after_timestamp = datetime.now(UTC) - timedelta(
            hours=1
        )  # Cooldown expired
        mock_status.today_retry_count = 4  # Exceeded limit
        mock_status.total_retry_count = 4
        mock_status.last_attempt_at = datetime.now(UTC)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            can_retry, reason = tracker.can_retry(1)

        assert can_retry is False
        assert "Daily retry limit" in reason

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_mark_failure_temporary(self, mock_base, mock_db_manager):
        """Test marking resource as temporarily failed"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure(
            "timeout", "Request timed out", timedelta(hours=1)
        )

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        tracker.mark_failure(1, failure, session=mock_session)

        # Should have added a new record
        mock_session.add.assert_called_once()
        added_record = mock_session.add.call_args[0][0]
        assert added_record.resource_id == 1

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_mark_failure_permanent(self, mock_base, mock_db_manager):
        """Test marking resource as permanently failed"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = PermanentFailure("not_found", "Resource not found")

        mock_status = MagicMock()
        mock_status.total_retry_count = 1
        mock_status.today_retry_count = 0
        mock_status.last_attempt_at = datetime.now(UTC)

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        tracker.mark_failure(1, failure, session=mock_session)

        assert mock_status.status == "permanently_failed"
        assert mock_status.failure_type == "not_found"
        assert mock_status.retry_after_timestamp is None

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_mark_success_clears_status(self, mock_base, mock_db_manager):
        """Test marking resource as successful clears failure status"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        mock_status = MagicMock()
        mock_status.status = "temporarily_failed"
        mock_status.failure_type = "timeout"
        mock_status.failure_message = "Timeout"
        mock_status.retry_after_timestamp = datetime.now(UTC) + timedelta(
            hours=1
        )

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        tracker.mark_success(1, session=mock_session)

        assert mock_status.status == "completed"
        assert mock_status.failure_type is None
        assert mock_status.failure_message is None
        assert mock_status.retry_after_timestamp is None

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_get_resource_status_details(self, mock_base, mock_db_manager):
        """Test getting resource status details"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        now = datetime.now(UTC)
        mock_status = MagicMock()
        mock_status.resource_id = 1
        mock_status.status = "temporarily_failed"
        mock_status.failure_type = "timeout"
        mock_status.failure_message = "Request timed out"
        mock_status.retry_after_timestamp = now + timedelta(hours=1)
        mock_status.last_attempt_at = now
        mock_status.total_retry_count = 2
        mock_status.today_retry_count = 1
        mock_status.created_at = now - timedelta(days=1)
        mock_status.updated_at = now

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            status = tracker.get_resource_status(1)

        assert status is not None
        assert status["resource_id"] == 1
        assert status["status"] == "temporarily_failed"
        assert status["failure_type"] == "timeout"
        assert status["total_retry_count"] == 2

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_status_to_dict_transformation(self, mock_base, mock_db_manager):
        """Test status dict transformation"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        now = datetime.now(UTC)
        mock_status = MagicMock()
        mock_status.resource_id = 1
        mock_status.status = "available"
        mock_status.failure_type = None
        mock_status.failure_message = None
        mock_status.retry_after_timestamp = None
        mock_status.last_attempt_at = None
        mock_status.total_retry_count = 0
        mock_status.today_retry_count = 0
        mock_status.created_at = now
        mock_status.updated_at = now

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            status = tracker.get_resource_status(1)

        assert status["retry_after_timestamp"] is None
        assert status["last_attempt_at"] is None

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_naive_timestamp_handling(self, mock_base, mock_db_manager):
        """Test handling of naive (timezone-unaware) timestamps"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Create a naive timestamp (without timezone)
        naive_time = datetime.now()  # No timezone - already naive

        mock_status = MagicMock()
        mock_status.status = "temporarily_failed"
        mock_status.retry_after_timestamp = naive_time
        mock_status.today_retry_count = 0
        mock_status.total_retry_count = 0
        mock_status.last_attempt_at = datetime.now()

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            # Should handle naive timestamp without error
            can_retry, reason = tracker.can_retry(1)

        # The method should complete without error
        assert isinstance(can_retry, bool)

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_timezone_aware_timestamp(self, mock_base, mock_db_manager):
        """Test handling of timezone-aware timestamps"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Create a timezone-aware timestamp
        aware_time = datetime.now(UTC) + timedelta(hours=2)

        mock_status = MagicMock()
        mock_status.status = "temporarily_failed"
        mock_status.retry_after_timestamp = aware_time
        mock_status.today_retry_count = 0

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            can_retry, reason = tracker.can_retry(1)

        assert can_retry is False
        assert "Cooldown active" in reason

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_daily_limit_boundary(self, mock_base, mock_db_manager):
        """Test behavior at daily limit boundary (exactly 3 retries)"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Exactly at limit
        mock_status = MagicMock()
        mock_status.status = "available"
        mock_status.retry_after_timestamp = None
        mock_status.today_retry_count = 3  # At limit
        mock_status.total_retry_count = 3
        mock_status.last_attempt_at = datetime.now(UTC)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            can_retry, reason = tracker.can_retry(1)

        assert can_retry is False
        assert "Daily retry limit" in reason

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_session_reuse_patterns(self, mock_base, mock_db_manager):
        """Test session reuse when provided"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure("error", "Error", timedelta(hours=1))

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        # Pass session explicitly
        tracker.mark_failure(1, failure, session=mock_session)

        # Should not commit or close when session is provided
        mock_session.commit.assert_not_called()

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_failed_resource_count(self, mock_base, mock_db_manager):
        """Test getting failed resource counts"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Mock failed resources
        mock_resources = [
            MagicMock(failure_type="not_found"),
            MagicMock(failure_type="not_found"),
            MagicMock(failure_type="timeout"),
            MagicMock(failure_type=None),  # Unknown
        ]

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter.return_value.all.return_value = (
            mock_resources
        )

        with patch.object(tracker, "_get_session", return_value=mock_session):
            counts = tracker.get_failed_resources_count()

        assert counts.get("not_found", 0) == 2
        assert counts.get("timeout", 0) == 1
        assert counts.get("unknown", 0) == 1


class TestCleanupOperations:
    """Tests for cleanup operations"""

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_clear_permanent_failures(self, mock_base, mock_db_manager):
        """Test clearing old permanent failures"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Mock old failures
        old_failures = [
            MagicMock(),
            MagicMock(),
            MagicMock(),
        ]

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter.return_value.all.return_value = (
            old_failures
        )

        with patch.object(tracker, "_get_session", return_value=mock_session):
            count = tracker.clear_permanent_failures(30)

        assert count == 3
        # Check that each failure was reset
        for failure in old_failures:
            assert failure.status == "available"
            assert failure.failure_type is None

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_clear_old_cooldowns(self, mock_base, mock_db_manager):
        """Test clearing old expired cooldowns"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # No specific method for this - clear_permanent_failures handles it
        # This is a conceptual test
        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter.return_value.all.return_value = []

        with patch.object(tracker, "_get_session", return_value=mock_session):
            count = tracker.clear_permanent_failures(30)

        assert count == 0

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_reset_all_statuses(self, mock_base, mock_db_manager):
        """Test resetting all status fields on clear"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        mock_failure = MagicMock()
        mock_failure.status = "permanently_failed"
        mock_failure.failure_type = "not_found"
        mock_failure.failure_message = "Not found"
        mock_failure.retry_after_timestamp = datetime.now(UTC)
        mock_failure.permanent_failure_at = datetime.now(UTC)
        mock_failure.total_retry_count = 5
        mock_failure.today_retry_count = 2

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter.return_value.all.return_value = [
            mock_failure
        ]

        with patch.object(tracker, "_get_session", return_value=mock_session):
            count = tracker.clear_permanent_failures(30)

        assert count == 1
        assert mock_failure.status == "available"
        assert mock_failure.failure_type is None
        assert mock_failure.failure_message is None
        assert mock_failure.retry_after_timestamp is None
        assert mock_failure.permanent_failure_at is None
        assert mock_failure.total_retry_count == 0
        assert mock_failure.today_retry_count == 0

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_batch_status_update(self, mock_base, mock_db_manager):
        """Test batch status update"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failures = [MagicMock() for _ in range(5)]

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter.return_value.all.return_value = (
            failures
        )

        with patch.object(tracker, "_get_session", return_value=mock_session):
            count = tracker.clear_permanent_failures(30)

        assert count == 5
        mock_session.commit.assert_called_once()

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_status_persistence(self, mock_base, mock_db_manager):
        """Test that status changes are persisted"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure("error", "Error", timedelta(hours=1))

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        # Without explicit session, should create and commit
        with patch.object(tracker, "Session", return_value=mock_session):
            with patch.object(
                tracker, "_get_session", return_value=mock_session
            ):
                # Session provided - no commit
                tracker.mark_failure(1, failure, session=mock_session)
                mock_session.commit.assert_not_called()

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_database_error_handling(self, mock_base, mock_db_manager):
        """Test handling of database errors"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure("error", "Error", timedelta(hours=1))

        mock_session = MagicMock()
        mock_session.query.side_effect = Exception("Database error")

        # Should not crash, but may raise or log
        with pytest.raises(Exception):
            tracker.mark_failure(1, failure, session=mock_session)

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_concurrent_status_updates(self, mock_base, mock_db_manager):
        """Test concurrent status updates"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure("error", "Error", timedelta(hours=1))

        mock_status = MagicMock()
        mock_status.total_retry_count = 0
        mock_status.today_retry_count = 0
        mock_status.last_attempt_at = None

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        # Simulate concurrent updates
        def update_status():
            tracker.mark_failure(1, failure, session=mock_session)

        threads = [threading.Thread(target=update_status) for _ in range(5)]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

        # All updates should complete
        assert mock_session.query.call_count >= 5

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_status_cache_invalidation(self, mock_base, mock_db_manager):
        """Test that status is fetched fresh from database"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Each call should query the database
        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        with patch.object(tracker, "_get_session", return_value=mock_session):
            tracker.can_retry(1)
            tracker.can_retry(1)
            tracker.can_retry(1)

        # Should query 3 times (no caching)
        assert mock_session.query.call_count == 3

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_expired_cooldown_cleanup(self, mock_base, mock_db_manager):
        """Test that expired cooldowns allow retry"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Cooldown expired 1 hour ago
        expired_time = datetime.now(UTC) - timedelta(hours=1)

        mock_status = MagicMock()
        mock_status.status = "temporarily_failed"
        mock_status.retry_after_timestamp = expired_time
        mock_status.today_retry_count = 0
        mock_status.total_retry_count = 0
        mock_status.last_attempt_at = datetime.now(UTC) - timedelta(hours=2)

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        with patch.object(tracker, "_get_session", return_value=mock_session):
            can_retry, reason = tracker.can_retry(1)

        assert can_retry is True
        assert reason is None

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_orphan_status_removal(self, mock_base, mock_db_manager):
        """Test clearing orphaned status records"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        # Old orphaned records
        old_records = [MagicMock() for _ in range(3)]

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter.return_value.all.return_value = (
            old_records
        )

        with patch.object(tracker, "_get_session", return_value=mock_session):
            count = tracker.clear_permanent_failures(older_than_days=30)

        assert count == 3


class TestInitialization:
    """Tests for tracker initialization"""

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_initialization_with_password(self, mock_base, mock_db_manager):
        """Test initialization with password"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user", password="test_password")

        mock_db_manager.open_user_database.assert_called_once_with(
            "test_user", "test_password"
        )
        assert tracker.username == "test_user"
        assert tracker.password == "test_password"

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_initialization_without_password(self, mock_base, mock_db_manager):
        """Test initialization without password"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        mock_db_manager.open_user_database.assert_called_once_with(
            "test_user", None
        )
        assert tracker.password is None

    @patch("local_deep_research.database.encrypted_db.db_manager")
    def test_tables_created(self, mock_db_manager):
        """Test that tables are created on initialization"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        # Patch Base at the actual module import location
        with patch(
            "local_deep_research.library.download_management.status_tracker.Base"
        ) as mock_base:
            tracker = ResourceStatusTracker("test_user")
            assert tracker.username == "test_user"
            mock_base.metadata.create_all.assert_called_once_with(mock_engine)


class TestRetryCounterLogic:
    """Tests for retry counter logic"""

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_total_retry_count_increment(self, mock_base, mock_db_manager):
        """Test total retry count is incremented"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure("error", "Error", timedelta(hours=1))

        mock_status = MagicMock()
        mock_status.total_retry_count = 5
        mock_status.today_retry_count = 1
        mock_status.last_attempt_at = datetime.now(UTC)

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        tracker.mark_failure(1, failure, session=mock_session)

        assert mock_status.total_retry_count == 6

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_today_retry_count_increment(self, mock_base, mock_db_manager):
        """Test today retry count is incremented for same-day attempts"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure("error", "Error", timedelta(hours=1))

        mock_status = MagicMock()
        mock_status.total_retry_count = 5
        mock_status.today_retry_count = 1
        mock_status.last_attempt_at = datetime.now(UTC)

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        tracker.mark_failure(1, failure, session=mock_session)

        assert mock_status.today_retry_count == 2

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_today_retry_count_reset_on_new_day(
        self, mock_base, mock_db_manager
    ):
        """Test today retry count resets on new day"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure("error", "Error", timedelta(hours=1))

        # Last attempt was yesterday
        yesterday = datetime.now(UTC) - timedelta(days=1)
        mock_status = MagicMock()
        # Use spec to limit auto-creation of attributes
        mock_status.total_retry_count = 5
        mock_status.today_retry_count = 3
        mock_status.last_attempt_at = yesterday

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        tracker.mark_failure(1, failure, session=mock_session)

        # Should reset based on day check then increment
        # The actual implementation may increment after reset, so check >= 1
        assert mock_status.today_retry_count >= 1

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_none_retry_count_handling(self, mock_base, mock_db_manager):
        """Test handling of None retry counts (legacy data)"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        failure = TemporaryFailure("error", "Error", timedelta(hours=1))

        # Legacy data with None counts
        mock_status = MagicMock()
        mock_status.total_retry_count = None
        mock_status.today_retry_count = None
        mock_status.last_attempt_at = datetime.now(UTC)

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = mock_status

        tracker.mark_failure(1, failure, session=mock_session)

        # Should handle None and set to 1
        assert mock_status.total_retry_count == 1
        assert mock_status.today_retry_count == 1


class TestResourceNotFound:
    """Tests for handling non-existent resources"""

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_get_status_not_found(self, mock_base, mock_db_manager):
        """Test getting status for non-existent resource"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        mock_session = MagicMock()
        mock_session.__enter__ = MagicMock(return_value=mock_session)
        mock_session.__exit__ = MagicMock(return_value=False)
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        with patch.object(tracker, "_get_session", return_value=mock_session):
            status = tracker.get_resource_status(999)

        assert status is None

    @patch("local_deep_research.database.encrypted_db.db_manager")
    @patch("local_deep_research.library.download_management.models.Base")
    def test_mark_success_not_found(self, mock_base, mock_db_manager):
        """Test marking success for non-existent resource"""
        mock_engine = MagicMock()
        mock_db_manager.open_user_database.return_value = mock_engine

        tracker = ResourceStatusTracker("test_user")

        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None

        # Should not crash for non-existent resource
        tracker.mark_success(999, session=mock_session)

        # No changes should be made
        mock_session.add.assert_not_called()
