"""Tests for queue manager."""

import json
import tempfile
from collections.abc import Generator
from pathlib import Path

import pytest

from src.models import IssueMetadata
from src.queue import QueueItem, QueueItemStatus, QueueManager


class TestQueueItem:
    """Tests for QueueItem dataclass."""

    def test_queue_item_creation(self) -> None:
        """Test creating a queue item with all fields."""
        metadata = IssueMetadata(
            estimated_context=50000,
            difficulty="medium",
            assigned_agent="sonnet",
            blocks=[161, 162],
            blocked_by=[158],
        )
        item = QueueItem(
            issue_number=159,
            metadata=metadata,
            status=QueueItemStatus.PENDING,
        )

        assert item.issue_number == 159
        assert item.metadata == metadata
        assert item.status == QueueItemStatus.PENDING
        assert item.ready is False  # Should not be ready (blocked_by exists)

    def test_queue_item_defaults(self) -> None:
        """Test queue item with default values."""
        metadata = IssueMetadata()
        item = QueueItem(
            issue_number=160,
            metadata=metadata,
        )

        assert item.issue_number == 160
        assert item.status == QueueItemStatus.PENDING
        assert item.ready is True  # Should be ready (no blockers)

    def test_queue_item_serialization(self) -> None:
        """Test converting queue item to dict for JSON serialization."""
        metadata = IssueMetadata(
            estimated_context=30000,
            difficulty="easy",
            assigned_agent="haiku",
            blocks=[165],
            blocked_by=[],
        )
        item = QueueItem(
            issue_number=164,
            metadata=metadata,
            status=QueueItemStatus.IN_PROGRESS,
            ready=True,
        )

        data = item.to_dict()

        assert data["issue_number"] == 164
        assert data["status"] == "in_progress"
        assert data["ready"] is True
        assert data["metadata"]["estimated_context"] == 30000
        assert data["metadata"]["difficulty"] == "easy"

    def test_queue_item_deserialization(self) -> None:
        """Test creating queue item from dict."""
        data = {
            "issue_number": 161,
            "status": "completed",
            "ready": False,
            "metadata": {
                "estimated_context": 75000,
                "difficulty": "hard",
                "assigned_agent": "opus",
                "blocks": [166, 167],
                "blocked_by": [159],
            },
        }

        item = QueueItem.from_dict(data)

        assert item.issue_number == 161
        assert item.status == QueueItemStatus.COMPLETED
        assert item.ready is False
        assert item.metadata.estimated_context == 75000
        assert item.metadata.difficulty == "hard"
        assert item.metadata.assigned_agent == "opus"
        assert item.metadata.blocks == [166, 167]
        assert item.metadata.blocked_by == [159]


