#!/usr/bin/env python3
"""
Operator-client regression tests.

These tests validate the client contract using a mocked Celery runtime so the
suite stays deterministic and fast.
"""

from __future__ import annotations

import importlib.util
import asyncio
import sys
import time
import types
from pathlib import Path
from unittest.mock import Mock, patch

TESTS_DIR = Path(__file__).parent
REPO_ROOT = TESTS_DIR.parent
sys.path.insert(0, str(REPO_ROOT))


def _module_missing(module_name: str) -> bool:
    try:
        return importlib.util.find_spec(module_name) is None
    except (ModuleNotFoundError, ValueError, AttributeError):
        return module_name not in sys.modules


def install_client_dependency_stubs() -> None:
    if _module_missing("celery"):
        celery_mod = types.ModuleType("celery")

        class DummyCelery:
            def __init__(self, *args, **kwargs):
                self.conf = {}

            def send_task(self, *args, **kwargs):
                raise RuntimeError("Celery stub should be patched in tests")

        celery_mod.Celery = DummyCelery
        sys.modules["celery"] = celery_mod

    if _module_missing("celery.result"):
        celery_result_mod = types.ModuleType("celery.result")

        class AsyncResult:
            def __init__(self, *args, **kwargs):
                pass

        celery_result_mod.AsyncResult = AsyncResult
        sys.modules["celery.result"] = celery_result_mod


install_client_dependency_stubs()

from sandai.operator_client import OperatorClient, create_client
from sandai.operator_client.client import AsyncOperatorClient, create_async_client
from sandai.operator_client.config import VALID_PRIORITIES, env_bool, resolve_client_config
from sandai.operator_client.exceptions import (
    InvalidPriorityError,
    InvalidResultFormatError,
    InvalidTaskStatusError,
    TaskCancelledError,
    TaskDeadlineExceededError,
    TaskExecutionError,
    TaskTimeoutError,
)
from sandai.operator_client.types import AsyncSubmittedTask, AsyncTaskHandle
from sandai.operator_client.protocol import (
    build_task_payload,
    inspect_probe_result,
    raise_task_error,
)
from sandai.operator_client.transport import CeleryTransport
from sandai.operator_client.types import BatchTaskResult, OperatorDescription, OperatorTaskResult, SubmittedTask


def _mock_client_runtime():
    mock_config = Mock()
    mock_config.broker_url = "redis://localhost:6379/0"
    mock_config.result_backend = "redis://localhost:6379/0"
    return patch("sandai.operator_client.transport.Celery"), patch(
        "sandai.operator_client.client.get_celery_config", return_value=mock_config
    ), patch(
        "sandai.operator_client.client.get_default_celery_config",
        return_value=mock_config,
    )


def test_create_client_and_default_timeout() -> None:
    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_celery.return_value = Mock()
        client = OperatorClient("test-operator", "v1")
        created = create_client("test-operator", "v1")

    assert client.timeout == 28800
    assert created.timeout == 28800
    assert client.get_queue_info()["normal"] == "normal.test-operator.v1"


def test_resolve_client_config_honors_explicit_values_and_env_defaults() -> None:
    with patch.dict(
        "os.environ",
        {
            "SANDAI_OPERATOR_CELERY_BROKER_URL": "redis://env-broker/1",
            "SANDAI_OPERATOR_CELERY_RESULT_BACKEND": "redis://env-backend/2",
            "SANDAI_OPERATOR_AUTO_FORGET_RESULT": "0",
        },
        clear=False,
    ):
        explicit = resolve_client_config(
            "demo-operator",
            "v2",
            broker_url="redis://explicit-broker/3",
            auto_forget_result=None,
        )
        env_backed = resolve_client_config("demo-operator", "v2")

    assert explicit.broker_url == "redis://explicit-broker/3"
    assert explicit.result_backend == "redis://explicit-broker/3"
    assert explicit.auto_forget_result is False
    assert explicit.celery.task_serializer == "msgpack"
    assert explicit.celery.result_serializer == "json"
    assert explicit.celery.accept_content == ["json", "msgpack"]
    assert explicit.task_name == "sandai_operator.demo-operator.v2.process_task"
    assert explicit.app_name == "sandai_client_demo-operator_v2"
    assert explicit.queue_names == {
        priority: f"{priority}.demo-operator.v2" for priority in VALID_PRIORITIES
    }

    assert env_backed.broker_url == "redis://env-broker/1"
    assert env_backed.result_backend == "redis://env-backend/2"


