"""Tests for the job priority system (OCLAWMA-3).

This module tests:
- Priority levels (CRITICAL, HIGH, NORMAL, LOW)
- Priority-based dequeue ordering
- Preemption of lower priority running jobs by CRITICAL jobs
- Priority inheritance for dependent jobs
- Queue reordering on priority change
"""

from __future__ import annotations

import threading
import time

import pytest

from oclawma.queue import Job, JobQueue, JobStatus
from oclawma.queue.models import JobPriority
from oclawma.queue.queue import JobQueueError
from oclawma.queue.store import JobNotFoundError


class TestJobPriorityEnum:
    """Test JobPriority enumeration."""

    def test_priority_values(self):
        """Test priority enum values are correct."""
        assert JobPriority.CRITICAL.value == 100
        assert JobPriority.HIGH.value == 50
        assert JobPriority.NORMAL.value == 0
        assert JobPriority.LOW.value == -50

    def test_priority_ordering(self):
        """Test that priorities are ordered correctly."""
        assert JobPriority.CRITICAL > JobPriority.HIGH
        assert JobPriority.HIGH > JobPriority.NORMAL
        assert JobPriority.NORMAL > JobPriority.LOW

    def test_priority_comparison(self):
        """Test priority comparisons work."""
        assert JobPriority.CRITICAL == JobPriority.CRITICAL
        assert JobPriority.CRITICAL != JobPriority.HIGH
        assert JobPriority.HIGH >= JobPriority.NORMAL
        assert JobPriority.LOW <= JobPriority.NORMAL


class TestJobModelWithPriority:
    """Test Job model with priority enum."""

    def test_default_priority(self):
        """Test default priority is NORMAL."""
        job = Job(payload={"task": "test"})
        assert job.priority == JobPriority.NORMAL
        assert job.priority.value == 0

    def test_explicit_priority(self):
        """Test setting explicit priority."""
        job = Job(payload={"task": "test"}, priority=JobPriority.HIGH)
        assert job.priority == JobPriority.HIGH

    def test_priority_from_int(self):
        """Test creating job with integer priority."""
        job = Job(payload={"task": "test"}, priority=100)
        assert job.priority == JobPriority.CRITICAL

    def test_priority_serialization(self):
        """Test priority serializes correctly to DB."""
        job = Job(payload={"task": "test"}, priority=JobPriority.HIGH)
        db_dict = job.to_db_dict()
        assert db_dict["priority"] == 50

    def test_priority_deserialization(self):
        """Test priority deserializes correctly from DB."""
        job = Job(id=1, payload={"task": "test"}, priority=JobPriority.CRITICAL)
        db_dict = job.to_db_dict()
        restored = Job.from_db_row(db_dict)
        assert restored.priority == JobPriority.CRITICAL

    def test_job_with_dependencies(self):
        """Test job with dependencies."""
        job = Job(payload={"task": "test"}, depends_on=[1, 2, 3])
        assert job.depends_on == [1, 2, 3]

    def test_job_default_no_dependencies(self):
        """Test job has no dependencies by default."""
        job = Job(payload={"task": "test"})
        assert job.depends_on is None


class TestPriorityEnqueue:
    """Test enqueue with priority levels."""

    @pytest.fixture
    def queue(self, tmp_path):
        """Create a temporary queue for testing."""
        db_path = tmp_path / "test_queue.db"
        with JobQueue(db_path) as q:
            yield q

    def test_enqueue_with_critical_priority(self, queue):
        """Test enqueuing with CRITICAL priority."""
        job = queue.enqueue({"task": "critical"}, priority=JobPriority.CRITICAL)
        assert job.priority == JobPriority.CRITICAL

    def test_enqueue_with_high_priority(self, queue):
        """Test enqueuing with HIGH priority."""
        job = queue.enqueue({"task": "high"}, priority=JobPriority.HIGH)
        assert job.priority == JobPriority.HIGH

    def test_enqueue_with_normal_priority(self, queue):
        """Test enqueuing with NORMAL priority."""
        job = queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)
        assert job.priority == JobPriority.NORMAL

    def test_enqueue_with_low_priority(self, queue):
        """Test enqueuing with LOW priority."""
        job = queue.enqueue({"task": "low"}, priority=JobPriority.LOW)
        assert job.priority == JobPriority.LOW

    def test_enqueue_default_priority(self, queue):
        """Test enqueuing with default priority."""
        job = queue.enqueue({"task": "default"})
        assert job.priority == JobPriority.NORMAL

    def test_enqueue_with_priority_int(self, queue):
        """Test enqueuing with integer priority value."""
        job = queue.enqueue({"task": "high"}, priority=50)
        assert job.priority == JobPriority.HIGH


