"""
Unit tests for AI Task Jobs.

These tests verify the job enqueueing, status tracking,
and result storage functionality.
"""

import pytest
from unittest.mock import Mock, patch, MagicMock
import json

from app.tasks.ai_tasks import (
    enqueue_ai_task,
    process_ai_task,
    get_job_status,
    get_job_result,
    JobStatus,
    TaskType,
    TaskPriority,
    _store_job_status,
    _update_job_status,
    _store_job_result,
)


class TestEnqueueAITask:
    """Test AI task enqueueing."""

    @patch('app.tasks.ai_tasks.get_queue')
    @patch('app.tasks.ai_tasks._store_job_status')
    def test_enqueue_narrative_task(self, mock_store_status, mock_get_queue):
        """Test enqueueing a narrative task."""
        # Setup mock queue
        mock_queue = MagicMock()
        mock_job = MagicMock()
        mock_job.id = "test_job_123"
        mock_queue.enqueue.return_value = mock_job
        mock_get_queue.return_value = mock_queue

        # Enqueue task
        result = enqueue_ai_task(
            task_type="narrative",
            user_id="user_123",
            context={"action": "explore"},
            priority="normal",
        )

        # Verify
        assert "job_id" in result
        assert result["status"] == "queued"
        mock_queue.enqueue.assert_called_once()
        mock_store_status.assert_called_once()

    @patch('app.tasks.ai_tasks.get_queue')
    @patch('app.tasks.ai_tasks._store_job_status')
    def test_enqueue_high_priority_task(self, mock_store_status, mock_get_queue):
        """Test high priority task goes to front of queue."""
        mock_queue = MagicMock()
        mock_job = MagicMock()
        mock_queue.enqueue.return_value = mock_job
        mock_get_queue.return_value = mock_queue

        enqueue_ai_task(
            task_type="narrative",
            user_id="user_123",
            context={},
            priority="high",
        )

        # Verify at_front=True for high priority
        call_kwargs = mock_queue.enqueue.call_args[1]
        assert call_kwargs["at_front"] is True

    @patch('app.tasks.ai_tasks.get_queue')
    @patch('app.tasks.ai_tasks._store_job_status')
    def test_enqueue_with_session_and_character(self, mock_store_status, mock_get_queue):
        """Test enqueueing with session and character IDs."""
        mock_queue = MagicMock()
        mock_job = MagicMock()
        mock_queue.enqueue.return_value = mock_job
        mock_get_queue.return_value = mock_queue

        result = enqueue_ai_task(
            task_type="combat",
            user_id="user_123",
            context={"enemy": "goblin"},
            session_id="sess_456",
            character_id="char_789",
        )

        assert "job_id" in result

        # Verify kwargs passed to enqueue include session and character
        call_kwargs = mock_queue.enqueue.call_args[1]
        job_kwargs = call_kwargs["kwargs"]
        assert job_kwargs["session_id"] == "sess_456"
        assert job_kwargs["character_id"] == "char_789"

    def test_enqueue_invalid_task_type(self):
        """Test enqueueing with invalid task type raises error."""
        with pytest.raises(ValueError) as exc_info:
            enqueue_ai_task(
                task_type="invalid_type",
                user_id="user_123",
                context={},
            )

        assert "Invalid task_type" in str(exc_info.value)

    def test_enqueue_invalid_priority(self):
        """Test enqueueing with invalid priority raises error."""
        with pytest.raises(ValueError) as exc_info:
            enqueue_ai_task(
                task_type="narrative",
                user_id="user_123",
                context={},
                priority="invalid_priority",
            )

        assert "Invalid priority" in str(exc_info.value)