def test_resolve_client_config_honors_env_serializers() -> None:
    with patch.dict(
        "os.environ",
        {
            "SANDAI_OPERATOR_CELERY_BROKER_URL": "redis://env-broker/1",
            "SANDAI_OPERATOR_CELERY_RESULT_BACKEND": "redis://env-backend/2",
            "SANDAI_OPERATOR_CELERY_TASK_SERIALIZER": "json",
            "SANDAI_OPERATOR_CELERY_RESULT_SERIALIZER": "msgpack",
        },
        clear=False,
    ):
        config = resolve_client_config("demo-operator", "v2")

    assert config.celery.task_serializer == "json"
    assert config.celery.result_serializer == "msgpack"
    assert config.celery.accept_content == ["json", "msgpack"]


def test_env_bool_parses_truthy_and_falsy_values() -> None:
    with patch.dict("os.environ", {"BOOL_FLAG": "YeS"}, clear=False):
        assert env_bool("BOOL_FLAG") is True

    with patch.dict("os.environ", {"BOOL_FLAG": " 0 "}, clear=False):
        assert env_bool("BOOL_FLAG", default=True) is False

    with patch.dict("os.environ", {}, clear=True):
        assert env_bool("BOOL_FLAG", default=True) is True


def test_invalid_priority_raises_domain_error() -> None:
    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_celery.return_value = Mock()
        try:
            OperatorClient("test-operator", "v1", priority="urgent")
            raise AssertionError("expected InvalidPriorityError")
        except InvalidPriorityError as exc:
            assert exc.priority == "urgent"
            assert "normal" in exc.valid_priorities


def test_async_call_sets_deadline_cancel_key_and_persistent_output() -> None:
    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        mock_result = Mock()
        mock_result.id = "task-123"
        mock_app.send_task.return_value = mock_result
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1", timeout=300, cancel_key="default-session")
        started = time.time()
        client.async_call(
            input_artifacts=[{"uri": "file://input.txt", "slot": "input"}],
            options={"mode": "fast"},
            timeout=120,
            cancel_key="override-session",
            persistent_output=True,
        )

        kwargs = mock_app.send_task.call_args.kwargs["kwargs"]
        queue = mock_app.send_task.call_args.kwargs["queue"]

    assert queue == "normal.test-operator.v1"
    assert kwargs["cancel_key"] == "override-session"
    assert kwargs["meta"]["persistent-output"] is True
    assert abs(kwargs["deadline"] - (started + 120)) < 1.0


def test_build_task_payload_omits_deadline_when_no_timeout_is_effective() -> None:
    payload = build_task_payload(
        [{"uri": "file://input.txt"}],
        {"mode": "fast"},
        timeout=None,
        default_timeout=0,
        cancel_key=None,
        default_cancel_key="session-1",
        persistent_output=False,
    )

    assert "deadline" not in payload
    assert payload["cancel_key"] == "session-1"
    assert payload["meta"]["persistent-output"] is False


def test_sync_success_path_returns_artifacts_and_results() -> None:
    success_result = {
        "status": "success",
        "artifacts": [{"uri": "fake://output.txt"}],
        "results": {"ok": True},
    }

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        async_result = Mock()
        async_result.id = "task-success"
        async_result.get.return_value = success_result
        mock_app.send_task.return_value = async_result
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1")
        artifacts, results = client.sync([], {})

    assert artifacts == [{"uri": "fake://output.txt"}]
    assert results == {"ok": True}
    async_result.forget.assert_called_once()