class TestQueueManager:
    """Tests for QueueManager."""

    @pytest.fixture
    def temp_queue_file(self) -> Generator[Path, None, None]:
        """Create a temporary file for queue persistence."""
        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f:
            temp_path = Path(f.name)
        yield temp_path
        # Cleanup
        if temp_path.exists():
            temp_path.unlink()

    @pytest.fixture
    def queue_manager(self, temp_queue_file: Path) -> QueueManager:
        """Create a queue manager with temporary storage."""
        return QueueManager(queue_file=temp_queue_file)

    def test_enqueue_single_item(self, queue_manager: QueueManager) -> None:
        """Test enqueuing a single item."""
        metadata = IssueMetadata(
            estimated_context=40000,
            difficulty="medium",
            assigned_agent="sonnet",
            blocks=[],
            blocked_by=[],
        )

        queue_manager.enqueue(159, metadata)

        assert queue_manager.size() == 1
        item = queue_manager.get_item(159)
        assert item is not None
        assert item.issue_number == 159
        assert item.status == QueueItemStatus.PENDING
        assert item.ready is True

    def test_enqueue_multiple_items(self, queue_manager: QueueManager) -> None:
        """Test enqueuing multiple items."""
        meta1 = IssueMetadata(assigned_agent="sonnet")
        meta2 = IssueMetadata(assigned_agent="haiku")
        meta3 = IssueMetadata(assigned_agent="glm")

        queue_manager.enqueue(159, meta1)
        queue_manager.enqueue(160, meta2)
        queue_manager.enqueue(161, meta3)

        assert queue_manager.size() == 3

    def test_dequeue_item(self, queue_manager: QueueManager) -> None:
        """Test removing an item from the queue."""
        metadata = IssueMetadata()
        queue_manager.enqueue(159, metadata)

        assert queue_manager.size() == 1
        queue_manager.dequeue(159)
        assert queue_manager.size() == 0
        assert queue_manager.get_item(159) is None

    def test_dequeue_nonexistent_item(self, queue_manager: QueueManager) -> None:
        """Test dequeuing an item that doesn't exist."""
        # Should not raise error, just be a no-op
        queue_manager.dequeue(999)
        assert queue_manager.size() == 0

    def test_get_next_ready_simple(self, queue_manager: QueueManager) -> None:
        """Test getting next ready item with no dependencies."""
        meta1 = IssueMetadata(assigned_agent="sonnet")
        meta2 = IssueMetadata(assigned_agent="haiku")

        queue_manager.enqueue(159, meta1)
        queue_manager.enqueue(160, meta2)

        next_item = queue_manager.get_next_ready()
        assert next_item is not None
        # Should return first item (159) since both are ready
        assert next_item.issue_number == 159

    def test_get_next_ready_with_dependencies(self, queue_manager: QueueManager) -> None:
        """Test getting next ready item with dependency chain."""
        # Issue 160 blocks 161, 158 blocks 159
        meta_158 = IssueMetadata(blocks=[159], blocked_by=[])
        meta_159 = IssueMetadata(blocks=[161], blocked_by=[158])
        meta_160 = IssueMetadata(blocks=[161], blocked_by=[])
        meta_161 = IssueMetadata(blocks=[], blocked_by=[159, 160])

        queue_manager.enqueue(158, meta_158)
        queue_manager.enqueue(159, meta_159)
        queue_manager.enqueue(160, meta_160)
        queue_manager.enqueue(161, meta_161)

        # Should get 158 or 160 (both ready, no blockers)
        next_item = queue_manager.get_next_ready()
        assert next_item is not None
        assert next_item.issue_number in [158, 160]
        assert next_item.ready is True

    def test_get_next_ready_empty_queue(self, queue_manager: QueueManager) -> None:
        """Test getting next ready item from empty queue."""
        next_item = queue_manager.get_next_ready()
        assert next_item is None

    def test_get_next_ready_all_blocked(self, queue_manager: QueueManager) -> None:
        """Test getting next ready when all items are blocked."""
        # Circular dependency: 159 blocks 160, 160 blocks 159
        meta_159 = IssueMetadata(blocks=[160], blocked_by=[160])
        meta_160 = IssueMetadata(blocks=[159], blocked_by=[159])

        queue_manager.enqueue(159, meta_159)
        queue_manager.enqueue(160, meta_160)

        next_item = queue_manager.get_next_ready()
        # Should still return one (circular dependencies handled)
        assert next_item is not None

    def test_mark_complete(self, queue_manager: QueueManager) -> None:
        """Test marking an item as complete."""
        metadata = IssueMetadata()
        queue_manager.enqueue(159, metadata)

        queue_manager.mark_complete(159)

        item = queue_manager.get_item(159)
        assert item is not None
        assert item.status == QueueItemStatus.COMPLETED

    def test_mark_complete_unblocks_dependents(self, queue_manager: QueueManager) -> None:
        """Test that completing an item unblocks dependent items."""
        # 158 blocks 159
        meta_158 = IssueMetadata(blocks=[159], blocked_by=[])
        meta_159 = IssueMetadata(blocks=[], blocked_by=[158])

        queue_manager.enqueue(158, meta_158)
        queue_manager.enqueue(159, meta_159)

        # Initially, 159 should not be ready
        item_159 = queue_manager.get_item(159)
        assert item_159 is not None
        assert item_159.ready is False

        # Complete 158
        queue_manager.mark_complete(158)

        # Now 159 should be ready
        item_159_updated = queue_manager.get_item(159)
        assert item_159_updated is not None
        assert item_159_updated.ready is True

    def test_mark_complete_nonexistent_item(self, queue_manager: QueueManager) -> None:
        """Test marking nonexistent item as complete."""
        # Should not raise error, just be a no-op
        queue_manager.mark_complete(999)

    def test_update_ready_status(self, queue_manager: QueueManager) -> None:
        """Test updating ready status for all items."""
        # Complex dependency chain
        meta_158 = IssueMetadata(blocks=[159], blocked_by=[])
        meta_159 = IssueMetadata(blocks=[160, 161], blocked_by=[158])
        meta_160 = IssueMetadata(blocks=[], blocked_by=[159])
        meta_161 = IssueMetadata(blocks=[], blocked_by=[159])

        queue_manager.enqueue(158, meta_158)
        queue_manager.enqueue(159, meta_159)
        queue_manager.enqueue(160, meta_160)
        queue_manager.enqueue(161, meta_161)

        # Initially: 158 ready, others blocked
        item_158 = queue_manager.get_item(158)
        item_159 = queue_manager.get_item(159)
        item_160 = queue_manager.get_item(160)
        item_161 = queue_manager.get_item(161)
        assert item_158 is not None
        assert item_159 is not None
        assert item_160 is not None
        assert item_161 is not None
        assert item_158.ready is True
        assert item_159.ready is False
        assert item_160.ready is False
        assert item_161.ready is False

        # Complete 158
        queue_manager.mark_complete(158)

        # Now: 159 ready, 160 and 161 still blocked
        item_159_updated = queue_manager.get_item(159)
        item_160_updated = queue_manager.get_item(160)
        item_161_updated = queue_manager.get_item(161)
        assert item_159_updated is not None
        assert item_160_updated is not None
        assert item_161_updated is not None
        assert item_159_updated.ready is True
        assert item_160_updated.ready is False
        assert item_161_updated.ready is False

    def test_persistence_save(self, queue_manager: QueueManager, temp_queue_file: Path) -> None:
        """Test saving queue to disk."""
        metadata = IssueMetadata(
            estimated_context=50000,
            difficulty="medium",
            assigned_agent="sonnet",
            blocks=[161],
            blocked_by=[158],
        )

        queue_manager.enqueue(159, metadata)
        queue_manager.save()

        assert temp_queue_file.exists()

        # Verify JSON structure
        with open(temp_queue_file) as f:
            data = json.load(f)

        assert "items" in data
        assert len(data["items"]) == 1
        assert data["items"][0]["issue_number"] == 159

    def test_persistence_load(self, temp_queue_file: Path) -> None:
        """Test loading queue from disk."""
        # Create test data
        queue_data = {
            "items": [
                {
                    "issue_number": 159,
                    "status": "pending",
                    "ready": False,
                    "metadata": {
                        "estimated_context": 50000,
                        "difficulty": "medium",
                        "assigned_agent": "sonnet",
                        "blocks": [161],
                        "blocked_by": [158],
                    },
                },
                {
                    "issue_number": 160,
                    "status": "in_progress",
                    "ready": True,
                    "metadata": {
                        "estimated_context": 30000,
                        "difficulty": "easy",
                        "assigned_agent": "haiku",
                        "blocks": [],
                        "blocked_by": [],
                    },
                },
            ]
        }

        with open(temp_queue_file, "w") as f:
            json.dump(queue_data, f)

        # Load queue
        queue_manager = QueueManager(queue_file=temp_queue_file)

        assert queue_manager.size() == 2

        item_159 = queue_manager.get_item(159)
        assert item_159 is not None
        assert item_159.status == QueueItemStatus.PENDING
        assert item_159.ready is False

        item_160 = queue_manager.get_item(160)
        assert item_160 is not None
        assert item_160.status == QueueItemStatus.IN_PROGRESS
        assert item_160.ready is True

    def test_persistence_load_nonexistent_file(self, temp_queue_file: Path) -> None:
        """Test loading from nonexistent file creates empty queue."""
        # Don't create the file
        temp_queue_file.unlink(missing_ok=True)

        queue_manager = QueueManager(queue_file=temp_queue_file)

        assert queue_manager.size() == 0

    def test_persistence_autosave_on_enqueue(
        self, queue_manager: QueueManager, temp_queue_file: Path
    ) -> None:
        """Test that enqueue automatically saves to disk."""
        metadata = IssueMetadata()
        queue_manager.enqueue(159, metadata)

        # Should auto-save
        assert temp_queue_file.exists()

        # Load in new manager to verify
        new_manager = QueueManager(queue_file=temp_queue_file)
        assert new_manager.size() == 1

    def test_persistence_autosave_on_mark_complete(
        self, queue_manager: QueueManager, temp_queue_file: Path
    ) -> None:
        """Test that mark_complete automatically saves to disk."""
        metadata = IssueMetadata()
        queue_manager.enqueue(159, metadata)
        queue_manager.mark_complete(159)

        # Load in new manager to verify
        new_manager = QueueManager(queue_file=temp_queue_file)
        item = new_manager.get_item(159)
        assert item is not None
        assert item.status == QueueItemStatus.COMPLETED

    def test_circular_dependency_detection(self, queue_manager: QueueManager) -> None:
        """Test handling of circular dependencies."""
        # Create circular dependency: 159 -> 160 -> 161 -> 159
        meta_159 = IssueMetadata(blocks=[160], blocked_by=[161])
        meta_160 = IssueMetadata(blocks=[161], blocked_by=[159])
        meta_161 = IssueMetadata(blocks=[159], blocked_by=[160])

        queue_manager.enqueue(159, meta_159)
        queue_manager.enqueue(160, meta_160)
        queue_manager.enqueue(161, meta_161)

        # Should still be able to get next ready (break the cycle gracefully)
        next_item = queue_manager.get_next_ready()
        assert next_item is not None

    def test_list_all_items(self, queue_manager: QueueManager) -> None:
        """Test listing all items in queue."""
        meta1 = IssueMetadata(assigned_agent="sonnet")
        meta2 = IssueMetadata(assigned_agent="haiku")
        meta3 = IssueMetadata(assigned_agent="glm")

        queue_manager.enqueue(159, meta1)
        queue_manager.enqueue(160, meta2)
        queue_manager.enqueue(161, meta3)

        all_items = queue_manager.list_all()
        assert len(all_items) == 3
        issue_numbers = [item.issue_number for item in all_items]
        assert 159 in issue_numbers
        assert 160 in issue_numbers
        assert 161 in issue_numbers

    def test_list_ready_items(self, queue_manager: QueueManager) -> None:
        """Test listing only ready items."""
        meta_ready = IssueMetadata(blocked_by=[])
        meta_blocked = IssueMetadata(blocked_by=[158])

        queue_manager.enqueue(159, meta_ready)
        queue_manager.enqueue(160, meta_ready)
        queue_manager.enqueue(161, meta_blocked)

        ready_items = queue_manager.list_ready()
        assert len(ready_items) == 2
        issue_numbers = [item.issue_number for item in ready_items]
        assert 159 in issue_numbers
        assert 160 in issue_numbers
        assert 161 not in issue_numbers

    def test_get_item_nonexistent(self, queue_manager: QueueManager) -> None:
        """Test getting an item that doesn't exist."""
        item = queue_manager.get_item(999)
        assert item is None

    def test_status_transitions(self, queue_manager: QueueManager) -> None:
        """Test valid status transitions."""
        metadata = IssueMetadata()
        queue_manager.enqueue(159, metadata)

        # PENDING -> IN_PROGRESS
        item = queue_manager.get_item(159)
        assert item is not None
        assert item.status == QueueItemStatus.PENDING

        queue_manager.mark_in_progress(159)
        item = queue_manager.get_item(159)
        assert item is not None
        assert item.status == QueueItemStatus.IN_PROGRESS

        # IN_PROGRESS -> COMPLETED
        queue_manager.mark_complete(159)
        item = queue_manager.get_item(159)
        assert item is not None
        assert item.status == QueueItemStatus.COMPLETED