class TestPriorityDequeue:
    """Test priority-based dequeue ordering."""

    @pytest.fixture
    def queue(self, tmp_path):
        """Create a temporary queue for testing."""
        db_path = tmp_path / "test_queue.db"
        with JobQueue(db_path) as q:
            yield q

    def test_dequeue_priority_order_all_levels(self, queue):
        """Test that jobs are dequeued in priority order."""
        # Enqueue in random order
        queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)
        queue.enqueue({"task": "low"}, priority=JobPriority.LOW)
        queue.enqueue({"task": "critical"}, priority=JobPriority.CRITICAL)
        queue.enqueue({"task": "high"}, priority=JobPriority.HIGH)

        # Should come out in priority order
        job1 = queue.dequeue()
        job2 = queue.dequeue()
        job3 = queue.dequeue()
        job4 = queue.dequeue()

        assert job1.payload == {"task": "critical"}
        assert job2.payload == {"task": "high"}
        assert job3.payload == {"task": "normal"}
        assert job4.payload == {"task": "low"}

    def test_dequeue_same_priority_fifo(self, queue):
        """Test FIFO within same priority level."""
        queue.enqueue({"task": "first"}, priority=JobPriority.HIGH)
        time.sleep(0.01)
        queue.enqueue({"task": "second"}, priority=JobPriority.HIGH)
        time.sleep(0.01)
        queue.enqueue({"task": "third"}, priority=JobPriority.HIGH)

        job1 = queue.dequeue()
        job2 = queue.dequeue()
        job3 = queue.dequeue()

        assert job1.payload == {"task": "first"}
        assert job2.payload == {"task": "second"}
        assert job3.payload == {"task": "third"}

    def test_dequeue_mixed_priority_and_time(self, queue):
        """Test ordering with mixed priorities and creation times."""
        # Enqueue low priority first
        queue.enqueue({"task": "low_first"}, priority=JobPriority.LOW)
        time.sleep(0.01)
        # Then high priority (should still come first)
        queue.enqueue({"task": "high_later"}, priority=JobPriority.HIGH)

        job1 = queue.dequeue()
        job2 = queue.dequeue()

        assert job1.payload == {"task": "high_later"}
        assert job2.payload == {"task": "low_first"}