def test_sync_maps_deadline_cancel_and_generic_errors() -> None:
    deadline_result = {
        "status": "error",
        "error": {
            "error_type": "TaskDeadlineExceededError",
            "error_message": "deadline exceeded",
            "error_details": {"deadline": 100.0, "current_time": 101.5},
        },
    }
    cancel_result = {
        "status": "error",
        "error": {
            "error_type": "TaskCancelledError",
            "error_message": "cancelled",
            "error_details": {"cancel_key": "cancel:user-1"},
        },
    }
    generic_result = {
        "status": "error",
        "error": {
            "error_type": "ProcessingError",
            "error_message": "boom",
            "error_details": {"stage": "compute"},
        },
    }

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        mock_celery.return_value = mock_app

        deadline_async = Mock(id="deadline-task")
        deadline_async.get.return_value = deadline_result
        cancel_async = Mock(id="cancel-task")
        cancel_async.get.return_value = cancel_result
        generic_async = Mock(id="generic-task")
        generic_async.get.return_value = generic_result
        mock_app.send_task.side_effect = [deadline_async, cancel_async, generic_async]

        client = OperatorClient("test-operator", "v1")

        try:
            client.sync([], {})
            raise AssertionError("expected TaskDeadlineExceededError")
        except TaskDeadlineExceededError as exc:
            assert exc.task_id == "deadline-task"

        try:
            client.sync([], {})
            raise AssertionError("expected TaskCancelledError")
        except TaskCancelledError as exc:
            assert exc.task_id == "cancel-task"

        try:
            client.sync([], {})
            raise AssertionError("expected TaskExecutionError")
        except TaskExecutionError as exc:
            assert exc.task_id == "generic-task"
            assert exc.error_details == {"stage": "compute"}


def test_sync_raises_invalid_status_and_invalid_format() -> None:
    unknown_status = {"status": "partial"}
    invalid_format = ["not", "a", "dict"]

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        mock_celery.return_value = mock_app

        status_async = Mock(id="status-task")
        status_async.get.return_value = unknown_status
        format_async = Mock(id="format-task")
        format_async.get.return_value = invalid_format
        mock_app.send_task.side_effect = [status_async, format_async]

        client = OperatorClient("test-operator", "v1")

        try:
            client.sync([], {})
            raise AssertionError("expected InvalidTaskStatusError")
        except InvalidTaskStatusError as exc:
            assert exc.task_id == "status-task"
            assert exc.status == "partial"

        try:
            client.sync([], {})
            raise AssertionError("expected InvalidResultFormatError")
        except InvalidResultFormatError as exc:
            assert exc.task_id == "format-task"


def test_sync_maps_timeout_exceptions() -> None:
    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        async_result = Mock(id="timeout-task")
        async_result.get.side_effect = RuntimeError("operation timed out")
        mock_app.send_task.return_value = async_result
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1", timeout=5)
        try:
            client.sync([], {})
            raise AssertionError("expected TaskTimeoutError")
        except TaskTimeoutError as exc:
            assert exc.task_id == "timeout-task"
            assert exc.timeout == 5


def test_transport_get_result_keeps_envelopes_and_raises_backend_exceptions() -> None:
    transport = object.__new__(CeleryTransport)

    success_async = Mock()
    success_async.get.return_value = {"status": "error", "error": {"error_type": "ProcessingError"}}
    assert transport.get_result(success_async, timeout=9) == {
        "status": "error",
        "error": {"error_type": "ProcessingError"},
    }
    success_async.get.assert_called_once_with(timeout=9, propagate=False)

    failure_async = Mock()
    failure_async.get.return_value = RuntimeError("backend exploded")
    try:
        transport.get_result(failure_async, timeout=3)
        raise AssertionError("expected backend exception to be re-raised")
    except RuntimeError as exc:
        assert str(exc) == "backend exploded"


def test_raise_task_error_normalizes_non_mapping_payloads() -> None:
    try:
        raise_task_error("task-non-mapping", ["broken", "payload"])
        raise AssertionError("expected TaskExecutionError")
    except TaskExecutionError as exc:
        assert exc.task_id == "task-non-mapping"
        assert exc.error_message == "['broken', 'payload']"
        assert exc.error_details == {}


def test_raise_task_error_wraps_non_dict_error_details() -> None:
    try:
        raise_task_error(
            "task-details",
            {
                "error_type": "ProcessingError",
                "error_message": "bad details",
                "error_details": ["step-1", "step-2"],
            },
        )
        raise AssertionError("expected TaskExecutionError")
    except TaskExecutionError as exc:
        assert exc.error_details == {"raw_error_details": ["step-1", "step-2"]}