class TestQueueCorruptionHandling:
    """Tests for queue file corruption handling."""

    @pytest.fixture
    def temp_queue_file(self) -> Generator[Path, None, None]:
        """Create a temporary file for queue persistence."""
        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f:
            temp_path = Path(f.name)
        yield temp_path
        # Cleanup - remove main file and any backup files
        if temp_path.exists():
            temp_path.unlink()
        # Clean up backup files
        for backup in temp_path.parent.glob(f"{temp_path.stem}.corrupted.*.json"):
            backup.unlink()

    def test_corrupted_json_logs_error_and_creates_backup(
        self, temp_queue_file: Path, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Test that corrupted JSON file triggers logging and backup creation."""
        # Write invalid JSON to the file
        with open(temp_queue_file, "w") as f:
            f.write("{ invalid json content }")

        import logging

        with caplog.at_level(logging.ERROR):
            queue_manager = QueueManager(queue_file=temp_queue_file)

        # Verify queue is empty after corruption
        assert queue_manager.size() == 0

        # Verify error was logged
        assert "Queue file corruption detected" in caplog.text
        assert "JSONDecodeError" in caplog.text

        # Verify backup file was created
        backup_files = list(temp_queue_file.parent.glob(f"{temp_queue_file.stem}.corrupted.*.json"))
        assert len(backup_files) == 1
        assert "Corrupted queue file backed up" in caplog.text

        # Verify backup contains original corrupted content
        with open(backup_files[0]) as f:
            backup_content = f.read()
        assert "invalid json content" in backup_content

    def test_corrupted_structure_logs_error_and_creates_backup(
        self, temp_queue_file: Path, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Test that valid JSON with invalid structure triggers logging and backup."""
        # Write valid JSON but with missing required fields
        with open(temp_queue_file, "w") as f:
            json.dump(
                {
                    "items": [
                        {
                            "issue_number": 159,
                            # Missing "status", "ready", "metadata" fields
                        }
                    ]
                },
                f,
            )

        import logging

        with caplog.at_level(logging.ERROR):
            queue_manager = QueueManager(queue_file=temp_queue_file)

        # Verify queue is empty after corruption
        assert queue_manager.size() == 0

        # Verify error was logged (KeyError for missing fields)
        assert "Queue file corruption detected" in caplog.text
        assert "KeyError" in caplog.text

        # Verify backup file was created
        backup_files = list(temp_queue_file.parent.glob(f"{temp_queue_file.stem}.corrupted.*.json"))
        assert len(backup_files) == 1

    def test_invalid_status_value_logs_error_and_creates_backup(
        self, temp_queue_file: Path, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Test that invalid enum value triggers logging and backup."""
        # Write valid JSON but with invalid status enum value
        with open(temp_queue_file, "w") as f:
            json.dump(
                {
                    "items": [
                        {
                            "issue_number": 159,
                            "status": "invalid_status",
                            "ready": True,
                            "metadata": {
                                "estimated_context": 50000,
                                "difficulty": "medium",
                                "assigned_agent": "sonnet",
                                "blocks": [],
                                "blocked_by": [],
                            },
                        }
                    ]
                },
                f,
            )

        import logging

        with caplog.at_level(logging.ERROR):
            queue_manager = QueueManager(queue_file=temp_queue_file)

        # Verify queue is empty after corruption
        assert queue_manager.size() == 0

        # Verify error was logged (ValueError for invalid enum)
        assert "Queue file corruption detected" in caplog.text
        assert "ValueError" in caplog.text

        # Verify backup file was created
        backup_files = list(temp_queue_file.parent.glob(f"{temp_queue_file.stem}.corrupted.*.json"))
        assert len(backup_files) == 1

    def test_queue_reset_logged_after_corruption(
        self, temp_queue_file: Path, caplog: pytest.LogCaptureFixture
    ) -> None:
        """Test that queue reset is logged after handling corruption."""
        # Write invalid JSON
        with open(temp_queue_file, "w") as f:
            f.write("not valid json")

        import logging

        with caplog.at_level(logging.ERROR):
            QueueManager(queue_file=temp_queue_file)

        # Verify the reset was logged
        assert "Queue reset to empty state after corruption" in caplog.text