class TestPreemption:
    """Test CRITICAL job preemption of lower priority running jobs."""

    @pytest.fixture
    def queue(self, tmp_path):
        """Create a temporary queue for testing."""
        db_path = tmp_path / "test_queue.db"
        with JobQueue(db_path) as q:
            yield q

    def test_critical_job_triggers_preemption(self, queue):
        """Test that enqueuing CRITICAL job triggers preemption check."""
        # Start a normal priority job
        queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)
        running_job = queue.dequeue()
        assert running_job.status == JobStatus.RUNNING

        # Enqueue a CRITICAL job - this should trigger preemption
        queue.enqueue({"task": "critical"}, priority=JobPriority.CRITICAL)

        # The normal job should have been preempted
        preempted_job = queue.get_job(running_job.id)
        assert preempted_job.status == JobStatus.PAUSED

    def test_preempted_job_can_be_resumed(self, queue):
        """Test that preempted jobs can be resumed."""
        # Start and preempt a job
        queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)
        running_job = queue.dequeue()
        queue.enqueue({"task": "critical"}, priority=JobPriority.CRITICAL)

        # Resume the preempted job
        resumed = queue.resume(running_job.id)
        assert resumed.status == JobStatus.RUNNING
        assert resumed.id == running_job.id

    def test_critical_job_not_preempted_by_same_priority(self, queue):
        """Test that CRITICAL jobs don't preempt each other."""
        # Start first critical job
        queue.enqueue({"task": "critical1"}, priority=JobPriority.CRITICAL)
        job1 = queue.dequeue()
        assert job1.status == JobStatus.RUNNING

        # Enqueue another critical job
        queue.enqueue({"task": "critical2"}, priority=JobPriority.CRITICAL)

        # First job should still be running
        job1_check = queue.get_job(job1.id)
        assert job1_check.status == JobStatus.RUNNING

        # Second job should be pending
        job2 = queue.dequeue()
        assert job2.payload == {"task": "critical2"}
        assert job2.status == JobStatus.RUNNING

    def test_high_job_not_preempted_by_high(self, queue):
        """Test that HIGH jobs don't preempt each other."""
        queue.enqueue({"task": "high1"}, priority=JobPriority.HIGH)
        job1 = queue.dequeue()

        queue.enqueue({"task": "high2"}, priority=JobPriority.HIGH)

        job1_check = queue.get_job(job1.id)
        assert job1_check.status == JobStatus.RUNNING

    def test_only_critical_triggers_preemption(self, queue):
        """Test that only CRITICAL priority triggers preemption."""
        # Start a LOW job
        queue.enqueue({"task": "low"}, priority=JobPriority.LOW)
        low_job = queue.dequeue()

        # Enqueue HIGH - should NOT preempt
        queue.enqueue({"task": "high"}, priority=JobPriority.HIGH)

        low_check = queue.get_job(low_job.id)
        assert low_check.status == JobStatus.RUNNING  # Still running

    def test_get_preemptable_jobs(self, queue):
        """Test getting jobs that can be preempted."""
        # Create running jobs of different priorities
        queue.enqueue({"task": "critical"}, priority=JobPriority.CRITICAL)
        queue.enqueue({"task": "high"}, priority=JobPriority.HIGH)
        queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)
        queue.enqueue({"task": "low"}, priority=JobPriority.LOW)

        critical = queue.dequeue()
        high = queue.dequeue()
        normal = queue.dequeue()
        low = queue.dequeue()

        # Get jobs that can be preempted by a CRITICAL job
        preemptable = queue.get_preemptable_jobs(JobPriority.CRITICAL)
        preemptable_ids = {j.id for j in preemptable}

        # Should include HIGH, NORMAL, LOW but not CRITICAL
        assert critical.id not in preemptable_ids
        assert high.id in preemptable_ids
        assert normal.id in preemptable_ids
        assert low.id in preemptable_ids

    def test_get_preemptable_jobs_high_priority(self, queue):
        """Test getting jobs that can be preempted by HIGH priority."""
        # Create running jobs
        queue.enqueue({"task": "critical"}, priority=JobPriority.CRITICAL)
        queue.enqueue({"task": "high"}, priority=JobPriority.HIGH)
        queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)

        critical = queue.dequeue()
        high = queue.dequeue()
        normal = queue.dequeue()

        # Get jobs that can be preempted by a HIGH job
        preemptable = queue.get_preemptable_jobs(JobPriority.HIGH)
        preemptable_ids = {j.id for j in preemptable}

        # HIGH can only preempt NORMAL and LOW (not CRITICAL or other HIGH)
        assert critical.id not in preemptable_ids
        assert high.id not in preemptable_ids
        assert normal.id in preemptable_ids


class TestPriorityInheritance:
    """Test priority inheritance for dependent jobs."""

    @pytest.fixture
    def queue(self, tmp_path):
        """Create a temporary queue for testing."""
        db_path = tmp_path / "test_queue.db"
        with JobQueue(db_path) as q:
            yield q

    def test_enqueue_with_dependencies(self, queue):
        """Test enqueuing job with dependencies."""
        # Create parent job
        parent = queue.enqueue({"task": "parent"}, priority=JobPriority.NORMAL)

        # Create child job that depends on parent
        child = queue.enqueue({"task": "child"}, priority=JobPriority.LOW, depends_on=[parent.id])

        assert child.depends_on == [parent.id]

    def test_dependency_resolution_order(self, queue):
        """Test that dependencies affect dequeue order."""
        # Create parent job
        parent = queue.enqueue({"task": "parent"}, priority=JobPriority.LOW)

        # Create child with higher priority but dependency
        child = queue.enqueue({"task": "child"}, priority=JobPriority.HIGH, depends_on=[parent.id])

        # Parent should be dequeued first (dependency)
        job1 = queue.dequeue()
        assert job1.id == parent.id

        # Complete parent
        queue.complete(parent.id)

        # Now child can run
        job2 = queue.dequeue()
        assert job2.id == child.id

    def test_dependent_job_blocked_until_dependency_complete(self, queue):
        """Test dependent job is blocked until dependency completes."""
        parent = queue.enqueue({"task": "parent"}, priority=JobPriority.LOW)
        queue.enqueue({"task": "child"}, priority=JobPriority.HIGH, depends_on=[parent.id])

        # Get parent
        job1 = queue.dequeue()
        assert job1.id == parent.id

        # Try to get next - should be None because child is blocked
        job2 = queue.dequeue()
        assert job2 is None

        # Fail parent - child should still be blocked
        queue.fail(parent.id, "Parent failed")
        job3 = queue.dequeue()
        assert job3 is None

    def test_get_blocked_jobs(self, queue):
        """Test getting jobs that are blocked by dependencies."""
        parent = queue.enqueue({"task": "parent"})
        child1 = queue.enqueue({"task": "child1"}, depends_on=[parent.id])
        child2 = queue.enqueue({"task": "child2"}, depends_on=[parent.id])
        independent = queue.enqueue({"task": "independent"})

        blocked = queue.get_blocked_jobs()
        blocked_ids = {j.id for j in blocked}

        assert parent.id not in blocked_ids
        assert child1.id in blocked_ids
        assert child2.id in blocked_ids
        assert independent.id not in blocked_ids

    def test_unblocked_after_dependency_complete(self, queue):
        """Test job becomes unblocked after dependency completes."""
        parent = queue.enqueue({"task": "parent"})
        child = queue.enqueue({"task": "child"}, depends_on=[parent.id])

        # Initially blocked
        assert len(queue.get_blocked_jobs()) == 1

        # Complete parent
        job = queue.dequeue()
        queue.complete(job.id)

        # Now unblocked
        assert len(queue.get_blocked_jobs()) == 0

        # Can dequeue child
        child_job = queue.dequeue()
        assert child_job.id == child.id