def test_desc_is_cached_and_forgets_result() -> None:
    desc_result = {
        "status": "success",
        "doc": "hello",
        "options_schema": {"type": "object"},
        "results_schema": {"type": "object"},
    }

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        async_result = Mock(id="desc-task")
        async_result.get.return_value = desc_result
        mock_app.send_task.return_value = async_result
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1")
        first = client.desc
        second = client.desc

    assert first == second
    assert first["doc"] == "hello"
    assert mock_app.send_task.call_count == 1
    async_result.forget.assert_called_once()


def test_desc_rejects_invalid_status() -> None:
    invalid_desc = {"status": "partial", "doc": "hello"}

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        async_result = Mock(id="desc-invalid")
        async_result.get.return_value = invalid_desc
        mock_app.send_task.return_value = async_result
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1")
        try:
            _ = client.desc
            raise AssertionError("expected InvalidTaskStatusError")
        except InvalidTaskStatusError as exc:
            assert exc.task_id == "desc-invalid"
            assert exc.status == "partial"

    async_result.forget.assert_called_once()


def test_desc_maps_timeout_and_error_payloads() -> None:
    error_desc = {
        "status": "error",
        "error": {
            "error_type": "ProcessingError",
            "error_message": "desc failed",
            "error_details": {"stage": "describe"},
        },
    }

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        timeout_async = Mock(id="desc-timeout")
        timeout_async.get.side_effect = RuntimeError("backend timeout")
        error_async = Mock(id="desc-error")
        error_async.get.return_value = error_desc
        mock_app.send_task.side_effect = [timeout_async, error_async]
        mock_celery.return_value = mock_app

        timeout_client = OperatorClient("test-operator", "v1", timeout=7)
        try:
            _ = timeout_client.desc
            raise AssertionError("expected TaskTimeoutError")
        except TaskTimeoutError as exc:
            assert exc.task_id == "desc-timeout"
            assert exc.timeout == 7

        error_client = OperatorClient("test-operator", "v1")
        try:
            _ = error_client.desc
            raise AssertionError("expected TaskExecutionError")
        except TaskExecutionError as exc:
            assert exc.task_id == "desc-error"
            assert exc.error_details == {"stage": "describe"}

    timeout_async.forget.assert_called_once()
    error_async.forget.assert_called_once()


def test_types_expose_legacy_compatibility_helpers() -> None:
    submitted_ok = SubmittedTask(index=0, async_result=Mock(id="task-1"))
    submitted_fail = SubmittedTask(index=1, error=RuntimeError("submit failed"))
    batch_ok = BatchTaskResult(
        index=0,
        task_id="task-1",
        submitted=True,
        artifacts=[{"uri": "fake://artifact"}],
        results={"ok": True},
    )
    batch_fail = BatchTaskResult(index=1, task_id=None, submitted=False, error=RuntimeError("nope"))
    desc = OperatorDescription(doc="hello", options_schema={"type": "object"}, results_schema=None)
    task_result = OperatorTaskResult(artifacts=[{"uri": "fake://artifact"}], results={"ok": True})

    assert submitted_ok.submitted is True
    assert submitted_ok.task_id == "task-1"
    assert submitted_fail.submitted is False
    assert submitted_fail.task_id is None
    assert batch_ok.success is True
    assert batch_ok.to_legacy_tuple() == ([{"uri": "fake://artifact"}], {"ok": True})
    assert batch_fail.success is False
    assert batch_fail.to_legacy_tuple() == ([], {})
    assert desc.to_dict() == {
        "doc": "hello",
        "options_schema": {"type": "object"},
        "results_schema": None,
    }
    assert task_result.to_legacy_tuple() == ([{"uri": "fake://artifact"}], {"ok": True})


