# region OrthogonalTranslationFixes [TYPE Module] [SEMANTICS test, translate, orthogonal, verification]
# @BRIEF Orthogonal verification of translation system fixes: cancel lock timeout, null content, periodic commit, async->def migration.
# @LAYER Test
# @RELATION BINDS_TO -> [TranslationOrchestrator]
# @RELATION BINDS_TO -> [TranslationExecutor]
# @RELATION BINDS_TO -> [TranslateRunRoutesModule]
# @TEST_EDGE  cancel_lock_timeout -> lock_timeout expiry falls back to CANCEL_REQUESTED flag
# @TEST_EDGE  cancel_nonexistent_run -> raises ValueError for missing run
# @TEST_EDGE  cancel_pending_run -> PENDING runs can be cancelled
# @TEST_EDGE  cancel_completed_at_set -> completed_at is populated after cancel
# @TEST_EDGE  cancel_event_log -> RUN_CANCELLED event exists in event log
# @TEST_EDGE  execute_cancelled_after_executor -> orchestrator handles CANCELLED status from executor
# @TEST_EDGE  execute_zero_rows -> orchestrator handles zero-row completion
# @TEST_EDGE  async_to_def_routes -> sync route handlers work with FastAPI TestClient
# @TEST_EDGE  no_await_in_routes -> no accidental await in sync handlers

from datetime import UTC, datetime
import pytest
from unittest.mock import MagicMock, patch

from src.models.translate import (
    TranslationJob,
    TranslationRun,
)
from src.plugins.translate.executor import TranslationExecutor
from src.plugins.translate.orchestrator import TranslationOrchestrator


# region periodic_fixtures [TYPE Block]
# @BRIEF Local fixtures for periodic commit tests (not shared via conftest).
@pytest.fixture
def mock_job() -> MagicMock:
    job = MagicMock(spec=TranslationJob)
    job.id = "job-periodic-1"
    job.name = "Periodic Test Job"
    job.status = "ACTIVE"
    job.source_dialect = "postgresql"
    job.target_dialect = "postgresql"
    job.database_dialect = "postgresql"
    job.source_datasource_id = None
    job.translation_column = "name"
    job.context_columns = ["category"]
    job.target_schema = "public"
    job.target_table = "translated_data"
    job.target_key_cols = ["id"]
    job.source_key_cols = ["id"]
    job.source_table = "source_data"
    job.target_languages = ["en"]
    job.provider_id = "provider-1"
    job.batch_size = 50
    job.upsert_strategy = "MERGE"
    job.target_column = None
    return job


@pytest.fixture
def mock_run() -> MagicMock:
    run = MagicMock(spec=TranslationRun)
    run.id = "run-periodic-1"
    run.job_id = "job-periodic-1"
    run.status = "PENDING"
    run.started_at = None
    run.completed_at = None
    run.error_message = None
    run.total_records = 0
    run.successful_records = 0
    run.failed_records = 0
    run.skipped_records = 0
    run.trigger_type = "manual"
    run.config_snapshot = {"full_translation": False}
    return run
# endregion periodic_fixtures


