"""Tests for the batch job module."""

from __future__ import annotations

import json

import pytest

from oclawma.queue import (
    BatchError,
    BatchJob,
    BatchManager,
    BatchNotFoundError,
    BatchProgress,
    BatchResult,
    BatchStatus,
    JobPriority,
    JobQueue,
    JobStatus,
)


class TestBatchJobModel:
    """Test BatchJob model."""

    def test_default_creation(self):
        """Test creating a batch job with defaults."""
        job = BatchJob(payload={"task": "test"})

        assert job.id is None
        assert job.payload == {"task": "test"}
        assert job.status == JobStatus.PENDING
        assert job.batch_id is None
        assert job.batch_config == {}
        assert job.batch_order == 0

    def test_batch_creation(self):
        """Test creating a batch job with batch fields."""
        job = BatchJob(
            payload={"task": "test"},
            batch_id="batch-123",
            batch_config={"template": "welcome"},
            batch_order=5,
            batch_result={"output": "success"},
        )

        assert job.batch_id == "batch-123"
        assert job.batch_config == {"template": "welcome"}
        assert job.batch_order == 5
        assert job.batch_result == {"output": "success"}

    def test_to_db_dict(self):
        """Test serialization to database dictionary."""
        job = BatchJob(
            id=1,
            payload={"task": "test"},
            batch_id="batch-123",
            batch_config={"shared": "config"},
            batch_order=3,
            status=JobStatus.PENDING,
            priority=JobPriority.HIGH,
        )

        db_dict = job.to_db_dict()

        assert db_dict["id"] == 1
        payload = json.loads(db_dict["payload"])
        assert "__batch__" in payload
        assert payload["__batch__"]["batch_id"] == "batch-123"
        assert payload["__batch__"]["batch_config"] == {"shared": "config"}
        assert payload["__batch__"]["batch_order"] == 3

    def test_from_job(self):
        """Test creating BatchJob from regular Job."""
        from oclawma.queue.models import Job as BaseJob

        base_job = BaseJob(
            id=1,
            payload={
                "task": "test",
                "__batch__": {
                    "batch_id": "batch-123",
                    "batch_config": {"template": "welcome"},
                    "batch_order": 2,
                    "batch_result": None,
                },
            },
            status=JobStatus.COMPLETED,
        )

        batch_job = BatchJob.from_job(base_job)

        assert batch_job.id == 1
        assert batch_job.payload == {"task": "test"}
        assert batch_job.batch_id == "batch-123"
        assert batch_job.batch_config == {"template": "welcome"}
        assert batch_job.batch_order == 2
        assert batch_job.status == JobStatus.COMPLETED


class TestBatchProgress:
    """Test BatchProgress dataclass."""

    def test_all_pending(self):
        """Test progress with all pending jobs."""
        progress = BatchProgress(
            batch_id="batch-123",
            total=10,
            pending=10,
            running=0,
            completed=0,
            failed=0,
            cancelled=0,
        )

        assert progress.status == BatchStatus.PENDING
        assert progress.percent_complete == 0.0

    def test_all_completed(self):
        """Test progress with all completed jobs."""
        progress = BatchProgress(
            batch_id="batch-123",
            total=10,
            pending=0,
            running=0,
            completed=10,
            failed=0,
            cancelled=0,
        )

        assert progress.status == BatchStatus.COMPLETED
        assert progress.percent_complete == 100.0

    def test_all_failed(self):
        """Test progress with all failed jobs."""
        progress = BatchProgress(
            batch_id="batch-123",
            total=10,
            pending=0,
            running=0,
            completed=0,
            failed=10,
            cancelled=0,
        )

        assert progress.status == BatchStatus.FAILED
        assert progress.percent_complete == 100.0

    def test_partial_failure(self):
        """Test progress with partial failure."""
        progress = BatchProgress(
            batch_id="batch-123",
            total=10,
            pending=0,
            running=0,
            completed=5,
            failed=3,
            cancelled=2,
        )

        assert progress.status == BatchStatus.PARTIAL
        assert progress.percent_complete == 100.0

    def test_running(self):
        """Test progress with running jobs."""
        progress = BatchProgress(
            batch_id="batch-123",
            total=10,
            pending=5,
            running=5,
            completed=0,
            failed=0,
            cancelled=0,
        )

        assert progress.status == BatchStatus.RUNNING
        assert progress.percent_complete == 0.0

    def test_cancelled(self):
        """Test progress with all cancelled jobs."""
        progress = BatchProgress(
            batch_id="batch-123",
            total=10,
            pending=0,
            running=0,
            completed=0,
            failed=0,
            cancelled=10,
        )

        assert progress.status == BatchStatus.CANCELLED
        assert progress.percent_complete == 100.0