def test_batch_submit_collect_and_probe_expose_structured_status() -> None:
    success_result = {"status": "success", "artifacts": [{"uri": "done"}], "results": {"ok": True}}

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        submit_success = Mock(id="submitted-ok")
        collect_success = Mock(id="collected-ok")
        collect_success.get.return_value = success_result
        probe_success = Mock(id="probe-ok")
        probe_success.get.return_value = success_result
        mock_app.send_task.side_effect = [
            submit_success,
            RuntimeError("send failed"),
            collect_success,
            probe_success,
        ]
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1")
        submissions = client.batch_submit([
            ([{"uri": "1"}], {"i": 1}),
            ([{"uri": "2"}], {"i": 2}),
        ])
        detailed = client.batch_collect([
            submissions[1],
            client.batch_submit([([{"uri": "3"}], {"i": 3})])[0],
        ])
        probe = client.probe(timeout=3)

    assert submissions[0].submitted is True
    assert submissions[0].task_id == "submitted-ok"
    assert submissions[1].submitted is False
    assert isinstance(submissions[1].error, RuntimeError)
    assert detailed[0].success is False
    assert isinstance(detailed[0].error, RuntimeError)
    assert str(detailed[0].error) == "send failed"
    assert detailed[1].success is True
    assert detailed[1].to_legacy_tuple() == ([{"uri": "done"}], {"ok": True})
    assert probe.submit_ok is True
    assert probe.retrieval_ok is True
    assert probe.response_ok is True
    collect_success.forget.assert_called_once()
    probe_success.forget.assert_called_once()


def test_batch_async_and_batch_sync_preserve_order_and_partial_failures() -> None:
    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        mock_celery.return_value = mock_app

        async_only_1 = Mock(id="batch-async-1")
        async_only_2 = Mock(id="batch-async-2")
        sync_1 = Mock(id="batch-sync-1")
        sync_2 = Mock(id="batch-sync-2")
        sync_1.get.return_value = {"status": "success", "artifacts": [{"uri": "a"}], "results": {"i": 1}}
        sync_2.get.return_value = {"status": "success", "artifacts": [{"uri": "b"}], "results": {"i": 2}}

        mock_app.send_task.side_effect = [
            async_only_1,
            RuntimeError("send failed"),
            async_only_2,
            sync_1,
            RuntimeError("send failed"),
            sync_2,
        ]

        client = OperatorClient("test-operator", "v1")
        async_results = client.batch_async([
            ([{"uri": "1"}], {"i": 1}),
            ([{"uri": "2"}], {"i": 2}),
            ([{"uri": "3"}], {"i": 3}),
        ])
        sync_results = client.batch_sync([
            ([{"uri": "1"}], {"i": 1}),
            ([{"uri": "2"}], {"i": 2}),
            ([{"uri": "3"}], {"i": 3}),
        ])

    assert len(async_results) == 3
    assert async_results[1] is None
    assert sync_results == [([{"uri": "a"}], {"i": 1}), ([], {}), ([{"uri": "b"}], {"i": 2})]


def test_sync_and_batch_collect_ignore_forget_failures_after_success() -> None:
    success_result = {"status": "success", "artifacts": [{"uri": "done"}], "results": {"ok": True}}

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        sync_async = Mock(id="sync-forget-fails")
        sync_async.get.return_value = success_result
        sync_async.forget.side_effect = RuntimeError("forget failed")

        batch_async = Mock(id="batch-forget-fails")
        batch_async.get.return_value = success_result
        batch_async.forget.side_effect = RuntimeError("forget failed")

        mock_app.send_task.side_effect = [sync_async, batch_async]
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1", debug=True)
        sync_value = client.sync([], {})
        submissions = client.batch_submit([([], {})])
        batch_value = client.batch_collect(submissions)

    assert sync_value == ([{"uri": "done"}], {"ok": True})
    assert batch_value[0].success is True
    assert batch_value[0].to_legacy_tuple() == ([{"uri": "done"}], {"ok": True})