class TestProcessAITask:
    """Test AI task processing with mocked NarrativeGenerator."""

    @pytest.fixture
    def sample_character(self):
        """Sample character data for tests."""
        return {
            "name": "Aldric",
            "level": 3,
            "player_class": "Fighter",
            "current_hp": 25,
            "max_hp": 30,
            "stats": {"strength": 16, "dexterity": 12},
            "skills": [],
            "effects": []
        }

    @pytest.fixture
    def sample_game_state(self):
        """Sample game state for tests."""
        return {
            "current_location": "Tavern",
            "location_type": "TAVERN",
            "discovered_locations": [],
            "active_quests": []
        }

    @pytest.fixture
    def mock_narrative_response(self):
        """Mock NarrativeResponse for tests."""
        from app.ai.narrative_generator import NarrativeResponse
        return NarrativeResponse(
            narrative="You enter the tavern...",
            tokens_used=150,
            model="meta/meta-llama-3-8b-instruct",
            context_type="story_progression",
            generation_time=2.5
        )

    @patch('app.tasks.ai_tasks._get_user_tier')
    @patch('app.tasks.ai_tasks._update_game_session')
    @patch('app.tasks.ai_tasks.NarrativeGenerator')
    @patch('app.tasks.ai_tasks._update_job_status')
    @patch('app.tasks.ai_tasks._store_job_result')
    def test_process_narrative_task(
        self, mock_store_result, mock_update_status, mock_generator_class,
        mock_update_session, mock_get_tier, sample_character, sample_game_state, mock_narrative_response
    ):
        """Test processing a narrative task with NarrativeGenerator."""
        from app.ai.model_selector import UserTier

        # Setup mocks
        mock_get_tier.return_value = UserTier.FREE
        mock_generator = MagicMock()
        mock_generator.generate_story_response.return_value = mock_narrative_response
        mock_generator_class.return_value = mock_generator

        # Process task
        result = process_ai_task(
            task_type="narrative",
            user_id="user_123",
            context={
                "action": "I explore the tavern",
                "character": sample_character,
                "game_state": sample_game_state
            },
            job_id="job_123",
            session_id="sess_456",
            character_id="char_789"
        )

        # Verify result structure
        assert "narrative" in result
        assert result["narrative"] == "You enter the tavern..."
        assert result["tokens_used"] == 150
        assert result["model"] == "meta/meta-llama-3-8b-instruct"

        # Verify generator was called
        mock_generator.generate_story_response.assert_called_once()

        # Verify session update was called
        mock_update_session.assert_called_once()

    @patch('app.tasks.ai_tasks._get_user_tier')
    @patch('app.tasks.ai_tasks.NarrativeGenerator')
    @patch('app.tasks.ai_tasks._update_job_status')
    @patch('app.tasks.ai_tasks._store_job_result')
    def test_process_combat_task(
        self, mock_store_result, mock_update_status, mock_generator_class,
        mock_get_tier, sample_character, mock_narrative_response
    ):
        """Test processing a combat task."""
        from app.ai.model_selector import UserTier

        mock_get_tier.return_value = UserTier.BASIC
        mock_generator = MagicMock()
        mock_generator.generate_combat_narration.return_value = mock_narrative_response
        mock_generator_class.return_value = mock_generator

        result = process_ai_task(
            task_type="combat",
            user_id="user_123",
            context={
                "character": sample_character,
                "combat_state": {"round_number": 1, "enemies": [], "current_turn": "player"},
                "action": "swings sword",
                "action_result": {"hit": True, "damage": 10}
            },
            job_id="job_123",
        )

        assert "combat_narrative" in result
        mock_generator.generate_combat_narration.assert_called_once()

    @patch('app.tasks.ai_tasks._get_user_tier')
    @patch('app.tasks.ai_tasks.NarrativeGenerator')
    @patch('app.tasks.ai_tasks._update_job_status')
    @patch('app.tasks.ai_tasks._store_job_result')
    def test_process_quest_selection_task(
        self, mock_store_result, mock_update_status, mock_generator_class,
        mock_get_tier, sample_character, sample_game_state
    ):
        """Test processing a quest selection task."""
        from app.ai.model_selector import UserTier

        mock_get_tier.return_value = UserTier.FREE
        mock_generator = MagicMock()
        mock_generator.generate_quest_selection.return_value = "goblin_cave"
        mock_generator_class.return_value = mock_generator

        result = process_ai_task(
            task_type="quest_selection",
            user_id="user_123",
            context={
                "character": sample_character,
                "eligible_quests": [{"quest_id": "goblin_cave"}],
                "game_context": sample_game_state
            },
            job_id="job_123",
        )

        assert result["selected_quest_id"] == "goblin_cave"
        mock_generator.generate_quest_selection.assert_called_once()

    @patch('app.tasks.ai_tasks._get_user_tier')
    @patch('app.tasks.ai_tasks.NarrativeGenerator')
    @patch('app.tasks.ai_tasks._update_job_status')
    @patch('app.tasks.ai_tasks._store_job_result')
    def test_process_npc_dialogue_task(
        self, mock_store_result, mock_update_status, mock_generator_class,
        mock_get_tier, sample_character, sample_game_state, mock_narrative_response
    ):
        """Test processing an NPC dialogue task."""
        from app.ai.model_selector import UserTier

        mock_get_tier.return_value = UserTier.PREMIUM
        mock_generator = MagicMock()
        mock_generator.generate_npc_dialogue.return_value = mock_narrative_response
        mock_generator_class.return_value = mock_generator

        result = process_ai_task(
            task_type="npc_dialogue",
            user_id="user_123",
            context={
                "character": sample_character,
                "npc": {"name": "Innkeeper", "role": "Barkeeper"},
                "conversation_topic": "What's the news?",
                "game_state": sample_game_state
            },
            job_id="job_123",
        )

        assert "dialogue" in result
        mock_generator.generate_npc_dialogue.assert_called_once()

    @patch('app.tasks.ai_tasks._update_job_status')
    def test_process_task_failure_invalid_type(self, mock_update_status):
        """Test task processing failure with invalid task type."""
        with pytest.raises(ValueError):
            process_ai_task(
                task_type="invalid",
                user_id="user_123",
                context={},
                job_id="job_123",
            )

        # Verify status was updated to PROCESSING then FAILED
        calls = mock_update_status.call_args_list
        assert any(call[0][1] == JobStatus.FAILED for call in calls)

    @patch('app.tasks.ai_tasks._get_user_tier')
    @patch('app.tasks.ai_tasks._update_job_status')
    def test_process_task_missing_context_fields(self, mock_update_status, mock_get_tier):
        """Test task processing failure with missing required context fields."""
        from app.ai.model_selector import UserTier
        mock_get_tier.return_value = UserTier.FREE

        with pytest.raises(ValueError) as exc_info:
            process_ai_task(
                task_type="narrative",
                user_id="user_123",
                context={"action": "test"},  # Missing character and game_state
                job_id="job_123",
            )

        assert "Missing required context field" in str(exc_info.value)