# region TestCancelRunOrthogonal [TYPE Class]
# @BRIEF Orthogonal tests for cancel_run that are NOT covered by existing test_cancel_run.
class TestCancelRunOrthogonal:
    """Verify cancel_run edge cases: lock timeout, nonexistent run, PENDING status, completed_at, event log."""

    # region test_cancel_run_pending_status [C:2] [TYPE Function]
    # @BRIEF PENDING runs can be cancelled (only COMPLETED/FAILED/CANCELLED should be rejected).
    def test_cancel_run_pending_status(self) -> None:
        db = MagicMock()
        config_manager = MagicMock()

        run = MagicMock(spec=TranslationRun)
        run.id = "run-pending-1"
        run.job_id = "job-1"
        run.status = "PENDING"

        db.query.return_value.filter.return_value.first.side_effect = [run, None]

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        result = orch.cancel_run("run-pending-1")

        assert result.status == "CANCELLED"
    # endregion test_cancel_run_pending_status

    # region test_cancel_run_nonexistent_run [C:2] [TYPE Function]
    # @BRIEF cancel_run with non-existent run_id raises ValueError.
    def test_cancel_run_nonexistent_run(self) -> None:
        db = MagicMock()
        config_manager = MagicMock()

        # Query returns None — run does not exist
        db.query.return_value.filter.return_value.first.return_value = None

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        with pytest.raises(ValueError, match="not found"):
            orch.cancel_run("run-nonexistent")
    # endregion test_cancel_run_nonexistent_run

    # region test_cancel_run_completed_at_set [C:2] [TYPE Function]
    # @BRIEF After cancel, completed_at is populated (not None).
    def test_cancel_run_completed_at_set(self) -> None:
        db = MagicMock()
        config_manager = MagicMock()

        run = MagicMock(spec=TranslationRun)
        run.id = "run-ts-1"
        run.job_id = "job-1"
        run.status = "RUNNING"
        run.completed_at = None  # Initially None

        db.query.return_value.filter.return_value.first.side_effect = [run, None]

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        result = orch.cancel_run("run-ts-1")

        assert result.completed_at is not None, "completed_at should be set after cancel"
        assert result.status == "CANCELLED"
    # endregion test_cancel_run_completed_at_set

    # region test_cancel_run_event_log_recorded [C:2] [TYPE Function]
    # @BRIEF After cancel, event_log.log_event is called with RUN_CANCELLED.
    def test_cancel_run_event_log_recorded(self) -> None:
        db = MagicMock()
        config_manager = MagicMock()

        run = MagicMock(spec=TranslationRun)
        run.id = "run-evt-1"
        run.job_id = "job-1"
        run.status = "RUNNING"

        db.query.return_value.filter.return_value.first.side_effect = [run, None]

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        with patch.object(orch.event_log, "log_event") as mock_log_event:
            result = orch.cancel_run("run-evt-1")

            mock_log_event.assert_called_once()
            call_kwargs = mock_log_event.call_args
            assert call_kwargs.kwargs.get("event_type") == "RUN_CANCELLED" or \
                   (len(call_kwargs.args) >= 3 and call_kwargs.args[2] == "RUN_CANCELLED"), \
                f"Expected RUN_CANCELLED event, got {call_kwargs}"
    # endregion test_cancel_run_event_log_recorded

    # region test_cancel_run_lock_timeout_fallback [C:2] [TYPE Function]
    # @BRIEF When SET LOCAL lock_timeout causes exception (row locked), fallback sets CANCEL_REQUESTED flag.
    def test_cancel_run_lock_timeout_fallback(self) -> None:
        """Simulate row lock timeout: SET LOCAL raises, then direct SQL UPDATE is used."""
        db = MagicMock()
        config_manager = MagicMock()

        # First call to db.execute (SET LOCAL) raises OperationalError
        from sqlalchemy.exc import OperationalError
        mock_conn = MagicMock()
        mock_conn.execute.side_effect = OperationalError("lock timeout", {}, None)

        # After the exception path, query should return a run
        run_after_flag = MagicMock(spec=TranslationRun)
        run_after_flag.id = "run-lock-1"
        run_after_flag.job_id = "job-1"
        run_after_flag.status = "RUNNING"
        run_after_flag.error_message = None

        # Sequence: execute(SET LOCAL) raises, execute(UPDATE) succeeds,
        # then query returns the run
        call_count = [0]
        def execute_side_effect(stmt_or_text, *args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                # SET LOCAL — raise
                raise OperationalError("lock timeout", {}, None)
            # UPDATE — succeed
            return MagicMock()

        db.execute.side_effect = execute_side_effect
        db.query.return_value.filter.return_value.first.return_value = run_after_flag

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        result = orch.cancel_run("run-lock-1")

        # The fallback path should have been taken
        assert db.execute.call_count >= 2, (
            f"Expected at least 2 db.execute calls (SET LOCAL + UPDATE), got {db.execute.call_count}"
        )
        assert result.status == "RUNNING"  # Run is returned with flag set, not directly cancelled
    # endregion test_cancel_run_lock_timeout_fallback

    # region test_cancel_run_lock_timeout_nonexistent_after_flag [C:2] [TYPE Function]
    # @BRIEF After lock timeout fallback, if run doesn't exist, raises ValueError.
    def test_cancel_run_lock_timeout_nonexistent_after_flag(self) -> None:
        from sqlalchemy.exc import OperationalError

        db = MagicMock()
        config_manager = MagicMock()

        call_count = [0]
        def execute_side_effect(stmt_or_text, *args, **kwargs):
            call_count[0] += 1
            if call_count[0] == 1:
                raise OperationalError("lock timeout", {}, None)
            return MagicMock()

        db.execute.side_effect = execute_side_effect
        # After flag set, run not found
        db.query.return_value.filter.return_value.first.return_value = None

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        with pytest.raises(ValueError, match="not found"):
            orch.cancel_run("run-ghost")
    # endregion test_cancel_run_lock_timeout_nonexistent_after_flag

    # region test_cancel_run_flush_lock_timeout_fallback [C:2] [TYPE Function]
    # @BRIEF If row lock happens during flush, fallback sets CANCEL_REQUESTED and does not raise 500.
    def test_cancel_run_flush_lock_timeout_fallback(self) -> None:
        from sqlalchemy.exc import OperationalError

        db = MagicMock()
        config_manager = MagicMock()

        locked_run = MagicMock(spec=TranslationRun)
        locked_run.id = "run-flush-lock-1"
        locked_run.job_id = "job-1"
        locked_run.status = "RUNNING"
        locked_run.error_message = None

        flagged_run = MagicMock(spec=TranslationRun)
        flagged_run.id = "run-flush-lock-1"
        flagged_run.job_id = "job-1"
        flagged_run.status = "RUNNING"
        flagged_run.error_message = "CANCEL_REQUESTED"

        db.query.return_value.filter.return_value.first.side_effect = [locked_run, flagged_run]
        db.flush.side_effect = OperationalError("lock timeout", {}, None)

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        result = orch.cancel_run("run-flush-lock-1")

        db.rollback.assert_called_once()
        assert db.execute.call_count >= 2, "Expected SET LOCAL and fallback UPDATE to run"
        assert result.error_message == "CANCEL_REQUESTED"
        assert result.status == "RUNNING"
    # endregion test_cancel_run_flush_lock_timeout_fallback

    # region test_cancel_run_failed_status_rejected [C:2] [TYPE Function]
    # @BRIEF FAILED runs cannot be cancelled (only PENDING/RUNNING).
    def test_cancel_run_failed_status_rejected(self) -> None:
        db = MagicMock()
        config_manager = MagicMock()

        run = MagicMock(spec=TranslationRun)
        run.id = "run-fail-1"
        run.job_id = "job-1"
        run.status = "FAILED"

        db.query.return_value.filter.return_value.first.side_effect = [run, None]

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        with pytest.raises(ValueError, match="Cannot cancel"):
            orch.cancel_run("run-fail-1")
    # endregion test_cancel_run_failed_status_rejected

    # region test_cancel_run_cancelled_status_rejected [C:2] [TYPE Function]
    # @BRIEF Already CANCELLED runs cannot be cancelled again.
    def test_cancel_run_cancelled_status_rejected(self) -> None:
        db = MagicMock()
        config_manager = MagicMock()

        run = MagicMock(spec=TranslationRun)
        run.id = "run-already-1"
        run.job_id = "job-1"
        run.status = "CANCELLED"

        db.query.return_value.filter.return_value.first.side_effect = [run, None]

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        with pytest.raises(ValueError, match="Cannot cancel"):
            orch.cancel_run("run-already-1")
    # endregion test_cancel_run_cancelled_status_rejected


# endregion TestCancelRunOrthogonal


# region TestExecuteRunCancellation [TYPE Class]
# @BRIEF Orthogonal tests for execute_run: cancelled-after-executor path and zero-row path.
class TestExecuteRunCancellation:
    """Verify execute_run handles CANCELLED status from executor and zero-row scenarios."""

    # region test_execute_run_returns_cancelled [C:2] [TYPE Function]
    # @BRIEF When executor returns a CANCELLED run, orchestrator should not attempt SQL insert.
    def test_execute_run_returns_cancelled(self) -> None:
        db = MagicMock()
        config_manager = MagicMock()

        job = MagicMock(spec=TranslationJob)
        job.id = "job-1"
        job.target_table = "target"
        job.target_languages = ["en"]

        run = MagicMock()
        run.id = "run-c-1"
        run.job_id = "job-1"
        run.status = "PENDING"

        cancelled_run = MagicMock()
        cancelled_run.id = "run-c-1"
        cancelled_run.status = "CANCELLED"
        cancelled_run.total_records = 5
        cancelled_run.successful_records = 3
        cancelled_run.failed_records = 2
        cancelled_run.completed_at = datetime.now(UTC)

        db.query.return_value.filter.return_value.first.side_effect = [job, cancelled_run]

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        engine = orch._runner._executor_engine
        with patch("src.plugins.translate.orchestrator_exec.TranslationExecutor") as MockExec:
            mock_exec = MagicMock()
            mock_exec.execute_run.return_value = cancelled_run
            MockExec.return_value = mock_exec
            with patch.object(engine, "event_log"), \
                 patch.object(engine._aggregator, "update_language_stats"):
                result = orch.execute_run(run)

        # Should NOT attempt SQL generation when executor returned CANCELLED
        assert result.status == "CANCELLED"
    # endregion test_execute_run_returns_cancelled

    # region test_execute_run_zero_rows [C:2] [TYPE Function]
    # @BRIEF Executor with zero source rows returns COMPLETED, orchestrator handles it.
    def test_execute_run_zero_rows(self) -> None:
        db = MagicMock()
        config_manager = MagicMock()

        job = MagicMock(spec=TranslationJob)
        job.id = "job-1"
        job.target_table = "target"
        job.target_languages = ["en"]

        run = MagicMock()
        run.id = "run-0-1"
        run.job_id = "job-1"
        run.status = "PENDING"

        completed_run = MagicMock()
        completed_run.id = "run-0-1"
        completed_run.status = "COMPLETED"
        completed_run.total_records = 0
        completed_run.successful_records = 0
        completed_run.failed_records = 0
        completed_run.completed_at = datetime.now(UTC)
        completed_run.insert_status = "skipped"

        db.query.return_value.filter.return_value.first.side_effect = [job, completed_run]

        orch = TranslationOrchestrator(db, config_manager, "test-user")
        engine = orch._runner._executor_engine
        with patch("src.plugins.translate.orchestrator_exec.TranslationExecutor") as MockExec:
            mock_exec = MagicMock()
            mock_exec.execute_run.return_value = completed_run
            MockExec.return_value = mock_exec
            with patch.object(engine, "event_log"), \
                 patch.object(engine._aggregator, "update_language_stats"):
                result = orch.execute_run(run, skip_insert=True)

        assert result.status == "COMPLETED"
        assert result.total_records == 0
    # endregion test_execute_run_zero_rows


# endregion TestExecuteRunCancellation


# region TestNullContentEdgeCases [TYPE Class]
# @BRIEF Additional null content edge cases beyond what coder #1 wrote.
class TestNullContentEdgeCases:
    """Verify all msg.get("content") or "" edge cases including boolean False and 0."""

    # region test_false_content_coerced [C:2] [TYPE Function]
    # @BRIEF Boolean False content should be coerced to "" (falsy).
    def test_false_content_coerced(self) -> None:
        msg = {"content": False}
        content = msg.get("content") or ""
        assert content == "", f"Expected empty string for False, got {content!r}"

    # region test_zero_content_coerced [C:2] [TYPE Function]
    # @BRIEF Integer 0 content should be coerced to "" (falsy).
    def test_zero_content_coerced(self) -> None:
        msg = {"content": 0}
        content = msg.get("content") or ""
        assert content == "", f"Expected empty string for 0, got {content!r}"

    # region test_whitespace_content_preserved [C:2] [TYPE Function]
    # @BRIEF Whitespace-only content should be preserved (truthy string).
    def test_whitespace_content_preserved(self) -> None:
        msg = {"content": "   "}
        content = msg.get("content") or ""
        assert content == "   ", f"Expected whitespace preserved, got {content!r}"

    # region test_empty_list_content_preserved [C:2] [TYPE Function]
    # @BRIEF List content should be preserved (truthy non-string).
    def test_list_content_preserved(self) -> None:
        msg = {"content": []}
        content = msg.get("content") or ""
        # Empty list is falsy, so it becomes ""
        assert content == "", f"Expected empty string for [], got {content!r}"

    # region test_nested_dict_content_preserved [C:2] [TYPE Function]
    # @BRIEF Nested dict content should be preserved (truthy).
    def test_nested_dict_content_preserved(self) -> None:
        msg = {"content": {"rows": []}}
        content = msg.get("content") or ""
        assert isinstance(content, dict), f"Expected dict preserved, got {type(content)}"

    # region test_none_explicit_check [C:2] [TYPE Function]
    # @BRIEF Explicit None check: verify the old TypeError path is unreachable.
    def test_none_explicit_check(self) -> None:
        """Ensure len(None) TypeError is never raised."""
        msg = {"content": None}
        content = msg.get("content") or ""
        # The old code would do len(content) where content was None → TypeError
        # Now content is "" → len("") is 0, no error
        result = len(content)
        assert result == 0


# endregion TestNullContentEdgeCases


# region TestPeriodicCommitVisibility [TYPE Class]
# @BRIEF Verify that periodic commit after each batch makes status visible to other sessions.
class TestPeriodicCommitVisibility:
    """Verify the periodic commit mechanism: commit after batch, refresh, check flag."""

    # region test_commit_called_after_each_batch [C:2] [TYPE Function]
    # @BRIEF db.commit() is called after each batch processing.
    def test_commit_called_after_each_batch(self, mock_job: MagicMock, mock_run: MagicMock) -> None:
        db = MagicMock()
        config_manager = MagicMock()
        executor = TranslationExecutor(db, config_manager)

        db.query.return_value.filter.return_value.first.return_value = mock_job
        executor._preview_edits_cache = {}

        # Two batches worth of rows
        mock_source_rows = [
            {"row_index": str(i), "source_text": f"text{i}",
             "approved_translation": None,
             "source_object_name": f"Row {i}",
             "source_data": {"id": str(i), "name": f"text{i}"}}
            for i in range(2)
        ]

        with (
            patch.object(executor, '_fetch_source_rows', return_value=mock_source_rows),
            patch.object(executor, '_process_batch', return_value={
                "successful": 1, "failed": 0, "skipped": 0,
            }),
        ):
            db.refresh.side_effect = None
            db.refresh.return_value = None
            executor.execute_run(mock_run)

            # commit is called after each batch (line 172 in executor.py).
            # Final status uses flush(), not commit() — the orchestrator commits.
            # With 2 rows / batch_size=50 → 1 batch → 1 commit from executor.
            assert db.commit.call_count >= 1, (
                f"Expected at least 1 commit (after batch), got {db.commit.call_count}"
            )

    # region test_refresh_called_after_commit [C:2] [TYPE Function]
    # @BRIEF db.refresh(run) is called after each batch commit to re-read cancellation flag.
    def test_refresh_called_after_commit(self, mock_job: MagicMock, mock_run: MagicMock) -> None:
        db = MagicMock()
        config_manager = MagicMock()
        executor = TranslationExecutor(db, config_manager)

        db.query.return_value.filter.return_value.first.return_value = mock_job
        executor._preview_edits_cache = {}

        mock_source_rows = [
            {"row_index": "0", "source_text": "hello",
             "approved_translation": None,
             "source_object_name": "Row 0",
             "source_data": {"id": "0", "name": "hello"}},
        ]

        with (
            patch.object(executor, '_fetch_source_rows', return_value=mock_source_rows),
            patch.object(executor, '_process_batch', return_value={
                "successful": 1, "failed": 0, "skipped": 0,
            }),
        ):
            db.refresh.side_effect = None
            db.refresh.return_value = None
            executor.execute_run(mock_run)

            # refresh should be called after each batch commit
            assert db.refresh.call_count >= 1, (
                f"Expected at least 1 refresh call, got {db.refresh.call_count}"
            )


# endregion TestPeriodicCommitVisibility


# region TestAsyncToDefMigration [TYPE Class]
# @BRIEF Verify that the async->def migration in _run_routes.py works correctly.
class TestAsyncToDefMigration:
    """Verify route handlers are sync and work with FastAPI TestClient."""

    # region test_all_handlers_are_sync [C:2] [TYPE Function]
    # @BRIEF All 11 route handlers should be sync (def, not async def).
    def test_all_handlers_are_sync(self) -> None:
        """Import the router and verify all handler functions are sync."""
        import inspect

        from src.api.routes.translate._run_routes import router

        sync_handlers = []
        async_handlers = []

        target_names = {
            "run_translation", "retry_run", "retry_insert", "cancel_run",
            "get_run_history", "get_run_status", "get_run_records", "get_batches",
            "override_detected_language", "inline_edit_translation", "bulk_find_replace",
        }

        for route in router.routes:
            if hasattr(route, "endpoint"):
                name = route.endpoint.__name__
                if name in target_names:
                    if inspect.iscoroutinefunction(route.endpoint):
                        async_handlers.append(name)
                    else:
                        sync_handlers.append(name)

        assert len(async_handlers) == 0, (
            f"Found async handlers (should be sync): {async_handlers}"
        )
        assert len(sync_handlers) == len(target_names), (
            f"Expected {len(target_names)} sync handlers, found {len(sync_handlers)}: {sync_handlers}"
        )

    # region test_no_await_in_handler_bodies [C:2] [TYPE Function]
    # @BRIEF No `await` keyword should appear in the handler function bodies.
    def test_no_await_in_handler_bodies(self) -> None:
        """Scan handler source code for 'await' — should not appear in sync handlers."""
        import ast
        import inspect

        from src.api.routes.translate._run_routes import router

        target_names = {
            "run_translation", "retry_run", "retry_insert", "cancel_run",
            "get_run_history", "get_run_status", "get_run_records", "get_batches",
            "override_detected_language", "inline_edit_translation", "bulk_find_replace",
        }

        for route in router.routes:
            if hasattr(route, "endpoint"):
                name = route.endpoint.__name__
                if name in target_names:
                    source = inspect.getsource(route.endpoint)
                    tree = ast.parse(source)
                    awaits = [
                        node for node in ast.walk(tree)
                        if isinstance(node, ast.Await)
                    ]
                    assert len(awaits) == 0, (
                        f"Handler '{name}' contains {len(awaits)} await expressions"
                    )

    # region test_cancel_run_route_accessible [C:2] [TYPE Function]
    # @BRIEF Verify cancel_run route is registered and accessible via the router.
    def test_cancel_run_route_accessible(self) -> None:
        from src.api.routes.translate._run_routes import router

        cancel_routes = [
            r for r in router.routes
            if hasattr(r, "path") and "cancel" in r.path
        ]
        assert len(cancel_routes) == 1, (
            f"Expected 1 cancel route, found {len(cancel_routes)}"
        )
        assert cancel_routes[0].methods == {"POST"}, (
            f"Expected POST method for cancel, got {cancel_routes[0].methods}"
        )

    # region test_run_translation_route_accessible [C:2] [TYPE Function]
    # @BRIEF Verify run_translation route is registered at /jobs/{job_id}/run.
    def test_run_translation_route_accessible(self) -> None:
        from src.api.routes.translate._run_routes import router

        run_routes = [
            r for r in router.routes
            if hasattr(r, "path") and r.path.endswith("/run")
        ]
        assert len(run_routes) >= 1, (
            f"Expected at least 1 /run route, found {len(run_routes)}"
        )


# endregion TestAsyncToDefMigration
