# #region TestTranslateSchedulerAudit [C:3] [TYPE Module] [SEMANTICS test,translate,scheduler,audit,async-sync-mismatch,id-mismatch]
# @BRIEF Orthogonal audit of scheduled translation execution — async/sync mismatch and ID mismatch bugs.
# @RELATION BINDS_TO -> [execute_scheduled_translation]
# @RELATION BINDS_TO -> [TranslationScheduler]
# @RELATION BINDS_TO -> [SchedulerService]
# @RELATION BINDS_TO -> [AsyncJobRunner]
# @TEST_EDGE: async_coroutine_not_awaited — execute_run returns coroutine but never awaited
# @TEST_EDGE: disable_schedule_wrong_id — disable_schedule passes job_id instead of schedule_id
# @TEST_EDGE: delete_schedule_wrong_id — delete_schedule passes job_id instead of schedule_id
# @TEST_INVARIANT: execute_run_must_be_awaited -> VERIFIED_BY: [test_scheduler_uses_runner_for_execute_run]
# @TEST_INVARIANT: remove_job_uses_schedule_id -> VERIFIED_BY: [test_disable_schedule_uses_schedule_id, test_delete_schedule_uses_schedule_id]
import asyncio
import inspect
from pathlib import Path
from unittest.mock import MagicMock, patch, AsyncMock

import pytest

sys_path = str(Path(__file__).parent.parent / "src")
import sys
if sys_path not in sys.path:
    sys.path.insert(0, sys_path)


# =============================================================================
# BUG #1 FIX: async/sync mismatch — now uses AsyncJobRunner
# =============================================================================

class TestAsyncSyncMismatch:
    """Verify that execute_run is async and uses AsyncJobRunner."""

    # #region test_execute_run_is_async_coroutine [C:2] [TYPE Function]
    # @BRIEF Confirm execute_run is an async method that returns a coroutine.
    def test_execute_run_is_async_coroutine(self):
        """execute_run is defined as async def — calling it without await returns a coroutine object."""
        from src.plugins.translate.orchestrator import TranslationOrchestrator

        # Verify the method is actually async
        assert inspect.iscoroutinefunction(TranslationOrchestrator.execute_run), (
            "TranslationOrchestrator.execute_run must be an async method"
        )
    # #endregion test_execute_run_is_async_coroutine

    # #region test_scheduler_uses_runner_for_execute_run [C:2] [TYPE Function]
    # @BRIEF The scheduler uses AsyncJobRunner.run() to execute translations (FIXED).
    def test_scheduler_uses_runner_for_execute_run(self):
        """In execute_scheduled_translation, orch.execute_run(run) is called via runner.run()."""
        import ast
        import inspect

        # Read the source code of execute_scheduled_translation
        from src.plugins.translate.scheduler import execute_scheduled_translation
        source = inspect.getsource(execute_scheduled_translation)

        # Parse the AST to find the call to execute_run
        tree = ast.parse(source)

        # Find all Call nodes that call execute_run
        execute_run_calls = []
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Attribute) and node.func.attr == 'execute_run':
                    execute_run_calls.append(node)

        assert len(execute_run_calls) > 0, "No calls to execute_run found in execute_scheduled_translation"

        # Check if any call is wrapped in runner.run()
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Attribute) and node.func.attr == 'run':
                    # Check if the argument is an execute_run call
                    for arg in node.args:
                        if isinstance(arg, ast.Call):
                            if isinstance(arg.func, ast.Attribute) and arg.func.attr == 'execute_run':
                                # Found runner.run(orch.execute_run(run)) — this is correct
                                return

        pytest.fail(
            "FIX NOT APPLIED: execute_run should be called via runner.run() in execute_scheduled_translation. "
            "Expected: runner.run(orch.execute_run(run))"
        )
    # #endregion test_scheduler_uses_runner_for_execute_run

    # #region test_manual_trigger_uses_asyncio_create_task [C:2] [TYPE Function]
    # @BRIEF Manual trigger correctly uses asyncio.create_task for async execution.
    def test_manual_trigger_uses_asyncio_create_task(self):
        """The manual trigger route uses asyncio.create_task() which correctly handles async execution."""
        import ast
        import inspect
        from src.api.routes.translate._run_routes import run_translation

        source = inspect.getsource(run_translation)
        tree = ast.parse(source)

        # Look for asyncio.create_task calls
        has_create_task = False
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Attribute):
                    if node.func.attr == 'create_task':
                        has_create_task = True
                elif isinstance(node.func, ast.Name):
                    if node.func.id == 'create_task':
                        has_create_task = True

        assert has_create_task, (
            "Manual trigger should use asyncio.create_task() for async execution"
        )
    # #endregion test_manual_trigger_uses_asyncio_create_task