class TestJobStatus:
    """Test job status retrieval."""

    @patch('app.tasks.ai_tasks.RedisService')
    def test_get_job_status_from_cache(self, mock_redis_class):
        """Test getting job status from Redis cache."""
        mock_redis = MagicMock()
        mock_redis.get_json.return_value = {
            "job_id": "job_123",
            "status": "completed",
            "created_at": "2025-11-21T10:00:00Z",
        }
        mock_redis_class.return_value = mock_redis

        result = get_job_status("job_123")

        assert result["job_id"] == "job_123"
        assert result["status"] == "completed"

    @patch('app.tasks.ai_tasks.RedisService')
    @patch('app.tasks.ai_tasks.get_redis_connection')
    @patch('app.tasks.ai_tasks.Job')
    def test_get_job_status_from_rq(self, mock_job_class, mock_get_conn, mock_redis_class):
        """Test getting job status from RQ when not in cache."""
        # No cached status
        mock_redis = MagicMock()
        mock_redis.get_json.return_value = None
        mock_redis_class.return_value = mock_redis

        # RQ job
        mock_job = MagicMock()
        mock_job.is_finished = True
        mock_job.is_failed = False
        mock_job.is_started = False
        mock_job.created_at = None
        mock_job.started_at = None
        mock_job.ended_at = None
        mock_job_class.fetch.return_value = mock_job

        result = get_job_status("job_123")

        assert result["status"] == "completed"

    @patch('app.tasks.ai_tasks.RedisService')
    @patch('app.tasks.ai_tasks.get_redis_connection')
    @patch('app.tasks.ai_tasks.Job')
    def test_get_job_status_not_found(self, mock_job_class, mock_get_conn, mock_redis_class):
        """Test getting status of non-existent job."""
        mock_redis = MagicMock()
        mock_redis.get_json.return_value = None
        mock_redis_class.return_value = mock_redis

        mock_job_class.fetch.side_effect = Exception("Job not found")

        result = get_job_status("nonexistent_job")

        assert result["status"] == "unknown"
        assert "error" in result


class TestJobResult:
    """Test job result retrieval."""

    @patch('app.tasks.ai_tasks.RedisService')
    def test_get_job_result_from_cache(self, mock_redis_class):
        """Test getting job result from Redis cache."""
        expected_result = {
            "narrative": "You enter the tavern...",
            "tokens_used": 450,
        }

        mock_redis = MagicMock()
        mock_redis.get_json.return_value = expected_result
        mock_redis_class.return_value = mock_redis

        result = get_job_result("job_123")

        assert result == expected_result

    @patch('app.tasks.ai_tasks.RedisService')
    @patch('app.tasks.ai_tasks.get_redis_connection')
    @patch('app.tasks.ai_tasks.Job')
    def test_get_job_result_from_rq(self, mock_job_class, mock_get_conn, mock_redis_class):
        """Test getting job result from RQ when not in cache."""
        expected_result = {"narrative": "You find a sword..."}

        mock_redis = MagicMock()
        mock_redis.get_json.return_value = None
        mock_redis_class.return_value = mock_redis

        mock_job = MagicMock()
        mock_job.is_finished = True
        mock_job.result = expected_result
        mock_job_class.fetch.return_value = mock_job

        result = get_job_result("job_123")

        assert result == expected_result

    @patch('app.tasks.ai_tasks.RedisService')
    @patch('app.tasks.ai_tasks.get_redis_connection')
    @patch('app.tasks.ai_tasks.Job')
    def test_get_job_result_not_found(self, mock_job_class, mock_get_conn, mock_redis_class):
        """Test getting result of non-existent job."""
        mock_redis = MagicMock()
        mock_redis.get_json.return_value = None
        mock_redis_class.return_value = mock_redis

        mock_job_class.fetch.side_effect = Exception("Job not found")

        result = get_job_result("nonexistent_job")

        assert result is None