class TestPriorityChange:
    """Test changing job priority and queue reordering."""

    @pytest.fixture
    def queue(self, tmp_path):
        """Create a temporary queue for testing."""
        db_path = tmp_path / "test_queue.db"
        with JobQueue(db_path) as q:
            yield q

    def test_change_priority(self, queue):
        """Test changing job priority."""
        job = queue.enqueue({"task": "test"}, priority=JobPriority.LOW)

        updated = queue.change_priority(job.id, JobPriority.HIGH)

        assert updated.priority == JobPriority.HIGH

    def test_priority_change_reorders_queue(self, queue):
        """Test that changing priority reorders the queue."""
        low = queue.enqueue({"task": "low"}, priority=JobPriority.LOW)
        normal = queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)

        # Change low to CRITICAL
        queue.change_priority(low.id, JobPriority.CRITICAL)

        # Should now come out first
        job1 = queue.dequeue()
        assert job1.id == low.id

        job2 = queue.dequeue()
        assert job2.id == normal.id

    def test_change_priority_only_pending(self, queue):
        """Test that priority can only be changed for pending jobs."""
        job = queue.enqueue({"task": "test"}, priority=JobPriority.LOW)

        # Start the job
        queue.dequeue()

        # Should not be able to change priority of running job
        with pytest.raises(JobQueueError, match="Cannot change priority"):
            queue.change_priority(job.id, JobPriority.HIGH)

    def test_change_priority_not_found(self, queue):
        """Test changing priority of non-existent job."""
        with pytest.raises(JobNotFoundError):
            queue.change_priority(999, JobPriority.HIGH)

    def test_priority_inheritance_on_enqueue(self, queue):
        """Test that jobs inherit priority from dependencies."""
        # Create high priority parent
        parent = queue.enqueue({"task": "parent"}, priority=JobPriority.HIGH)

        # Create child with low priority - should inherit from parent
        child = queue.enqueue({"task": "child"}, priority=JobPriority.LOW, depends_on=[parent.id])

        # Child should have inherited HIGH priority
        assert child.priority == JobPriority.HIGH

    def test_priority_inheritance_max_value(self, queue):
        """Test inheritance takes max priority from all dependencies."""
        # Create multiple dependencies with different priorities
        low = queue.enqueue({"task": "low"}, priority=JobPriority.LOW)
        high = queue.enqueue({"task": "high"}, priority=JobPriority.HIGH)
        normal = queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)

        # Child should inherit HIGH (max of dependencies)
        child = queue.enqueue(
            {"task": "child"}, priority=JobPriority.LOW, depends_on=[low.id, high.id, normal.id]
        )

        assert child.priority == JobPriority.HIGH


class TestPausedStatus:
    """Test PAUSED job status."""

    def test_paused_status_exists(self):
        """Test that PAUSED status exists in JobStatus."""
        assert hasattr(JobStatus, "PAUSED")
        assert JobStatus.PAUSED.value == "paused"

    def test_job_can_be_paused(self):
        """Test creating job with PAUSED status."""
        job = Job(payload={"task": "test"}, status=JobStatus.PAUSED)
        assert job.status == JobStatus.PAUSED