# =============================================================================
# BUG #2: ID mismatch — disable_schedule and delete_schedule pass wrong ID
# =============================================================================

class TestScheduleIdMismatch:
    """Verify that disable_schedule and delete_schedule use correct IDs."""

    # #region test_add_translation_job_uses_schedule_id [C:2] [TYPE Function]
    # @BRIEF add_translation_job constructs APScheduler job ID from schedule_id.
    def test_add_translation_job_uses_schedule_id(self):
        """add_translation_job creates job ID as 'translate_{schedule_id}'."""
        from src.core.scheduler import SchedulerService

        mock_task_manager = MagicMock()
        mock_config = MagicMock()

        svc = SchedulerService(mock_task_manager, mock_config)
        svc.scheduler = MagicMock()

        schedule_id = "sched-uuid-123"
        job_id = "job-uuid-456"

        svc.add_translation_job(
            schedule_id=schedule_id,
            job_id=job_id,
            cron_expression="*/30 * * * *",
            timezone="UTC",
            execution_mode="full"
        )

        # Verify the APScheduler job ID is based on schedule_id
        call_args = svc.scheduler.add_job.call_args
        aps_job_id = call_args.kwargs.get('id') or call_args.args[1] if len(call_args.args) > 1 else None

        assert aps_job_id == f"translate_{schedule_id}", (
            f"APScheduler job ID should be 'translate_{schedule_id}', got '{aps_job_id}'"
        )
    # #endregion test_add_translation_job_uses_schedule_id

    # #region test_remove_translation_job_uses_schedule_id [C:2] [TYPE Function]
    # @BRIEF remove_translation_job constructs APScheduler job ID from schedule_id.
    def test_remove_translation_job_uses_schedule_id(self):
        """remove_translation_job removes job with ID 'translate_{schedule_id}'."""
        from src.core.scheduler import SchedulerService

        mock_task_manager = MagicMock()
        mock_config = MagicMock()

        svc = SchedulerService(mock_task_manager, mock_config)
        svc.scheduler = MagicMock()

        schedule_id = "sched-uuid-123"
        svc.remove_translation_job(schedule_id=schedule_id)

        # Verify it tries to remove the correct job ID
        svc.scheduler.remove_job.assert_called_once_with(f"translate_{schedule_id}")
    # #endregion test_remove_translation_job_uses_schedule_id

    # #region test_disable_schedule_uses_schedule_id [C:2] [TYPE Function]
    # @BRIEF disable_schedule uses schedule.id (not job_id) for remove_translation_job (FIXED).
    def test_disable_schedule_uses_schedule_id(self):
        """FIXED: disable_schedule route uses schedule.id for remove_translation_job."""
        import ast
        import inspect
        from src.api.routes.translate._schedule_routes import disable_schedule

        source = inspect.getsource(disable_schedule)
        tree = ast.parse(source)

        # Find calls to remove_translation_job
        remove_calls = []
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Attribute) and node.func.attr == 'remove_translation_job':
                    remove_calls.append(node)

        assert len(remove_calls) > 0, "No calls to remove_translation_job found"

        # Check what argument is passed — should NOT be job_id directly
        for call in remove_calls:
            for kw in call.keywords:
                if kw.arg == 'schedule_id':
                    # Should NOT be job_id (the parameter name)
                    if isinstance(kw.value, ast.Name) and kw.value.id == 'job_id':
                        pytest.fail(
                            "BUG STILL EXISTS: disable_schedule passes job_id to remove_translation_job. "
                            "Expected: schedule.id from set_schedule_active() return value."
                        )
    # #endregion test_disable_schedule_uses_schedule_id

    # #region test_delete_schedule_uses_schedule_id [C:2] [TYPE Function]
    # @BRIEF delete_schedule uses schedule.id (not job_id) for remove_translation_job (FIXED).
    def test_delete_schedule_uses_schedule_id(self):
        """FIXED: delete_schedule route uses schedule.id for remove_translation_job."""
        import ast
        import inspect
        from src.api.routes.translate._schedule_routes import delete_schedule

        source = inspect.getsource(delete_schedule)
        tree = ast.parse(source)

        # Find calls to remove_translation_job
        remove_calls = []
        for node in ast.walk(tree):
            if isinstance(node, ast.Call):
                if isinstance(node.func, ast.Attribute) and node.func.attr == 'remove_translation_job':
                    remove_calls.append(node)

        assert len(remove_calls) > 0, "No calls to remove_translation_job found"

        # Check what argument is passed — should NOT be job_id directly
        for call in remove_calls:
            for kw in call.keywords:
                if kw.arg == 'schedule_id':
                    # Should NOT be job_id (the parameter name)
                    if isinstance(kw.value, ast.Name) and kw.value.id == 'job_id':
                        pytest.fail(
                            "BUG STILL EXISTS: delete_schedule passes job_id to remove_translation_job. "
                            "Expected: schedule_id from get_schedule() return value."
                        )
    # #endregion test_delete_schedule_uses_schedule_id

    # #region test_schedule_id_differs_from_job_id [C:2] [TYPE Function]
    # @BRIEF TranslationSchedule.id and TranslationSchedule.job_id are different UUIDs.
    def test_schedule_id_differs_from_job_id(self):
        """TranslationSchedule has separate id and job_id fields — they are different UUIDs."""
        from src.models.translate import TranslationSchedule

        # Verify the model has both fields
        assert hasattr(TranslationSchedule, 'id'), "TranslationSchedule should have 'id' field"
        assert hasattr(TranslationSchedule, 'job_id'), "TranslationSchedule should have 'job_id' field"

        # The id is the primary key, job_id is a foreign key to TranslationJob
        # They are different UUIDs
        import uuid
        schedule_id = str(uuid.uuid4())
        job_id = str(uuid.uuid4())

        assert schedule_id != job_id, (
            "schedule_id and job_id are different UUIDs — "
            "using job_id where schedule_id is expected will cause ID mismatch"
        )
    # #endregion test_schedule_id_differs_from_job_id