class TestBatchManager:
    """Test BatchManager functionality."""

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

    @pytest.fixture
    def manager(self, queue):
        """Create a BatchManager instance."""
        return BatchManager(queue)

    def test_create_batch(self, manager):
        """Test creating a batch."""
        batch_id = manager.create_batch(
            name="test-batch",
            description="A test batch",
        )

        assert batch_id is not None
        assert len(batch_id) == 36  # UUID length

    def test_create_batch_without_name(self, manager):
        """Test creating a batch without a name."""
        batch_id = manager.create_batch()

        assert batch_id is not None

    def test_submit_jobs(self, manager):
        """Test submitting jobs to a batch."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [
            {"task": "email", "to": "user1@example.com"},
            {"task": "email", "to": "user2@example.com"},
            {"task": "email", "to": "user3@example.com"},
        ]

        jobs = manager.submit_jobs(
            batch_id=batch_id,
            payloads=payloads,
            shared_config={"template": "welcome"},
        )

        assert len(jobs) == 3
        for i, job in enumerate(jobs):
            assert job.batch_id == batch_id
            assert job.batch_config == {"template": "welcome"}
            assert job.batch_order == i
            assert job.id is not None

    def test_submit_jobs_empty_payloads(self, manager):
        """Test submitting empty payloads."""
        batch_id = manager.create_batch(name="test-batch")
        jobs = manager.submit_jobs(batch_id=batch_id, payloads=[])

        assert jobs == []

    def test_submit_jobs_invalid_batch_id(self, manager):
        """Test submitting jobs with invalid batch_id."""
        with pytest.raises(BatchError, match="batch_id is required"):
            manager.submit_jobs(batch_id="", payloads=[{"task": "test"}])

        with pytest.raises(BatchError, match="batch_id is required"):
            manager.submit_jobs(batch_id=None, payloads=[{"task": "test"}])

    def test_submit_jobs_sequential(self, manager):
        """Test submitting sequential jobs."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [
            {"task": "step1"},
            {"task": "step2"},
            {"task": "step3"},
        ]

        jobs = manager.submit_jobs(
            batch_id=batch_id,
            payloads=payloads,
            sequential=True,
        )

        assert len(jobs) == 3
        # Check dependencies
        assert jobs[0].depends_on is None
        assert jobs[1].depends_on == [jobs[0].id]
        assert jobs[2].depends_on == [jobs[1].id]

    def test_get_progress_empty_batch(self, manager):
        """Test getting progress for non-existent batch."""
        batch_id = manager.create_batch(name="test-batch")

        with pytest.raises(BatchNotFoundError):
            manager.get_progress(batch_id)

    def test_get_progress(self, manager, queue):
        """Test getting batch progress."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [
            {"task": "job1"},
            {"task": "job2"},
            {"task": "job3"},
        ]

        jobs = manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        # Complete one job
        queue.complete(jobs[0].id)
        # Fail one job
        queue.fail(jobs[1].id, "Test error")
        # Leave one pending

        progress = manager.get_progress(batch_id)

        assert progress.batch_id == batch_id
        assert progress.total == 3
        assert progress.completed == 1
        assert progress.failed == 1
        assert progress.pending == 1
        assert progress.percent_complete == pytest.approx(66.67, rel=0.1)

    def test_list_batches(self, manager):
        """Test listing batches."""
        # Create multiple batches
        batch1 = manager.create_batch(name="batch-1")
        batch2 = manager.create_batch(name="batch-2")

        # Submit jobs to batch1
        manager.submit_jobs(
            batch_id=batch1,
            payloads=[{"task": "test"}, {"task": "test"}],
        )

        batches = manager.list_batches()

        assert len(batches) >= 2
        batch_ids = [b["batch_id"] for b in batches]
        assert batch1 in batch_ids
        assert batch2 in batch_ids

    # -------------------------------------------------------------------------
    # Bulk Operations Tests
    # -------------------------------------------------------------------------

    def test_bulk_retry(self, manager, queue):
        """Test bulk retry operation."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(3)]

        jobs = manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        # Fail all jobs
        for job in jobs:
            queue.fail(job.id, f"Error for job {job.id}")

        # Retry failed jobs
        result = manager.bulk_retry(batch_id)

        assert result.success is True
        assert result.processed_count == 3
        assert result.failed_count == 0

        # Check that jobs are now pending
        progress = manager.get_progress(batch_id)
        assert progress.pending == 3
        assert progress.failed == 0

    def test_bulk_retry_failed_only(self, manager, queue):
        """Test bulk retry with failed_only=True."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(3)]

        jobs = manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        # Fail one job, complete one, leave one pending
        queue.fail(jobs[0].id, "Error")
        queue.complete(jobs[1].id)
        # jobs[2] stays pending

        result = manager.bulk_retry(batch_id, failed_only=True)

        assert result.processed_count == 1

    def test_bulk_retry_all(self, manager, queue):
        """Test bulk retry with failed_only=False."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(3)]

        jobs = manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        # Fail one job
        queue.fail(jobs[0].id, "Error")
        # Complete one
        queue.complete(jobs[1].id)

        result = manager.bulk_retry(batch_id, failed_only=False)

        # Only the failed one will be retried (completed ones aren't retryable)
        assert result.processed_count == 1

    def test_bulk_retry_with_callback(self, manager, queue):
        """Test bulk retry with error callback."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": "job1"}]

        jobs = manager.submit_jobs(batch_id=batch_id, payloads=payloads)
        queue.fail(jobs[0].id, "Error")

        errors = []

        def on_error(job_id, exc):
            errors.append((job_id, str(exc)))

        result = manager.bulk_retry(batch_id, on_error=on_error)

        assert result.success is True

    def test_bulk_cancel_pending(self, manager, queue):
        """Test bulk cancel for pending jobs."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(3)]

        manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        result = manager.bulk_cancel(batch_id, pending_only=True)

        assert result.success is True
        assert result.processed_count == 3

        # Verify all cancelled (jobs are deleted, so batch appears empty)
        # After cancellation, the batch should have no jobs
        with pytest.raises(BatchNotFoundError):
            manager.get_progress(batch_id)

    def test_bulk_cancel_all_statuses(self, manager, queue):
        """Test bulk cancel for all cancellable statuses."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(3)]

        manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        # Start one job
        job = queue.dequeue()
        assert job is not None

        # Cancel all (pending + running)
        result = manager.bulk_cancel(batch_id, pending_only=False)

        assert result.processed_count >= 2  # At least pending + running

    def test_bulk_delete(self, manager, queue):
        """Test bulk delete operation."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(3)]

        jobs = manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        # Complete one job
        queue.complete(jobs[0].id)

        result = manager.bulk_delete(batch_id, completed_only=True)

        assert result.success is True
        assert result.processed_count == 1

    def test_bulk_delete_all(self, manager, queue):
        """Test bulk delete for all jobs."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(3)]

        manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        result = manager.bulk_delete(batch_id, completed_only=False)

        assert result.success is True
        assert result.processed_count == 3

        # Verify no jobs remain
        with pytest.raises(BatchNotFoundError):
            manager.get_progress(batch_id)

    def test_get_batch_stats(self, manager, queue):
        """Test getting detailed batch statistics."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(5)]

        jobs = manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        # Complete 2, fail 1, leave 2 pending
        queue.complete(jobs[0].id)
        queue.complete(jobs[1].id)
        queue.fail(jobs[2].id, "Error")

        stats = manager.get_batch_stats(batch_id)

        assert stats["batch_id"] == batch_id
        assert stats["total_jobs"] == 5
        assert stats["completed"] == 2
        assert stats["failed"] == 1
        assert stats["pending"] == 2
        assert "percent_complete" in stats
        assert "average_retries" in stats

    def test_batch_result_dataclass(self):
        """Test BatchResult dataclass."""
        result = BatchResult(
            batch_id="batch-123",
            success=False,
            processed_count=8,
            failed_count=2,
            failed_job_ids=[1, 2],
            errors=["Error 1", "Error 2"],
        )

        assert result.batch_id == "batch-123"
        assert result.success is False
        assert result.processed_count == 8
        assert result.failed_count == 2
        assert result.failed_job_ids == [1, 2]
        assert result.errors == ["Error 1", "Error 2"]

    def test_batch_error_exception(self):
        """Test BatchError exception."""
        with pytest.raises(BatchError, match="Test error"):
            raise BatchError("Test error")

    def test_batch_not_found_error_exception(self):
        """Test BatchNotFoundError exception."""
        with pytest.raises(BatchNotFoundError, match="Batch not found"):
            raise BatchNotFoundError("Batch not found")

    def test_context_manager(self, queue):
        """Test BatchManager as context manager."""
        with BatchManager(queue) as manager:
            batch_id = manager.create_batch(name="test")
            assert batch_id is not None

    def test_batch_with_priority(self, manager):
        """Test submitting batch jobs with priority."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": "job1"}]

        jobs = manager.submit_jobs(
            batch_id=batch_id,
            payloads=payloads,
            priority=JobPriority.HIGH,
        )

        assert jobs[0].priority == JobPriority.HIGH

    def test_batch_with_max_retries(self, manager):
        """Test submitting batch jobs with custom max_retries."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": "job1"}]

        jobs = manager.submit_jobs(
            batch_id=batch_id,
            payloads=payloads,
            max_retries=5,
        )

        assert jobs[0].max_retries == 5

    def test_pause_and_resume_batch(self, manager, queue):
        """Test pausing and resuming a batch."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": f"job{i}"} for i in range(3)]

        manager.submit_jobs(batch_id=batch_id, payloads=payloads)

        # Pause the batch
        pause_result = manager.pause_batch(batch_id)
        assert pause_result.processed_count == 3

        # Resume the batch
        resume_result = manager.resume_batch(batch_id)
        assert resume_result.processed_count == 3

    def test_bulk_retry_not_retryable(self, manager, queue):
        """Test bulk retry when jobs are not retryable."""
        batch_id = manager.create_batch(name="test-batch")
        payloads = [{"task": "job1"}]

        jobs = manager.submit_jobs(
            batch_id=batch_id,
            payloads=payloads,
            max_retries=0,  # No retries allowed
        )
        queue.fail(jobs[0].id, "Error")

        result = manager.bulk_retry(batch_id)

        # Job isn't retryable, so processed_count should be 0
        assert result.processed_count == 0

    def test_bulk_operations_nonexistent_batch(self, manager):
        """Test bulk operations on non-existent batch."""
        fake_batch_id = "nonexistent-batch-123"

        with pytest.raises(BatchNotFoundError):
            manager.bulk_retry(fake_batch_id)

        with pytest.raises(BatchNotFoundError):
            manager.bulk_cancel(fake_batch_id)

        with pytest.raises(BatchNotFoundError):
            manager.bulk_delete(fake_batch_id)


class TestBatchIntegration:
    """Integration tests for batch operations."""

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

    def test_end_to_end_batch_workflow(self, queue):
        """Test complete batch workflow from creation to completion."""
        with BatchManager(queue) as manager:
            # 1. Create a batch
            batch_id = manager.create_batch(
                name="email-campaign",
                description="Send welcome emails to new users",
            )
            assert batch_id is not None

            # 2. Submit jobs
            payloads = [
                {"to": "user1@example.com", "template": "welcome"},
                {"to": "user2@example.com", "template": "welcome"},
                {"to": "user3@example.com", "template": "welcome"},
            ]
            jobs = manager.submit_jobs(
                batch_id=batch_id,
                payloads=payloads,
                shared_config={"from": "noreply@example.com"},
            )
            assert len(jobs) == 3

            # 3. Check initial progress
            progress = manager.get_progress(batch_id)
            assert progress.total == 3
            assert progress.pending == 3
            assert progress.status == BatchStatus.PENDING

            # 4. Process some jobs (skip batch tracking jobs)
            completed = 0
            while completed < 2:
                job = queue.dequeue()
                if job is None:
                    break
                # Skip batch tracking jobs
                if job.payload.get("__batch_tracking__"):
                    queue.complete(job.id)
                    continue
                queue.complete(job.id)
                completed += 1

            # 5. Fail one job
            job = queue.dequeue()
            while job and job.payload.get("__batch_tracking__"):
                queue.complete(job.id)
                job = queue.dequeue()
            assert job is not None
            queue.fail(job.id, "SMTP error")

            # 6. Check updated progress
            progress = manager.get_progress(batch_id)
            assert progress.completed == 2
            assert progress.failed == 1
            assert progress.status == BatchStatus.PARTIAL

            # 7. Retry failed job
            retry_result = manager.bulk_retry(batch_id)
            assert retry_result.processed_count == 1

            # Complete the retried job (keep dequeuing until we complete one)
            while True:
                job = queue.dequeue()
                if job:
                    queue.complete(job.id)
                    break

            # 8. Get final stats
            stats = manager.get_batch_stats(batch_id)
            assert stats["total_jobs"] == 3

            # 9. Clean up - delete completed jobs
            delete_result = manager.bulk_delete(batch_id, completed_only=True)
            assert delete_result.processed_count >= 2

    def test_sequential_batch_execution(self, queue):
        """Test that sequential batch jobs execute in order."""
        with BatchManager(queue) as manager:
            batch_id = manager.create_batch(name="sequential-batch")

            payloads = [
                {"step": 1, "action": "initialize"},
                {"step": 2, "action": "process"},
                {"step": 3, "action": "finalize"},
            ]

            jobs = manager.submit_jobs(
                batch_id=batch_id,
                payloads=payloads,
                sequential=True,
            )

            # Verify dependencies
            assert jobs[0].depends_on is None
            assert jobs[1].depends_on == [jobs[0].id]
            assert jobs[2].depends_on == [jobs[1].id]

            # First job should be available (skip batch tracking jobs)
            job1 = queue.dequeue()
            while job1 and job1.payload.get("__batch_tracking__"):
                queue.complete(job1.id)
                job1 = queue.dequeue()
            assert job1 is not None
            assert job1.payload.get("step") == 1

            # Second job should not be available (depends on first)
            job2 = queue.dequeue()
            assert job2 is None  # Because job1 is still running

            # Complete first job
            queue.complete(job1.id)

            # Now second job should be available (skip any tracking jobs)
            job2 = queue.dequeue()
            while job2 and job2.payload.get("__batch_tracking__"):
                queue.complete(job2.id)
                job2 = queue.dequeue()
            assert job2 is not None
            assert job2.payload.get("step") == 2

    def test_multiple_batches_isolation(self, queue):
        """Test that multiple batches are isolated from each other."""
        with BatchManager(queue) as manager:
            # Create two batches
            batch1 = manager.create_batch(name="batch-1")
            batch2 = manager.create_batch(name="batch-2")

            # Submit jobs to each batch
            manager.submit_jobs(
                batch_id=batch1,
                payloads=[{"task": "b1-job1"}, {"task": "b1-job2"}],
            )
            manager.submit_jobs(
                batch_id=batch2,
                payloads=[{"task": "b2-job1"}],
            )

            # Check progress for each batch
            progress1 = manager.get_progress(batch1)
            progress2 = manager.get_progress(batch2)

            assert progress1.total == 2
            assert progress2.total == 1

            # Complete one job from batch1 (skip tracking jobs)
            job = queue.dequeue()
            while job and job.payload.get("__batch_tracking__"):
                queue.complete(job.id)
                job = queue.dequeue()
            assert job is not None
            queue.complete(job.id)

            # Recheck progress
            progress1 = manager.get_progress(batch1)
            progress2 = manager.get_progress(batch2)

            assert progress1.completed == 1
            assert progress2.completed == 0

    def test_partial_failure_handling(self, queue):
        """Test handling of partial failures in a batch."""
        with BatchManager(queue) as manager:
            batch_id = manager.create_batch(name="partial-failures")

            payloads = [{"task": f"job{i}"} for i in range(10)]
            jobs = manager.submit_jobs(batch_id=batch_id, payloads=payloads)

            # Complete 5 jobs, fail 3, leave 2 pending
            for i, job in enumerate(jobs):
                if i < 5:
                    queue.complete(job.id)
                elif i < 8:
                    queue.fail(job.id, f"Error {i}")

            # Check progress
            progress = manager.get_progress(batch_id)
            assert progress.status == BatchStatus.PARTIAL
            assert progress.completed == 5
            assert progress.failed == 3
            assert progress.pending == 2

            # Retry failed jobs
            retry_result = manager.bulk_retry(batch_id)
            assert retry_result.processed_count == 3

            # After retry, we have 5 pending jobs (2 original + 3 retried)
            # Dequeue and complete until we have 8 total completed
            while True:
                progress = manager.get_progress(batch_id)
                if progress.completed >= 8:
                    break
                job = queue.dequeue()
                if job:
                    queue.complete(job.id)
                else:
                    break

            # Now should have 8 completed and 2 pending
            progress = manager.get_progress(batch_id)
            assert progress.status == BatchStatus.PENDING
            assert progress.completed == 8
            assert progress.pending == 2

    def test_batch_list_with_progress(self, queue):
        """Test listing batches shows progress correctly."""
        with BatchManager(queue) as manager:
            # Create and populate batch
            batch_id = manager.create_batch(name="progress-test")
            manager.submit_jobs(
                batch_id=batch_id,
                payloads=[{"task": f"job{i}"} for i in range(4)],
            )

            # Complete half (skip batch tracking jobs)
            completed = 0
            while completed < 2:
                job = queue.dequeue()
                if job and not job.payload.get("__batch_tracking__"):
                    queue.complete(job.id)
                    completed += 1

            batches = manager.list_batches()
            batch_info = next(b for b in batches if b["batch_id"] == batch_id)

            assert batch_info["name"] == "progress-test"
            assert batch_info["job_count"] == 4
            assert batch_info["percent_complete"] == 50.0

    def test_error_callback_in_bulk_operations(self, queue):
        """Test error callbacks in bulk operations."""
        with BatchManager(queue) as manager:
            batch_id = manager.create_batch(name="error-callback-test")
            payloads = [{"task": f"job{i}"} for i in range(3)]
            manager.submit_jobs(batch_id=batch_id, payloads=payloads)

            errors_captured = []

            def on_error(job_id, exc):
                errors_captured.append((job_id, str(exc)))

            # Try to cancel with a callback (shouldn't trigger errors normally)
            result = manager.bulk_cancel(batch_id, on_error=on_error)

            assert result.success is True
            assert len(errors_captured) == 0  # No errors expected

    def test_batch_with_metadata(self, queue):
        """Test batch with metadata."""
        with BatchManager(queue) as manager:
            metadata = {"source": "api", "version": "1.0", "tags": ["test", "integration"]}

            batch_id = manager.create_batch(
                name="metadata-test",
                description="Testing metadata",
                metadata=metadata,
            )

            assert batch_id is not None

            batches = manager.list_batches()
            batch_info = next(b for b in batches if b["batch_id"] == batch_id)
            assert batch_info["name"] == "metadata-test"