def test_probe_and_ping_cover_submit_failure_retrieval_failure_and_error_envelope() -> None:
    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        retrieval_async = Mock(id="probe-retrieval-fail")
        retrieval_async.get.side_effect = RuntimeError("backend unavailable")

        envelope_async = Mock(id="probe-error-envelope")
        envelope_async.get.return_value = {
            "status": "error",
            "error": {
                "error_type": "ProcessingError",
                "error_message": "operator returned an error",
                "error_details": {"stage": "probe"},
            },
        }

        ping_async = Mock(id="probe-ping")
        ping_async.get.return_value = {"status": "success", "artifacts": [], "results": {}}

        mock_app.send_task.side_effect = [
            RuntimeError("submit exploded"),
            retrieval_async,
            envelope_async,
            ping_async,
        ]
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1")
        submit_fail = client.probe(timeout=2)
        retrieval_fail = client.probe(timeout=2)
        response_error = client.probe(timeout=2)
        ping_ok = client.ping(timeout=2)

    assert submit_fail.submit_ok is False
    assert submit_fail.retrieval_ok is False
    assert submit_fail.response_ok is False
    assert submit_fail.error == "submit exploded"

    assert retrieval_fail.submit_ok is True
    assert retrieval_fail.retrieval_ok is False
    assert retrieval_fail.response_ok is False
    assert retrieval_fail.task_id == "probe-retrieval-fail"
    assert retrieval_fail.error == "backend unavailable"

    assert response_error.submit_ok is True
    assert response_error.retrieval_ok is True
    assert response_error.response_ok is True
    assert response_error.error == "operator returned an error"

    assert ping_ok is True
    retrieval_async.forget.assert_called_once()
    envelope_async.forget.assert_called_once()
    ping_async.forget.assert_called_once()


def test_inspect_probe_result_rejects_invalid_payloads() -> None:
    ok, error = inspect_probe_result(["not", "a", "mapping"])
    assert ok is False
    assert "Invalid result format" in error

    ok, error = inspect_probe_result({"status": "partial"})
    assert ok is False
    assert error == "Invalid task status: 'partial'"


def test_auto_forget_can_be_disabled() -> None:
    success_result = {"status": "success", "artifacts": [], "results": {}}

    with _mock_client_runtime()[0] as mock_celery, _mock_client_runtime()[1], _mock_client_runtime()[2]:
        mock_app = Mock()
        async_result = Mock(id="no-forget")
        async_result.get.return_value = success_result
        mock_app.send_task.return_value = async_result
        mock_celery.return_value = mock_app

        client = OperatorClient("test-operator", "v1", auto_forget_result=False)
        client.sync([], {})

    async_result.forget.assert_not_called()


def test_async_types_expose_handle_helpers() -> None:
    transport = Mock()
    transport.get_result = Mock(return_value={"status": "success"})
    transport.forget = Mock()
    transport.result_exists = Mock(return_value=True)
    transport.get_state = Mock(return_value="SUCCESS")
    async_result = Mock(id="async-task-1")
    handle = AsyncTaskHandle(async_result=async_result, transport=transport)
    submitted = AsyncSubmittedTask(index=0, handle=handle)

    assert handle.task_id == "async-task-1"
    assert submitted.submitted is True
    assert submitted.task_id == "async-task-1"


def test_create_async_client_and_async_sync_path() -> None:
    config = resolve_client_config("test-operator", "v1")
    transport = Mock()
    transport.app = Mock()

    async def send_task(task_name, *, kwargs, queue):
        result = Mock(id="async-sync-task")
        return result

    async def get_result(async_result, *, timeout):
        return {"status": "success", "artifacts": [{"uri": "async://done"}], "results": {"ok": True}}

    async def forget(async_result):
        return None

    async def aclose():
        return None

    transport.send_task.side_effect = send_task
    transport.get_result.side_effect = get_result
    transport.forget.side_effect = forget
    transport.aclose.side_effect = aclose

    async def scenario() -> None:
        with patch("sandai.operator_client.client.resolve_client_config", return_value=config), patch(
            "sandai.operator_client.client.AsyncTransportAdapter", return_value=transport
        ):
            client = AsyncOperatorClient("test-operator", "v1")
            created = create_async_client("test-operator", "v1")
            assert created.timeout == 28800
            artifacts, results = await client.sync([], {})
            assert artifacts == [{"uri": "async://done"}]
            assert results == {"ok": True}
            await client.aclose()

    asyncio.run(scenario())