class TestPriorityStats:
    """Test priority-related statistics."""

    @pytest.fixture
    def queue(self, tmp_path):
        """Create a temporary queue for testing."""
        db_path = tmp_path / "test_queue.db"
        with JobQueue(db_path) as q:
            yield q

    def test_stats_by_priority(self, queue):
        """Test getting stats grouped by priority."""
        queue.enqueue({"task": "critical"}, priority=JobPriority.CRITICAL)
        queue.enqueue({"task": "high1"}, priority=JobPriority.HIGH)
        queue.enqueue({"task": "high2"}, priority=JobPriority.HIGH)
        queue.enqueue({"task": "normal"}, priority=JobPriority.NORMAL)

        stats = queue.get_stats_by_priority()

        assert stats[JobPriority.CRITICAL] == 1
        assert stats[JobPriority.HIGH] == 2
        assert stats[JobPriority.NORMAL] == 1
        assert stats[JobPriority.LOW] == 0


class TestPriorityIntegration:
    """Integration tests for priority system."""

    @pytest.fixture
    def queue(self, tmp_path):
        """Create a temporary queue for testing."""
        db_path = tmp_path / "test_queue.db"
        with JobQueue(db_path) as q:
            yield q

    def test_full_priority_workflow(self, queue):
        """Test complete priority workflow with preemption and inheritance."""
        # 1. Enqueue jobs with different priorities
        low = queue.enqueue({"task": "background"}, priority=JobPriority.LOW)
        normal = queue.enqueue({"task": "standard"}, priority=JobPriority.NORMAL)

        # 2. Start the normal job
        running_normal = queue.dequeue()
        assert running_normal.id == normal.id
        assert running_normal.status == JobStatus.RUNNING

        # 3. Enqueue a CRITICAL job - should preempt the normal job
        critical = queue.enqueue({"task": "urgent"}, priority=JobPriority.CRITICAL)

        # Normal job should be paused
        paused = queue.get_job(normal.id)
        assert paused.status == JobStatus.PAUSED

        # 4. CRITICAL job should be dequeued next
        urgent_job = queue.dequeue()
        assert urgent_job.id == critical.id

        # 5. Complete critical job
        queue.complete(critical.id)

        # 6. Resume paused job
        resumed = queue.resume(normal.id)
        assert resumed.status == JobStatus.RUNNING

        # 7. Complete normal job
        queue.complete(normal.id)

        # 8. Finally process low priority job
        low_job = queue.dequeue()
        assert low_job.id == low.id
        queue.complete(low.id)

        # Verify all completed
        stats = queue.get_stats()
        assert stats.completed == 3

    def test_priority_with_dependencies_and_preemption(self, queue):
        """Test complex scenario with priorities and dependencies."""
        # Create a dependency chain
        base = queue.enqueue({"task": "base"}, priority=JobPriority.NORMAL)

        # Child with LOW priority - should inherit NORMAL from parent
        child = queue.enqueue({"task": "child"}, priority=JobPriority.LOW, depends_on=[base.id])
        assert child.priority == JobPriority.NORMAL  # Inherited

        # Grandchild with explicit CRITICAL - keeps CRITICAL
        grandchild = queue.enqueue(
            {"task": "grandchild"}, priority=JobPriority.CRITICAL, depends_on=[child.id]
        )
        assert grandchild.priority == JobPriority.CRITICAL

        # Process in dependency order, respecting priorities at each level
        job1 = queue.dequeue()
        assert job1.id == base.id
        queue.complete(base.id)

        # Child is now unblocked but grandchild has CRITICAL priority
        # However grandchild is still blocked by child
        job2 = queue.dequeue()
        assert job2.id == child.id
        queue.complete(child.id)

        job3 = queue.dequeue()
        assert job3.id == grandchild.id

    def test_concurrent_priority_access(self, queue):
        """Test thread-safe priority operations."""
        results = []
        errors = []

        def enqueue_priority_task(priority, n):
            try:
                job = queue.enqueue({"task": n}, priority=priority)
                results.append((priority, job.id))
            except Exception as e:
                errors.append(e)

        # Create threads that enqueue with different priorities
        threads = []
        for i in range(20):
            priority = [
                JobPriority.CRITICAL,
                JobPriority.HIGH,
                JobPriority.NORMAL,
                JobPriority.LOW,
            ][i % 4]
            t = threading.Thread(target=enqueue_priority_task, args=(priority, i))
            threads.append(t)

        for t in threads:
            t.start()
        for t in threads:
            t.join()

        assert len(errors) == 0
        assert len(results) == 20

        # Verify dequeue order respects priority
        priorities = []
        for _ in range(20):
            job = queue.dequeue()
            if job:
                priorities.append(job.priority)

        # Should be in descending priority order
        for i in range(len(priorities) - 1):
            assert priorities[i] >= priorities[i + 1]