class TestStatusStorage:
    """Test job status storage functions."""

    @patch('app.tasks.ai_tasks.RedisService')
    def test_store_job_status(self, mock_redis_class):
        """Test storing initial job status."""
        mock_redis = MagicMock()
        mock_redis_class.return_value = mock_redis

        _store_job_status(
            job_id="job_123",
            status=JobStatus.QUEUED,
            task_type="narrative",
            user_id="user_456",
        )

        mock_redis.set_json.assert_called_once()
        call_args = mock_redis.set_json.call_args
        stored_data = call_args[0][1]

        assert stored_data["job_id"] == "job_123"
        assert stored_data["status"] == "queued"
        assert stored_data["task_type"] == "narrative"
        assert stored_data["user_id"] == "user_456"

    @patch('app.tasks.ai_tasks.RedisService')
    def test_update_job_status_to_processing(self, mock_redis_class):
        """Test updating job status to processing."""
        mock_redis = MagicMock()
        mock_redis.get_json.return_value = {
            "job_id": "job_123",
            "status": "queued",
            "created_at": "2025-11-21T10:00:00Z",
        }
        mock_redis_class.return_value = mock_redis

        _update_job_status("job_123", JobStatus.PROCESSING)

        mock_redis.set_json.assert_called_once()
        call_args = mock_redis.set_json.call_args
        updated_data = call_args[0][1]

        assert updated_data["status"] == "processing"
        assert updated_data["started_at"] is not None

    @patch('app.tasks.ai_tasks.RedisService')
    def test_update_job_status_to_completed(self, mock_redis_class):
        """Test updating job status to completed with result."""
        mock_redis = MagicMock()
        mock_redis.get_json.return_value = {
            "job_id": "job_123",
            "status": "processing",
        }
        mock_redis_class.return_value = mock_redis

        result_data = {"narrative": "Test result"}
        _update_job_status("job_123", JobStatus.COMPLETED, result=result_data)

        call_args = mock_redis.set_json.call_args
        updated_data = call_args[0][1]

        assert updated_data["status"] == "completed"
        assert updated_data["completed_at"] is not None
        assert updated_data["result"] == result_data

    @patch('app.tasks.ai_tasks.RedisService')
    def test_update_job_status_to_failed(self, mock_redis_class):
        """Test updating job status to failed with error."""
        mock_redis = MagicMock()
        mock_redis.get_json.return_value = {
            "job_id": "job_123",
            "status": "processing",
        }
        mock_redis_class.return_value = mock_redis

        _update_job_status("job_123", JobStatus.FAILED, error="Something went wrong")

        call_args = mock_redis.set_json.call_args
        updated_data = call_args[0][1]

        assert updated_data["status"] == "failed"
        assert updated_data["error"] == "Something went wrong"

    @patch('app.tasks.ai_tasks.RedisService')
    def test_store_job_result(self, mock_redis_class):
        """Test storing job result."""
        mock_redis = MagicMock()
        mock_redis_class.return_value = mock_redis

        result_data = {
            "narrative": "You discover a hidden passage...",
            "tokens_used": 500,
        }

        _store_job_result("job_123", result_data)

        mock_redis.set_json.assert_called_once()
        call_args = mock_redis.set_json.call_args

        assert call_args[0][0] == "ai_job_result:job_123"
        assert call_args[0][1] == result_data
        assert call_args[1]["ttl"] == 3600  # 1 hour


class TestTaskTypes:
    """Test task type and priority enums."""

    def test_task_type_values(self):
        """Test all task types are defined."""
        assert TaskType.NARRATIVE.value == "narrative"
        assert TaskType.COMBAT.value == "combat"
        assert TaskType.QUEST_SELECTION.value == "quest_selection"
        assert TaskType.NPC_DIALOGUE.value == "npc_dialogue"

    def test_task_priority_values(self):
        """Test all priorities are defined."""
        assert TaskPriority.LOW.value == "low"
        assert TaskPriority.NORMAL.value == "normal"
        assert TaskPriority.HIGH.value == "high"

    def test_job_status_values(self):
        """Test all job statuses are defined."""
        assert JobStatus.QUEUED.value == "queued"
        assert JobStatus.PROCESSING.value == "processing"
        assert JobStatus.COMPLETED.value == "completed"
        assert JobStatus.FAILED.value == "failed"