def test_async_desc_batch_and_probe_paths() -> None:
    config = resolve_client_config("test-operator", "v1")
    transport = Mock()
    transport.app = Mock()

    send_results = [Mock(id="desc-task"), Mock(id="batch-1"), Mock(id="batch-2"), Mock(id="probe-task")]

    async def send_task(task_name, *, kwargs, queue):
        return send_results.pop(0)

    async def get_result(async_result, *, timeout):
        mapping = {
            "desc-task": {"status": "success", "doc": "hello", "options_schema": {}, "results_schema": {}},
            "batch-1": {"status": "success", "artifacts": [{"uri": "batch://1"}], "results": {"i": 1}},
            "batch-2": {"status": "success", "artifacts": [{"uri": "batch://2"}], "results": {"i": 2}},
            "probe-task": {"status": "success", "artifacts": [], "results": {}},
        }
        return mapping[async_result.id]

    async def forget(async_result):
        return None

    async def aclose():
        return None

    transport.send_task.side_effect = send_task
    transport.get_result.side_effect = get_result
    transport.forget.side_effect = forget
    transport.aclose.side_effect = aclose

    async def scenario() -> None:
        with patch("sandai.operator_client.client.resolve_client_config", return_value=config), patch(
            "sandai.operator_client.client.AsyncTransportAdapter", return_value=transport
        ):
            client = AsyncOperatorClient("test-operator", "v1")
            first = await client.desc()
            second = await client.desc()
            batch = await client.batch_sync([
                ([], {"i": 1}),
                ([], {"i": 2}),
            ])
            probe = await client.probe()
            assert first == second
            assert first["doc"] == "hello"
            assert batch == [([{"uri": "batch://1"}], {"i": 1}), ([{"uri": "batch://2"}], {"i": 2})]
            assert probe.submit_ok is True
            assert probe.retrieval_ok is True
            assert probe.response_ok is True
            await client.aclose()

    asyncio.run(scenario())


def main() -> int:
    tests = [
        ("create_client", test_create_client_and_default_timeout),
        ("resolve_config", test_resolve_client_config_honors_explicit_values_and_env_defaults),
        ("resolve_config_env_serializers", test_resolve_client_config_honors_env_serializers),
        ("env_bool", test_env_bool_parses_truthy_and_falsy_values),
        ("invalid_priority", test_invalid_priority_raises_domain_error),
        ("async_payload", test_async_call_sets_deadline_cancel_key_and_persistent_output),
        ("build_payload_without_deadline", test_build_task_payload_omits_deadline_when_no_timeout_is_effective),
        ("sync_success", test_sync_success_path_returns_artifacts_and_results),
        ("sync_error_mapping", test_sync_maps_deadline_cancel_and_generic_errors),
        ("sync_invalid_shapes", test_sync_raises_invalid_status_and_invalid_format),
        ("sync_timeout", test_sync_maps_timeout_exceptions),
        ("transport_get_result", test_transport_get_result_keeps_envelopes_and_raises_backend_exceptions),
        ("raise_task_error_non_mapping", test_raise_task_error_normalizes_non_mapping_payloads),
        ("raise_task_error_non_dict_details", test_raise_task_error_wraps_non_dict_error_details),
        ("desc_success", test_desc_is_cached_and_forgets_result),
        ("desc_invalid_status", test_desc_rejects_invalid_status),
        ("desc_error_mapping", test_desc_maps_timeout_and_error_payloads),
        ("types_legacy_helpers", test_types_expose_legacy_compatibility_helpers),
        ("structured_batch_probe", test_batch_submit_collect_and_probe_expose_structured_status),
        ("batch", test_batch_async_and_batch_sync_preserve_order_and_partial_failures),
        ("forget_failures_do_not_override_success", test_sync_and_batch_collect_ignore_forget_failures_after_success),
        ("probe_and_ping_failures", test_probe_and_ping_cover_submit_failure_retrieval_failure_and_error_envelope),
        ("inspect_probe_invalid_payloads", test_inspect_probe_result_rejects_invalid_payloads),
        ("auto_forget", test_auto_forget_can_be_disabled),
        ("async_types", test_async_types_expose_handle_helpers),
        ("async_sync", test_create_async_client_and_async_sync_path),
        ("async_desc_batch_probe", test_async_desc_batch_and_probe_paths),
    ]

    passed = 0
    failed = 0

    print("Operator-client regression tests")
    print("=" * 60)
    for name, test_func in tests:
        try:
            test_func()
            print(f"PASS {name}")
            passed += 1
        except Exception as exc:
            print(f"FAIL {name}: {exc}")
            import traceback

            traceback.print_exc()
            failed += 1
    print("=" * 60)
    print(f"passed={passed} failed={failed}")
    return 0 if failed == 0 else 1


if __name__ == "__main__":
    raise SystemExit(main())