# =============================================================================
# Integration verification: end-to-end scheduled execution flow
# =============================================================================

class TestScheduledExecutionFlow:
    """Verify the complete scheduled execution flow has the async/sync bug."""

    # #region test_scheduled_execution_uses_runner [C:2] [TYPE Function]
    # @BRIEF Full flow: scheduler triggers execute_run via AsyncJobRunner (FIXED).
    def test_scheduled_execution_uses_runner(self):
        """
        Simulate what happens when APScheduler triggers execute_scheduled_translation:
        1. TranslationOrchestrator is created
        2. start_run creates a TranslationRun
        3. execute_run is called via runner.run()
        4. The translation actually executes
        """
        from src.plugins.translate.scheduler import execute_scheduled_translation
        from src.plugins.translate.orchestrator import TranslationOrchestrator

        mock_db = MagicMock()
        mock_session_maker = MagicMock(return_value=mock_db)
        mock_config = MagicMock()

        # Setup schedule
        schedule = MagicMock()
        schedule.id = "sched-1"
        schedule.job_id = "job-1"
        schedule.is_active = True
        schedule.execution_mode = "full"

        q1 = MagicMock()
        q1.filter.return_value.first.return_value = schedule

        q2 = MagicMock()
        q2.filter.return_value.order_by.return_value.first.return_value = None  # no concurrent

        q3 = MagicMock()
        q3.filter.return_value.order_by.return_value.first.return_value = None  # no recent run

        mock_db.query.side_effect = [q1, q2, q3]

        # Setup orchestrator mock
        mock_run = MagicMock()
        mock_run.id = "run-1"
        mock_run.status = "PENDING"

        # Mock the runner to track calls
        mock_runner = MagicMock()

        with patch.object(TranslationOrchestrator, 'execute_run', return_value=mock_run):
            with patch.object(TranslationOrchestrator, 'start_run', return_value=mock_run):
                with patch('src.dependencies.get_async_job_runner', return_value=mock_runner):
                    execute_scheduled_translation(
                        schedule_id="sched-1",
                        job_id="job-1",
                        db_session_maker=mock_session_maker,
                        config_manager=mock_config,
                        execution_mode="full",
                    )

        # Verify that runner.run was called (the fix is applied)
        mock_runner.run.assert_called_once()
    # #endregion test_scheduled_execution_uses_runner
